add policy support

Add policy support to determine which user can access which objects
in which way

Change-Id: If959089366ec252d4a7904d0e78733a2bf52fff5
This commit is contained in:
zhuli 2017-08-25 18:30:15 +08:00
parent 57e4c042ca
commit c6c5ca042f
14 changed files with 329 additions and 3 deletions

3
.gitignore vendored
View File

@ -2,3 +2,6 @@
*.tox *.tox
*.testrepository *.testrepository
cyborg.egg-info cyborg.egg-info
# Sample profile
etc/cyborg/policy.json.sample

View File

@ -23,6 +23,7 @@ from cyborg.api.controllers import base
from cyborg.api.controllers import link from cyborg.api.controllers import link
from cyborg.api.controllers.v1 import types from cyborg.api.controllers.v1 import types
from cyborg.api import expose from cyborg.api import expose
from cyborg.common import policy
from cyborg import objects from cyborg import objects
@ -37,6 +38,8 @@ class Accelerator(base.APIBase):
uuid = types.uuid uuid = types.uuid
name = wtypes.text name = wtypes.text
description = wtypes.text description = wtypes.text
project_id = types.uuid
user_id = types.uuid
device_type = wtypes.text device_type = wtypes.text
acc_type = wtypes.text acc_type = wtypes.text
acc_capability = wtypes.text acc_capability = wtypes.text
@ -67,9 +70,16 @@ class Accelerator(base.APIBase):
return accelerator return accelerator
class AcceleratorsController(rest.RestController): class AcceleratorsControllerBase(rest.RestController):
def _get_resource(self, uuid):
self._resource = objects.Accelerator.get(pecan.request.context, uuid)
return self._resource
class AcceleratorsController(AcceleratorsControllerBase):
"""REST controller for Accelerators.""" """REST controller for Accelerators."""
@policy.authorize_wsgi("cyborg:accelerator", "create", False)
@expose.expose(Accelerator, body=types.jsontype, @expose.expose(Accelerator, body=types.jsontype,
status_code=http_client.CREATED) status_code=http_client.CREATED)
def post(self, values): def post(self, values):

View File

@ -17,6 +17,7 @@ from oslo_config import cfg
from oslo_context import context from oslo_context import context
from pecan import hooks from pecan import hooks
from cyborg.common import policy
from cyborg.conductor import rpcapi from cyborg.conductor import rpcapi
@ -50,11 +51,53 @@ class ConductorAPIHook(hooks.PecanHook):
class ContextHook(hooks.PecanHook): class ContextHook(hooks.PecanHook):
"""Configures a request context and attaches it to the request.""" """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.
X-Tenant-Id or X-Tenant:
Used for context.tenant.
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.
"""
def __init__(self, public_api_routes): def __init__(self, public_api_routes):
self.public_api_routes = public_api_routes self.public_api_routes = public_api_routes
super(ContextHook, self).__init__() super(ContextHook, self).__init__()
def before(self, state): def before(self, state):
state.request.context = context.get_admin_context() headers = state.request.headers
creds = {
'user_name': headers.get('X-User-Name'),
'user': headers.get('X-User-Id'),
'project_name': headers.get('X-Project-Name'),
'tenant': headers.get('X-Project-Id'),
'domain': headers.get('X-User-Domain-Id'),
'domain_name': headers.get('X-User-Domain-Name'),
'auth_token': headers.get('X-Auth-Token'),
'roles': headers.get('X-Roles', '').split(','),
}
is_admin = policy.authorize('is_admin', creds, creds)
state.request.context = context.RequestContext(
is_admin=is_admin, **creds)
def after(self, state):
if state.request.context == {}:
# An incorrect url path will not create RequestContext
return
# RequestContext will generate a request_id if no one
# passing outside, so it always contain a request_id.
request_id = state.request.context.request_id
state.response.headers['Openstack-Request-Id'] = request_id

View File

@ -109,3 +109,12 @@ class InvalidUUID(Invalid):
class InvalidJsonType(Invalid): class InvalidJsonType(Invalid):
_msg_fmt = _("%(value)s is not JSON serializable.") _msg_fmt = _("%(value)s is not JSON serializable.")
class NotAuthorized(CyborgException):
_msg_fmt = _("Not authorized.")
code = http_client.FORBIDDEN
class HTTPForbidden(NotAuthorized):
_msg_fmt = _("Access was denied to the following resource: %(resource)s")

234
cyborg/common/policy.py Normal file
View File

