Merge "add policy support"
This commit is contained in:
commit
cbe27fc8eb
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,3 +2,6 @@
|
||||
*.tox
|
||||
*.testrepository
|
||||
cyborg.egg-info
|
||||
|
||||
# Sample profile
|
||||
etc/cyborg/policy.json.sample
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
234
cyborg/common/policy.py
Normal 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
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
4
etc/cyborg/README.policy.json.txt
Normal file
4
etc/cyborg/README.policy.json.txt
Normal 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
4
etc/cyborg/policy.json
Normal file
@ -0,0 +1,4 @@
|
||||
# leave this file empty to use default policy defined in code.
|
||||
{
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
3
tools/config/cyborg-policy-generator.conf
Normal file
3
tools/config/cyborg-policy-generator.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
output_file = etc/cyborg/policy.json.sample
|
||||
namespace = cyborg.api
|
6
tox.ini
6
tox.ini
@ -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 = pep8 {posargs}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user