Gluon RBAC using keystone and oslo.policy

Contains code for intergrating keystone and
oslo.policy with Gluon.

Change-Id: Ib619a554a131d6e96cf0ce1312b2844593eb8c83
Implements: blueprint gluon-auth
This commit is contained in:
Kamal Hussain 2017-01-16 16:46:57 -06:00
parent 5cda5853c3
commit 2bcb838b29
4 changed files with 64 additions and 142 deletions

View File

@ -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:

View File

@ -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):
#

View File

@ -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))]
policy.enforce(
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

View File

@ -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