@ -0,0 +1,234 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.
"""Policy Engine For Cyborg."""
import functools
import sys
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log
from oslo_policy import policy
from oslo_versionedobjects import base as object_base
import pecan
import wsme
from cyborg.common import exception
_ENFORCER = None
CONF = cfg.CONF
LOG = log.getLogger(__name__)
default_policies = [
# Legacy setting, don't remove. Likely to be overridden by operators who
# forget to update their policy.json configuration file.
# This gets rolled into the new "is_admin" rule below.
policy.RuleDefault('admin_api',
'role:admin or role:administrator',
description='Legacy rule for cloud admin access'),
# is_public_api is set in the environment from AuthTokenMiddleware
policy.RuleDefault('public_api',
'is_public_api:True',
description='Internal flag for public API routes'),
# The policy check "@" will always accept an access. The empty list
# (``[]``) or the empty string (``""``) is equivalent to the "@"
policy.RuleDefault('allow',
'@',
description='any access will be passed'),
# the policy check "!" will always reject an access.
policy.RuleDefault('deny',
'!',
description='all access will be forbidden'),
policy.RuleDefault('is_admin',
'rule:admin_api',
description='Full read/write API access'),
policy.RuleDefault('admin_or_owner',
'is_admin:True or project_id:%(project_id)s',
description='Admin or owner API access'),
policy.RuleDefault('admin_or_user',
'is_admin:True or user_id:%(user_id)s',
description='Admin or user API access'),
policy.RuleDefault('default',
'rule:admin_or_owner',
description='Default API access rule'),
]
# NOTE: to follow policy-in-code spec, we define defaults for
# the granular policies in code, rather than in policy.json.
# All of these may be overridden by configuration, but we can
# depend on their existence throughout the code.
accelerator_policies = [
policy.RuleDefault('cyborg:accelerator:get',
'rule:default',
description='Retrieve accelerator records'),
policy.RuleDefault('cyborg:accelerator:create',
'rule:allow',
description='Create accelerator records'),
policy.RuleDefault('cyborg:accelerator:delete',
'rule:default',
description='Delete accelerator records'),
policy.RuleDefault('cyborg:accelerator:update',
'rule:default',
description='Update accelerator records'),
]
def list_policies():
return default_policies + accelerator_policies
@lockutils.synchronized('policy_enforcer', 'cyborg-')
def init_enforcer(policy_file=None, rules=None,
default_rule=None, use_conf=True):
"""Synchronously initializes the policy enforcer
:param policy_file: Custom policy file to use, if none is specified,
`CONF.oslo_policy.policy_file` will be used.
:param rules: Default dictionary / Rules to use. It will be
considered just in the first instantiation.
:param default_rule: Default rule to use,
CONF.oslo_policy.policy_default_rule will
be used if none is specified.
:param use_conf: Whether to load rules from config file.
"""
global _ENFORCER
if _ENFORCER:
return
# NOTE: Register defaults for policy-in-code here so that they are
# loaded exactly once - when this module-global is initialized.
# Defining these in the relevant API modules won't work
# because API classes lack singletons and don't use globals.
_ENFORCER = policy.Enforcer(CONF, policy_file=policy_file,
rules=rules,
default_rule=default_rule,
use_conf=use_conf)
_ENFORCER.register_defaults(list_policies())
def get_enforcer():
"""Provides access to the single accelerator of policy enforcer."""
global _ENFORCER
if not _ENFORCER:
init_enforcer()
return _ENFORCER
# NOTE: We can't call these methods from within decorators because the
# 'target' and 'creds' parameter must be fetched from the call time
# context-local pecan.request magic variable, but decorators are compiled
# at module-load time.
def authorize(rule, target, creds, do_raise=False, *args, **kwargs):
"""A shortcut for policy.Enforcer.authorize()
Checks authorization of a rule against the target and credentials, and
raises an exception if the rule is not defined.
"""
enforcer = get_enforcer()
try:
return enforcer.authorize(rule, target, creds, do_raise=do_raise,
*args, **kwargs)
except policy.PolicyNotAuthorized:
raise exception.HTTPForbidden(resource=rule)
# This decorator MUST appear first (the outermost decorator)
# on an API method for it to work correctly
def authorize_wsgi(api_name, act=None, need_target=True):
"""This is a decorator to simplify wsgi action policy rule check.
:param api_name: The collection name to be evaluate.
:param act: The function name of wsgi action.
:param need_target: Whether need target for authorization. Such as,
when create some resource , maybe target is not needed.
example:
from cyborg.common import policy
class AcceleratorController(rest.RestController):
....
@policy.authorize_wsgi("cyborg:accelerator", "create", False)
@wsme_pecan.wsexpose(Accelerator, body=types.jsontype,
status_code=http_client.CREATED)
def post(self, values):
...
"""
def wraper(fn):
action = '%s:%s' % (api_name, act or fn.__name__)
# In this authorize method, we return a dict data when authorization
# fails or exception comes out. Maybe we can consider to use
# wsme.api.Response in future.
def return_error(resp_status):
exception_info = sys.exc_info()
orig_exception = exception_info[1]
orig_code = getattr(orig_exception, 'code', None)
pecan.response.status = orig_code or resp_status
data = wsme.api.format_exception(
exception_info,
pecan.conf.get('wsme', {}).get('debug', False)
)
del exception_info
return data
@functools.wraps(fn)
def handle(self, *args, **kwargs):
context = pecan.request.context
credentials = context.to_policy_values()
credentials['is_admin'] = context.is_admin
target = {}
# maybe we can pass "_get_resource" to authorize_wsgi
if need_target and hasattr(self, "_get_resource"):
try:
resource = getattr(self, "_get_resource")(*args, **kwargs)
# just support object, other type will just keep target as
# empty, then follow authorize method will fail and throw
# an exception
if isinstance(resource,
object_base.VersionedObjectDictCompat):
target = {'project_id': resource.project_id,
'user_id': resource.user_id}
except Exception:
return return_error(500)
elif need_target:
# if developer do not set _get_resource, just set target as
# empty, then follow authorize method will fail and throw an
# exception
target = {}
else:
# for create method, before resource exsites, we can check the
# the credentials with itself.
target = {'project_id': context.tenant,
'user_id': context.user}
try:
authorize(action, target, credentials, do_raise=True)
except Exception:
return return_error(403)
return fn(self, *args, **kwargs)
return handle
return wraper

