Merge "Gluon RBAC using keystone and oslo.policy"
This commit is contained in:
commit
ce980f3865
@ -1,4 +1,5 @@
|
||||
# Copyright 2016, Ericsson AB
|
||||
# Copyright 2017, Nokia
|
||||
#
|
||||
# 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,18 +13,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from keystonemiddleware import auth_token
|
||||
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.api import hooks
|
||||
from gluon.common import exception as g_exc
|
||||
|
||||
|
||||
# TODO(enikher)
|
||||
# from gluon.api import middleware
|
||||
|
||||
@ -44,16 +46,16 @@ app_dic = {'root': 'gluon.api.root.RootController',
|
||||
|
||||
|
||||
def setup_app(config=None):
|
||||
# app_hooks = [
|
||||
# hooks.PolicyHook(),
|
||||
# hooks.ContextHook()
|
||||
# ]
|
||||
app_hooks = [
|
||||
hooks.ContextHook(),
|
||||
hooks.PolicyHook()
|
||||
]
|
||||
|
||||
app = pecan.make_app(
|
||||
app_dic.pop('root'),
|
||||
logging=getattr(config, 'logging', {}),
|
||||
# wrap_app=_wrap_app,
|
||||
# hooks=app_hooks,
|
||||
wrap_app=_wrap_app,
|
||||
hooks=app_hooks,
|
||||
# TODO(enikher)
|
||||
# wrap_app=middleware.ParsableErrorMiddleware,
|
||||
**app_dic
|
||||
@ -73,9 +75,9 @@ def setup_app(config=None):
|
||||
def _wrap_app(app):
|
||||
app = request_id.RequestId(app)
|
||||
|
||||
if CONF.auth_strategy == 'noauth':
|
||||
if CONF.api.auth_strategy == 'noauth':
|
||||
pass
|
||||
elif CONF.auth_strategy == 'keystone':
|
||||
elif CONF.api.auth_strategy == 'keystone':
|
||||
app = auth_token.AuthProtocol(app, {})
|
||||
LOG.info("Keystone authentication is enabled")
|
||||
else:
|
||||
|
@ -14,18 +14,19 @@
|
||||
|
||||
import datetime
|
||||
import six
|
||||
|
||||
from pecan import rest
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from pecan import expose
|
||||
from pecan import rest
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from gluon.db import api as dbapi
|
||||
from gluon.managers.manager_base import get_api_manager
|
||||
|
||||
|
||||
class APIBase(wtypes.Base):
|
||||
|
||||
# TBD
|
||||
created_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||
"""The time in UTC at which the object is created"""
|
||||
@ -59,7 +60,6 @@ class APIBase(wtypes.Base):
|
||||
|
||||
|
||||
class APIBaseObject(APIBase):
|
||||
|
||||
@classmethod
|
||||
def class_builder(base_cls, name, _db_model, attributes):
|
||||
new_cls = type(name, (base_cls,), attributes)
|
||||
@ -107,7 +107,6 @@ class APIBaseObject(APIBase):
|
||||
|
||||
|
||||
class APIBaseList(APIBase):
|
||||
|
||||
@classmethod
|
||||
def class_builder(base_cls, name, list_name, api_object_class):
|
||||
new_cls = type(name, (base_cls,), {list_name: [api_object_class]})
|
||||
@ -133,6 +132,7 @@ class RootObjectController(rest.RestController):
|
||||
def class_builder(base_cls, name, api_obj_class, primary_key_type,
|
||||
api_name):
|
||||
new_cls = type(name, (base_cls,), {})
|
||||
new_cls.resource_name = name
|
||||
new_cls.list_object_class = APIBaseList.class_builder(name + 'List',
|
||||
name,
|
||||
api_obj_class)
|
||||
@ -143,6 +143,7 @@ class RootObjectController(rest.RestController):
|
||||
@wsme_pecan.wsexpose(new_cls.list_object_class, template='json')
|
||||
def get_all(self):
|
||||
return self.list_object_class.build()
|
||||
|
||||
new_cls.get_all = classmethod(get_all)
|
||||
|
||||
@wsme_pecan.wsexpose(new_cls.api_object_class,
|
||||
@ -150,6 +151,7 @@ class RootObjectController(rest.RestController):
|
||||
template='json')
|
||||
def get_one(self, key):
|
||||
return self.api_object_class.get_from_db(key)
|
||||
|
||||
new_cls.get_one = classmethod(get_one)
|
||||
|
||||
@wsme_pecan.wsexpose(new_cls.api_object_class,
|
||||
@ -157,6 +159,7 @@ class RootObjectController(rest.RestController):
|
||||
status_code=201)
|
||||
def post(self, body):
|
||||
return self.api_mgr.handle_create(self, body.as_dict())
|
||||
|
||||
new_cls.post = classmethod(post)
|
||||
|
||||
@wsme_pecan.wsexpose(new_cls.api_object_class,
|
||||
@ -164,15 +167,29 @@ class RootObjectController(rest.RestController):
|
||||
body=new_cls.api_object_class, template='json')
|
||||
def put(self, key, body):
|
||||
return self.api_mgr.handle_update(self, key, body.as_dict())
|
||||
|
||||
new_cls.put = classmethod(put)
|
||||
|
||||
@wsme_pecan.wsexpose(None, new_cls.primary_key_type, template='json')
|
||||
def delete(self, key):
|
||||
return self.api_mgr.handle_delete(self, key)
|
||||
|
||||
new_cls.delete = classmethod(delete)
|
||||
|
||||
return new_cls
|
||||
|
||||
@expose()
|
||||
def _route(self, args, request):
|
||||
result = super(RootObjectController, self)._route(args, request)
|
||||
request.context['resource'] = result[0].im_self.resource_name
|
||||
return result
|
||||
# @expose()
|
||||
# def _lookup(self, collection, *remainder):
|
||||
# #Set resource_action in the context to denote that
|
||||
# #this is a show operation and not list
|
||||
# request.context['resource_action'] = 'show'
|
||||
# return self
|
||||
|
||||
# TODO(hambtw) Needs to be reworked
|
||||
# class SubObjectController(RootObjectController):
|
||||
#
|
||||
|
@ -13,14 +13,17 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import webob
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import policy as oslo_policy
|
||||
from oslo_utils import excutils
|
||||
from pecan import hooks
|
||||
import webob
|
||||
|
||||
from gluon import constants as gluon_constants
|
||||
from gluon import policy
|
||||
|
||||
from gluon._i18n import _
|
||||
from gluon import constants
|
||||
from gluon import policy
|
||||
|
||||
|
||||
class PolicyHook(hooks.PecanHook):
|
||||
@ -28,140 +31,37 @@ class PolicyHook(hooks.PecanHook):
|
||||
|
||||
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'):
|
||||
if cfg.CONF.api.auth_strategy == 'noauth':
|
||||
return
|
||||
|
||||
if state.request.method not in ('GET', 'POST', 'PUT', 'DELETE'):
|
||||
return
|
||||
|
||||
method = gluon_constants.ACTION_MAP[state.request.method]
|
||||
|
||||
path_info = state.request.path_info
|
||||
|
||||
if not path_info:
|
||||
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]
|
||||
action = "%s_%s" % (method, resource)
|
||||
|
||||
# if not controller or utils.is_member_action(controller):
|
||||
# return
|
||||
gluon_context = state.request.context.get('gluon_context')
|
||||
|
||||
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))]
|
||||
gluon_context, action, None)
|
||||
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
|
||||
def after(self, state):
|
||||
# This method could be used for implementing access control
|
||||
# at the attribute level.
|
||||
return
|
||||
|
@ -29,7 +29,10 @@ API_SERVICE_OPTS = [
|
||||
help='etcd host'),
|
||||
cfg.IntOpt('etcd_port',
|
||||
default=2379,
|
||||
help='etcd port')
|
||||
help='etcd port'),
|
||||
cfg.StrOpt('auth_strategy',
|
||||
default='noauth',
|
||||
help='the type of authentication to use')
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
Loading…
Reference in New Issue
Block a user