Merge "Gluon RBAC using keystone and oslo.policy"

This commit is contained in:
Jenkins
2016-12-22 04:15:24 +00:00
committed by Gerrit Code Review
13 changed files with 1101 additions and 3 deletions

View File

@@ -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
View 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'
}

View 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

View 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

View 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

View File

View 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, '')