View File

@ -37,6 +37,8 @@ def upgrade():
sa.Column('uuid', sa.String(length=36), nullable=False), sa.Column('uuid', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True), sa.Column('description', sa.Text(), nullable=True),
sa.Column('project_id', sa.String(length=36), nullable=True),
sa.Column('user_id', sa.String(length=36), nullable=True),
sa.Column('device_type', sa.Text(), nullable=False), sa.Column('device_type', sa.Text(), nullable=False),
sa.Column('acc_type', sa.Text(), nullable=False), sa.Column('acc_type', sa.Text(), nullable=False),
sa.Column('acc_capability', sa.Text(), nullable=False), sa.Column('acc_capability', sa.Text(), nullable=False),

View File

@ -64,6 +64,8 @@ class Accelerator(Base):
uuid = Column(String(36), nullable=False) uuid = Column(String(36), nullable=False)
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
description = Column(String(255), nullable=True) description = Column(String(255), nullable=True)
project_id = Column(String(36), nullable=True)
user_id = Column(String(36), nullable=True)
device_type = Column(String(255), nullable=False) device_type = Column(String(255), nullable=False)
acc_type = Column(String(255), nullable=False) acc_type = Column(String(255), nullable=False)
acc_capability = Column(String(255), nullable=False) acc_capability = Column(String(255), nullable=False)

View File

@ -31,6 +31,8 @@ class Accelerator(base.CyborgObject, object_base.VersionedObjectDictCompat):
'uuid': object_fields.UUIDField(nullable=False), 'uuid': object_fields.UUIDField(nullable=False),
'name': object_fields.StringField(nullable=False), 'name': object_fields.StringField(nullable=False),
'description': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True),
'project_id': object_fields.UUIDField(nullable=True),
'user_id': object_fields.UUIDField(nullable=True),
'device_type': object_fields.StringField(nullable=False), 'device_type': object_fields.StringField(nullable=False),
'acc_type': object_fields.StringField(nullable=False), 'acc_type': object_fields.StringField(nullable=False),
'acc_capability': object_fields.StringField(nullable=False), 'acc_capability': object_fields.StringField(nullable=False),

View File

@ -0,0 +1,4 @@
To generate the sample policy.json file, run the following command from the top
level of the cyborg directory:
tox -egenpolicy

4
etc/cyborg/policy.json Normal file
View File

@ -0,0 +1,4 @@
# leave this file empty to use default policy defined in code.
{
}

View File

@ -18,6 +18,7 @@ oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0
oslo.db>=4.24.0 # Apache-2.0 oslo.db>=4.24.0 # Apache-2.0
oslo.utils>=3.20.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0
oslo.versionedobjects>=1.17.0 # Apache-2.0 oslo.versionedobjects>=1.17.0 # Apache-2.0
oslo.policy>=1.23.0 # Apache-2.0
SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
alembic>=0.8.10 # MIT alembic>=0.8.10 # MIT
stevedore>=1.20.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0

View File

@ -24,6 +24,9 @@ packages =
cyborg cyborg
[entry_points] [entry_points]
oslo.policy.policies =
cyborg.api = cyborg.common.policy:list_policies
console_scripts = console_scripts =
cyborg-api = cyborg.cmd.api:main cyborg-api = cyborg.cmd.api:main
cyborg-conductor = cyborg.cmd.conductor:main cyborg-conductor = cyborg.cmd.conductor:main

View File

@ -0,0 +1,3 @@
[DEFAULT]
output_file = etc/cyborg/policy.json.sample
namespace = cyborg.api

View File

@ -13,6 +13,12 @@ deps = -r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}' commands = python setup.py testr --slowest --testr-args='{posargs}'
[testenv:genpolicy]
sitepackages = False
envdir = {toxworkdir}/venv
commands =
oslopolicy-sample-generator --config-file=tools/config/cyborg-policy-generator.conf
[testenv:pep8] [testenv:pep8]
commands = flake8 {posargs} commands = flake8 {posargs}