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
*.testrepository
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.v1 import types
from cyborg.api import expose
from cyborg.common import policy
from cyborg import objects
@ -37,6 +38,8 @@ class Accelerator(base.APIBase):
uuid = types.uuid
name = wtypes.text
description = wtypes.text
project_id = types.uuid
user_id = types.uuid
device_type = wtypes.text
acc_type = wtypes.text
acc_capability = wtypes.text
@ -67,9 +70,16 @@ class Accelerator(base.APIBase):
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."""
@policy.authorize_wsgi("cyborg:accelerator", "create", False)
@expose.expose(Accelerator, body=types.jsontype,
status_code=http_client.CREATED)
def post(self, values):

View File

@ -17,6 +17,7 @@ from oslo_config import cfg
from oslo_context import context
from pecan import hooks
from cyborg.common import policy
from cyborg.conductor import rpcapi
@ -50,11 +51,53 @@ class ConductorAPIHook(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):
self.public_api_routes = public_api_routes
super(ContextHook, self).__init__()
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):
_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('name', sa.String(length=255), nullable=False),
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('acc_type', 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)
name = Column(String(255), nullable=False)
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)
acc_type = 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),
'name': object_fields.StringField(nullable=False),
'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),
'acc_type': 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.utils>=3.20.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
alembic>=0.8.10 # MIT
stevedore>=1.20.0 # Apache-2.0

View File

@ -24,6 +24,9 @@ packages =
cyborg
[entry_points]
oslo.policy.policies =
cyborg.api = cyborg.common.policy:list_policies
console_scripts =
cyborg-api = cyborg.cmd.api: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
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]
commands = flake8 {posargs}