Merge "Gluon RBAC using keystone and oslo.policy"
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Copyright 2015, Ericsson AB
|
||||
# Copyright 2016, Ericsson AB
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
@@ -12,11 +12,25 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import pecan
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from oslo_middleware import cors
|
||||
from oslo_middleware import http_proxy_to_wsgi
|
||||
from oslo_middleware import request_id
|
||||
|
||||
from gluon.common import exception as g_exc
|
||||
|
||||
# TODO(enikher)
|
||||
# from gluon.api import middleware
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
app_dic = {'root': 'gluon.api.root.RootController',
|
||||
'modules': ['gluon.api'],
|
||||
'debug': True,
|
||||
@@ -30,10 +44,16 @@ app_dic = {'root': 'gluon.api.root.RootController',
|
||||
|
||||
|
||||
def setup_app(config=None):
|
||||
# app_hooks = [
|
||||
# hooks.PolicyHook(),
|
||||
# hooks.ContextHook()
|
||||
# ]
|
||||
|
||||
app = pecan.make_app(
|
||||
app_dic.pop('root'),
|
||||
logging=getattr(config, 'logging', {}),
|
||||
# wrap_app=_wrap_app,
|
||||
# hooks=app_hooks,
|
||||
# TODO(enikher)
|
||||
# wrap_app=middleware.ParsableErrorMiddleware,
|
||||
**app_dic
|
||||
@@ -47,3 +67,42 @@ def setup_app(config=None):
|
||||
# TODO(enikher) add authentication
|
||||
# return auth.install(app, CONF, config.app.acl_public_routes)
|
||||
return app
|
||||
|
||||
|
||||
# adapted from Neutron code
|
||||
def _wrap_app(app):
|
||||
app = request_id.RequestId(app)
|
||||
|
||||
if CONF.auth_strategy == 'noauth':
|
||||
pass
|
||||
elif CONF.auth_strategy == 'keystone':
|
||||
app = auth_token.AuthProtocol(app, {})
|
||||
LOG.info("Keystone authentication is enabled")
|
||||
else:
|
||||
raise g_exc.InvalidConfigurationOption(
|
||||
opt_name='auth_strategy', opt_value=CONF.auth_strategy)
|
||||
|
||||
# dont bother authenticating version
|
||||
# app = versions.Versions(app)
|
||||
|
||||
# gluon server is behind the proxy
|
||||
app = http_proxy_to_wsgi.HTTPProxyToWSGI(app)
|
||||
|
||||
# This should be the last middleware in the list (which results in
|
||||
# it being the first in the middleware chain). This is to ensure
|
||||
# that any errors thrown by other middleware, such as an auth
|
||||
# middleware - are annotated with CORS headers, and thus accessible
|
||||
# by the browser.
|
||||
app = cors.CORS(app, CONF)
|
||||
app.set_latent(
|
||||
allow_headers=['X-Auth-Token', 'X-Identity-Status', 'X-Roles',
|
||||
'X-Service-Catalog', 'X-User-Id', 'X-Tenant-Id',
|
||||
'X-OpenStack-Request-ID',
|
||||
'X-Trace-Info', 'X-Trace-HMAC'],
|
||||
allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
|
||||
expose_headers=['X-Auth-Token', 'X-Subject-Token', 'X-Service-Token',
|
||||
'X-OpenStack-Request-ID',
|
||||
'X-Trace-Info', 'X-Trace-HMAC']
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
64
gluon/api/attributes.py
Normal file
64
gluon/api/attributes.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2016 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
# Defining a constant to avoid repeating string literal in several modules
|
||||
SHARED = 'shared'
|
||||
|
||||
NAME_MAX_LEN = 255
|
||||
TENANT_ID_MAX_LEN = 255
|
||||
DESCRIPTION_MAX_LEN = 255
|
||||
LONG_DESCRIPTION_MAX_LEN = 1024
|
||||
DEVICE_ID_MAX_LEN = 255
|
||||
DEVICE_OWNER_MAX_LEN = 255
|
||||
|
||||
# Define constants for base resource name
|
||||
BASEPORT = 'baseport'
|
||||
BASEPORTS = '%ss' % BASEPORT
|
||||
|
||||
# TODO(kh) RBAC at attribute level is enforced
|
||||
# Note: a default of ATTR_NOT_SPECIFIED indicates that an
|
||||
# attribute is not required, but will be generated by the plugin
|
||||
# if it is not specified. Particularly, a value of ATTR_NOT_SPECIFIED
|
||||
# is different from an attribute that has been specified with a value of
|
||||
# None. For example, if 'gateway_ip' is omitted in a request to
|
||||
# create a subnet, the plugin will receive ATTR_NOT_SPECIFIED
|
||||
# and the default gateway_ip will be generated.
|
||||
# However, if gateway_ip is specified as None, this means that
|
||||
# the subnet does not have a gateway IP.
|
||||
# The following is a short reference for understanding attribute info:
|
||||
# default: default value of the attribute (if missing, the attribute
|
||||
# becomes mandatory.
|
||||
# allow_post: the attribute can be used on POST requests.
|
||||
# allow_put: the attribute can be used on PUT requests.
|
||||
# validate: specifies rules for validating data in the attribute.
|
||||
# convert_to: transformation to apply to the value before it is returned
|
||||
# is_visible: the attribute is returned in GET responses.
|
||||
# required_by_policy: the attribute is required by the policy engine and
|
||||
# should therefore be filled by the API layer even if not present in
|
||||
# request body.
|
||||
# enforce_policy: the attribute is actively part of the policy enforcing
|
||||
# mechanism, ie: there might be rules which refer to this attribute.
|
||||
|
||||
RESOURCE_ATTRIBUTE_MAP = {
|
||||
BASEPORTS: {
|
||||
}
|
||||
}
|
||||
|
||||
# Identify the attribute used by a resource to reference another resource
|
||||
|
||||
RESOURCE_FOREIGN_KEYS = {
|
||||
BASEPORT: 'id'
|
||||
}
|
||||
21
gluon/api/hooks/__init__.py
Normal file
21
gluon/api/hooks/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2016 Nokia
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from gluon.api.hooks import context
|
||||
from gluon.api.hooks import policy_enforcement
|
||||
|
||||
|
||||
ContextHook = context.ContextHook
|
||||
PolicyHook = policy_enforcement.PolicyHook
|
||||
55
gluon/api/hooks/context.py
Normal file
55
gluon/api/hooks/context.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright 2016 Nokia
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_middleware import request_id
|
||||
from pecan import hooks
|
||||
|
||||
from gluon import context
|
||||
|
||||
|
||||
class ContextHook(hooks.PecanHook):
|
||||
"""Configures a request context and attaches it to the request.
|
||||
|
||||
The following HTTP request headers are used:
|
||||
X-User-Id or X-User:
|
||||
Used for context.user_id.
|
||||
X-Project-Id:
|
||||
Used for context.tenant_id.
|
||||
X-Project-Name:
|
||||
Used for context.tenant_name.
|
||||
X-Auth-Token:
|
||||
Used for context.auth_token.
|
||||
X-Roles:
|
||||
Used for setting context.is_admin flag to either True or False.
|
||||
The flag is set to True, if X-Roles contains either an administrator
|
||||
or admin substring. Otherwise it is set to False.
|
||||
|
||||
"""
|
||||
|
||||
priority = 50
|
||||
|
||||
def before(self, state):
|
||||
user_name = state.request.headers.get('X-User-Name', '')
|
||||
tenant_name = state.request.headers.get('X-Project-Name')
|
||||
req_id = state.request.headers.get(request_id.ENV_REQUEST_ID)
|
||||
|
||||
# Create a context with the authentication data
|
||||
ctx = context.Context.from_environ(state.request.environ,
|
||||
user_name=user_name,
|
||||
tenant_name=tenant_name,
|
||||
request_id=req_id)
|
||||
|
||||
# Inject the context...
|
||||
state.request.context['gluon_context'] = ctx
|
||||
167
gluon/api/hooks/policy_enforcement.py
Normal file
167
gluon/api/hooks/policy_enforcement.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Copyright 2016 Nokia
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_policy import policy as oslo_policy
|
||||
from oslo_utils import excutils
|
||||
from pecan import hooks
|
||||
import webob
|
||||
|
||||
from gluon._i18n import _
|
||||
from gluon import constants
|
||||
from gluon import policy
|
||||
|
||||
|
||||
class PolicyHook(hooks.PecanHook):
|
||||
priority = 100
|
||||
|
||||
def before(self, state):
|
||||
|
||||
# This hook should be run only for PUT,POST and DELETE methods
|
||||
resources = state.request.context.get('resources', [])
|
||||
|
||||
if state.request.method not in ('POST', 'PUT', 'DELETE'):
|
||||
return
|
||||
|
||||
# As this routine will likely alter the resources, do a shallow copy
|
||||
resources_copy = resources[:]
|
||||
gluon_context = state.request.context.get('gluon_context')
|
||||
resource = state.request.context.get('resource')
|
||||
|
||||
# If there is no resource for this request, don't bother running authZ
|
||||
# policies
|
||||
if not resource:
|
||||
return
|
||||
|
||||
# controller = utils.get_controller(state)
|
||||
controller = state.arguments.args[0]
|
||||
|
||||
# if not controller or utils.is_member_action(controller):
|
||||
# return
|
||||
|
||||
collection = state.request.context.get('collection')
|
||||
needs_prefetch = (state.request.method == 'PUT' or
|
||||
state.request.method == 'DELETE')
|
||||
policy.init()
|
||||
|
||||
action = controller.plugin_handlers[
|
||||
constants.ACTION_MAP[state.request.method]]
|
||||
|
||||
for item in resources_copy:
|
||||
try:
|
||||
policy.enforce(
|
||||
gluon_context, action, item,
|
||||
pluralized=collection)
|
||||
except oslo_policy.PolicyNotAuthorized:
|
||||
with excutils.save_and_reraise_exception() as ctxt:
|
||||
# If a tenant is modifying it's own object, it's safe to
|
||||
# return a 403. Otherwise, pretend that it doesn't exist
|
||||
# to avoid giving away information.
|
||||
orig_item_tenant_id = item.get('tenant_id')
|
||||
if (needs_prefetch and
|
||||
(gluon_context.tenant_id != orig_item_tenant_id or
|
||||
orig_item_tenant_id is None)):
|
||||
ctxt.reraise = False
|
||||
msg = _('The resource could not be found.')
|
||||
raise webob.exc.HTTPNotFound(msg)
|
||||
|
||||
def after(self, state):
|
||||
gluon_context = state.request.context.get('gluon_context')
|
||||
resource = state.request.context.get('resource')
|
||||
collection = state.request.context.get('collection')
|
||||
# = utils.get_controller(state)
|
||||
controller = state.arguments.args[0]
|
||||
|
||||
if not resource:
|
||||
# can't filter a resource we don't recognize
|
||||
return
|
||||
|
||||
if resource == 'extension':
|
||||
return
|
||||
try:
|
||||
data = state.response.json
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if state.request.method not in constants.ACTION_MAP:
|
||||
return
|
||||
|
||||
action = '%s_%s' % (constants.ACTION_MAP[state.request.method],
|
||||
resource)
|
||||
|
||||
if not data or (resource not in data and collection not in data):
|
||||
return
|
||||
|
||||
is_single = resource in data
|
||||
key = resource if is_single else collection
|
||||
to_process = [data[resource]] if is_single else data[collection]
|
||||
|
||||
# in the single case, we enforce which raises on violation
|
||||
# in the plural case, we just check so violating items are hidden
|
||||
policy_method = policy.enforce if is_single else policy.check
|
||||
|
||||
try:
|
||||
resp = [self._get_filtered_item(state.request, controller,
|
||||
resource, collection, item)
|
||||
for item in to_process
|
||||
if (state.request.method != 'GET' or
|
||||
policy_method(gluon_context, action, item,
|
||||
pluralized=collection))]
|
||||
except oslo_policy.PolicyNotAuthorized as e:
|
||||
# This exception must be explicitly caught as the exception
|
||||
# translation hook won't be called if an error occurs in the
|
||||
# 'after' handler.
|
||||
raise webob.exc.HTTPForbidden(str(e))
|
||||
|
||||
if is_single:
|
||||
resp = resp[0]
|
||||
state.response.json = {key: resp}
|
||||
|
||||
def _get_filtered_item(self, request, controller, resource, collection,
|
||||
data):
|
||||
gluon_context = request.context.get('gluon_context')
|
||||
to_exclude = self._exclude_attributes_by_policy(
|
||||
gluon_context, controller, resource, collection, data)
|
||||
return self._filter_attributes(request, data, to_exclude)
|
||||
|
||||
def _filter_attributes(self, request, data, fields_to_strip):
|
||||
# This routine will remove the fields that were requested to the
|
||||
# plugin for policy evaluation but were not specified in the
|
||||
# API request
|
||||
user_fields = request.params.getall('fields')
|
||||
return dict(item for item in data.items()
|
||||
if (item[0] not in fields_to_strip and
|
||||
(not user_fields or item[0] in user_fields)))
|
||||
|
||||
def _exclude_attributes_by_policy(self, context, controller, resource,
|
||||
collection, data):
|
||||
"""Identifies attributes to exclude according to authZ policies.
|
||||
|
||||
Return a list of attribute names which should be stripped from the
|
||||
response returned to the user because the user is not authorized
|
||||
to see them.
|
||||
"""
|
||||
attributes_to_exclude = []
|
||||
for attr_name in data.keys():
|
||||
attr_data = controller.resource_info.get(attr_name)
|
||||
if attr_data and attr_data['is_visible']:
|
||||
if policy.check(context, 'get_%s:%s' % (resource, attr_name),
|
||||
data, might_not_exist=True,
|
||||
pluralized=collection):
|
||||
# this attribute is visible, check next one
|
||||
continue
|
||||
# if the code reaches this point then either the policy check
|
||||
# failed or the attribute was not visible in the first place
|
||||
attributes_to_exclude.append(attr_name)
|
||||
return attributes_to_exclude
|
||||
0
gluon/api/views/__init__.py
Normal file
0
gluon/api/views/__init__.py
Normal file
58
gluon/api/views/versions.py
Normal file
58
gluon/api/views/versions.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def get_view_builder(req):
|
||||
base_url = req.application_url
|
||||
return ViewBuilder(base_url)
|
||||
|
||||
|
||||
class ViewBuilder(object):
|
||||
|
||||
def __init__(self, base_url):
|
||||
"""Object initialization.
|
||||
|
||||
:param base_url: url of the root wsgi application
|
||||
"""
|
||||
self.base_url = base_url
|
||||
|
||||
def build(self, version_data):
|
||||
"""Generic method used to generate a version entity."""
|
||||
version = {
|
||||
"id": version_data["id"],
|
||||
"status": version_data["status"],
|
||||
"links": self._build_links(version_data),
|
||||
}
|
||||
|
||||
return version
|
||||
|
||||
def _build_links(self, version_data):
|
||||
"""Generate a container of links that refer to the provided version."""
|
||||
href = self.generate_href(version_data["id"])
|
||||
|
||||
links = [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": href,
|
||||
},
|
||||
]
|
||||
|
||||
return links
|
||||
|
||||
def generate_href(self, version_number):
|
||||
"""Create an url that refers to a specific version_number."""
|
||||
return os.path.join(self.base_url, version_number, '')
|
||||
Reference in New Issue
Block a user