Add new default rules and mapping in policy base class

Cyborg Policy Default Refresh is one of the planned blueprints for victoria
release, the specification[0] has been merged in ussuri. To be brief, we need
to do the followings to incorporate authorization scopes into cyborg:
1. Add protection test for all APIs.
   A protection test is similar to an API test, but purely focused on the
   authoritative outcome.In other words, protection testing is sufficient when
   we can assert that a user is or isn’t allowed to do or see something. For
   example, users with a reader role on the system or a project shouldn’t be
   able to make writable changes.
2. Add the following applicable seven personas to cyborg and mark old
   ones as deprecated roles:
   * project reader
   * project member
   * project admin
   * system reader
   * system admin
   * system admin or owner
   * system or project reader
3. Rewrite check string(authorization rules) using new personas for all APIs
4. Update policy documentation on cyborg-doc page

This patch refreshed cyborg default RBAC policy to scoped RBAC policy, and
reorganized the policy framework into a more logical way:
    1) added seven personas to basic policies and marked legacy roles
       as deprecated ones.
    2) extract API_policies from policy.py to indenpendent policy files
    3) extract authorize_wsgi.py out from policy.py

[0]https://specs.openstack.org/openstack/cyborg-specs/specs/ussuri/approved/policy-defaults-refresh.html

Story: 2007024
Task: 40835

Change-Id: I948d0202ddcd82a532c4de2c1850893cbfaf003d
This commit is contained in:
Yumeng Bao
2020-07-09 13:48:04 +08:00
parent 27d5a7de39
commit 270d12e9a8
10 changed files with 389 additions and 222 deletions

View File

@@ -26,10 +26,10 @@ from cyborg.api.controllers import types
from cyborg.api.controllers.v2 import utils from cyborg.api.controllers.v2 import utils
from cyborg.api.controllers.v2 import versions from cyborg.api.controllers.v2 import versions
from cyborg.api import expose from cyborg.api import expose
from cyborg.common import authorize_wsgi
from cyborg.common import constants from cyborg.common import constants
from cyborg.common import exception from cyborg.common import exception
from cyborg.common.i18n import _ from cyborg.common.i18n import _
from cyborg.common import policy
from cyborg import objects from cyborg import objects
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@@ -115,7 +115,7 @@ class ARQsController(base.CyborgController):
except Exception: except Exception:
return None return None
@policy.authorize_wsgi("cyborg:arq", "create", False) @authorize_wsgi.authorize_wsgi("cyborg:arq", "create", False)
@expose.expose(ARQCollection, body=types.jsontype, @expose.expose(ARQCollection, body=types.jsontype,
status_code=http_client.CREATED) status_code=http_client.CREATED)
def post(self, req): def post(self, req):
@@ -169,7 +169,7 @@ class ARQsController(base.CyborgController):
LOG.info('[arqs] post returned: %s', ret) LOG.info('[arqs] post returned: %s', ret)
return ret return ret
@policy.authorize_wsgi("cyborg:arq", "get_one") @authorize_wsgi.authorize_wsgi("cyborg:arq", "get_one")
@expose.expose(ARQ, wtypes.text) @expose.expose(ARQ, wtypes.text)
def get_one(self, uuid): def get_one(self, uuid):
"""Get a single ARQ by UUID.""" """Get a single ARQ by UUID."""
@@ -177,7 +177,7 @@ class ARQsController(base.CyborgController):
extarq = objects.ExtARQ.get(context, uuid) extarq = objects.ExtARQ.get(context, uuid)
return ARQ.convert_with_links(extarq.arq) return ARQ.convert_with_links(extarq.arq)
@policy.authorize_wsgi("cyborg:arq", "get_all", False) @authorize_wsgi.authorize_wsgi("cyborg:arq", "get_all", False)
@expose.expose(ARQCollection, wtypes.text, types.uuid) @expose.expose(ARQCollection, wtypes.text, types.uuid)
def get_all(self, bind_state=None, instance=None): def get_all(self, bind_state=None, instance=None):
"""Retrieve a list of arqs.""" """Retrieve a list of arqs."""
@@ -216,7 +216,7 @@ class ARQsController(base.CyborgController):
LOG.info('[arqs:get_all] Returned: %s', ret) LOG.info('[arqs:get_all] Returned: %s', ret)
return ret return ret
@policy.authorize_wsgi("cyborg:arq", "delete", False) @authorize_wsgi.authorize_wsgi("cyborg:arq", "delete", False)
@expose.expose(None, wtypes.text, wtypes.text, @expose.expose(None, wtypes.text, wtypes.text,
status_code=http_client.NO_CONTENT) status_code=http_client.NO_CONTENT)
def delete(self, arqs=None, instance=None): def delete(self, arqs=None, instance=None):
@@ -304,7 +304,7 @@ class ARQsController(base.CyborgController):
reason = msg.format(instance_uuid) reason = msg.format(instance_uuid)
raise exception.PatchError(reason=reason) raise exception.PatchError(reason=reason)
@policy.authorize_wsgi("cyborg:arq", "update", False) @authorize_wsgi.authorize_wsgi("cyborg:arq", "update", False)
@expose.expose(None, body=types.jsontype, @expose.expose(None, body=types.jsontype,
status_code=http_client.ACCEPTED) status_code=http_client.ACCEPTED)
def patch(self, patch_list): def patch(self, patch_list):

View File

@@ -23,7 +23,7 @@ from cyborg.api.controllers import base
from cyborg.api.controllers import link from cyborg.api.controllers import link
from cyborg.api.controllers import types from cyborg.api.controllers import types
from cyborg.api import expose from cyborg.api import expose
from cyborg.common import policy from cyborg.common import authorize_wsgi
from cyborg import objects from cyborg import objects
@@ -110,7 +110,7 @@ class DeployablesController(base.CyborgController,
DeployableCollection): DeployableCollection):
"""REST controller for Deployables.""" """REST controller for Deployables."""
@policy.authorize_wsgi("cyborg:deployable", "get_one") @authorize_wsgi.authorize_wsgi("cyborg:deployable", "get_one")
@expose.expose(Deployable, types.uuid) @expose.expose(Deployable, types.uuid)
def get_one(self, uuid): def get_one(self, uuid):
"""Retrieve information about the given deployable. """Retrieve information about the given deployable.
@@ -120,7 +120,7 @@ class DeployablesController(base.CyborgController,
obj_dep = objects.Deployable.get(pecan.request.context, uuid) obj_dep = objects.Deployable.get(pecan.request.context, uuid)
return self.convert_with_link(obj_dep) return self.convert_with_link(obj_dep)
@policy.authorize_wsgi("cyborg:deployable", "get_all") @authorize_wsgi.authorize_wsgi("cyborg:deployable", "get_all")
@expose.expose(DeployableCollection, wtypes.ArrayType(types.FilterType)) @expose.expose(DeployableCollection, wtypes.ArrayType(types.FilterType))
def get_all(self, filters=None): def get_all(self, filters=None):
"""Retrieve a list of deployables. """Retrieve a list of deployables.

View File

@@ -26,8 +26,8 @@ from cyborg.api.controllers import base
from cyborg.api.controllers import link from cyborg.api.controllers import link
from cyborg.api.controllers import types from cyborg.api.controllers import types
from cyborg.api import expose from cyborg.api import expose
from cyborg.common import authorize_wsgi
from cyborg.common import exception from cyborg.common import exception
from cyborg.common import policy
from cyborg import objects from cyborg import objects
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@@ -83,7 +83,7 @@ class DeviceProfilesController(base.CyborgController,
DeviceProfileCollection): DeviceProfileCollection):
"""REST controller for Device Profiles.""" """REST controller for Device Profiles."""
@policy.authorize_wsgi("cyborg:device_profile", "create", False) @authorize_wsgi.authorize_wsgi("cyborg:device_profile", "create", False)
@expose.expose('json', body=types.jsontype, @expose.expose('json', body=types.jsontype,
status_code=http_client.CREATED) status_code=http_client.CREATED)
def post(self, req_devprof_list): def post(self, req_devprof_list):
@@ -163,7 +163,7 @@ class DeviceProfilesController(base.CyborgController,
return api_obj_devprofs return api_obj_devprofs
@policy.authorize_wsgi("cyborg:device_profile", "get_all", False) @authorize_wsgi.authorize_wsgi("cyborg:device_profile", "get_all", False)
@expose.expose('json', wtypes.text) @expose.expose('json', wtypes.text)
def get_all(self, name=None): def get_all(self, name=None):
"""Retrieve a list of device profiles.""" """Retrieve a list of device profiles."""
@@ -180,7 +180,7 @@ class DeviceProfilesController(base.CyborgController,
return wsme.api.Response(ret, status_code=http_client.OK, return wsme.api.Response(ret, status_code=http_client.OK,
return_type=wsme.types.DictType) return_type=wsme.types.DictType)
@policy.authorize_wsgi("cyborg:device_profile", "get_one") @authorize_wsgi.authorize_wsgi("cyborg:device_profile", "get_one")
@expose.expose('json', wtypes.text) @expose.expose('json', wtypes.text)
def get_one(self, uuid): def get_one(self, uuid):
"""Retrieve a single device profile by uuid.""" """Retrieve a single device profile by uuid."""
@@ -201,7 +201,7 @@ class DeviceProfilesController(base.CyborgController,
return wsme.api.Response(ret, status_code=http_client.OK, return wsme.api.Response(ret, status_code=http_client.OK,
return_type=wsme.types.DictType) return_type=wsme.types.DictType)
@policy.authorize_wsgi("cyborg:device_profile", "delete") @authorize_wsgi.authorize_wsgi("cyborg:device_profile", "delete")
@expose.expose(None, wtypes.text, status_code=http_client.NO_CONTENT) @expose.expose(None, wtypes.text, status_code=http_client.NO_CONTENT)
def delete(self, value): def delete(self, value):
"""Delete one or more device_profiles. """Delete one or more device_profiles.

View File

@@ -23,7 +23,7 @@ from cyborg.api.controllers import base
from cyborg.api.controllers import link from cyborg.api.controllers import link
from cyborg.api.controllers import types from cyborg.api.controllers import types
from cyborg.api import expose from cyborg.api import expose
from cyborg.common import policy from cyborg.common import authorize_wsgi
from cyborg import objects from cyborg import objects
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@@ -93,7 +93,7 @@ class DeviceCollection(base.APIBase):
class DevicesController(base.CyborgController): class DevicesController(base.CyborgController):
"""REST controller for Devices.""" """REST controller for Devices."""
@policy.authorize_wsgi("cyborg:device", "get_one") @authorize_wsgi.authorize_wsgi("cyborg:device", "get_one")
@expose.expose(Device, wtypes.text) @expose.expose(Device, wtypes.text)
def get_one(self, uuid): def get_one(self, uuid):
"""Get a single device by UUID. """Get a single device by UUID.
@@ -103,7 +103,7 @@ class DevicesController(base.CyborgController):
device = objects.Device.get(context, uuid) device = objects.Device.get(context, uuid)
return Device.convert_with_links(device) return Device.convert_with_links(device)
@policy.authorize_wsgi("cyborg:device", "get_all", False) @authorize_wsgi.authorize_wsgi("cyborg:device", "get_all", False)
@expose.expose(DeviceCollection, wtypes.text, wtypes.text, wtypes.text, @expose.expose(DeviceCollection, wtypes.text, wtypes.text, wtypes.text,
wtypes.ArrayType(types.FilterType)) wtypes.ArrayType(types.FilterType))
def get_all(self, type=None, vendor=None, hostname=None, filters=None): def get_all(self, type=None, vendor=None, hostname=None, filters=None):

View File

@@ -0,0 +1,167 @@
# 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 Authorize 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
from cyborg import policies
_ENFORCER = None
CONF = cfg.CONF
LOG = log.getLogger(__name__)
@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(policies.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 authorize_wsgi
class AcceleratorController(rest.RestController):
....
@authorize_wsgi.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

@@ -13,61 +13,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""Policy Engine For Cyborg."""
import functools """legacy old_policies, the following old_policies will be removed once
import sys new policies are implemented.
"""
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log
from oslo_policy import policy 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 # NOTE: to follow policy-in-code spec, we define defaults for
# the granular policies in code, rather than in policy.json. # the granular policies in code, rather than in policy.json.
# All of these may be overridden by configuration, but we can # All of these may be overridden by configuration, but we can
@@ -136,153 +86,3 @@ fpga_policies = [
'rule:allow', 'rule:allow',
description='Update fpga records'), description='Update fpga records'),
] ]
def list_policies():
return default_policies \
+ fpga_policies \
+ accelerator_request_policies \
+ device_profile_policies \
+ device_policies \
+ deployable_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

@@ -0,0 +1,33 @@
# Copyright 2020 ZTE Corporation.
# 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.
import itertools
from cyborg.common import policy as old_policy
from cyborg.policies import base
def list_policies():
return itertools.chain(
base.list_policies(),
# NOTE(yumeng)old_policies will also be loaded before they are replaced
# by new policies
old_policy.device_profile_policies,
old_policy.device_policies,
old_policy.deployable_policies,
old_policy.accelerator_request_policies,
old_policy.fpga_policies,
)

167
cyborg/policies/base.py Normal file
View File

@@ -0,0 +1,167 @@
# Copyright 2020 ZTE Corporation.
# 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.
from oslo_log import versionutils
from oslo_policy import policy
# All legacy policy and new policy mapping for all V2 APIs can be found
# here:https://wiki.openstack.org/wiki/Cyborg/Policy
# TODO(yumeng) Special string ``system_scope:all``
# We are explicitly setting system_scope:all in these check strings because
# they provide backwards compatibility in the event a deployment sets
# ``cyborg.conf [oslo_policy] enforce_scope = False``, which the default.
# Otherwise, this might open up APIs to be more permissive unintentionally if a
# deployment isn't enforcing scope. For example, the new rule for action
# 'cyborg:device_profile:create' will be System Scoped Admin with
# ``role:admin`` and scope_type=['system']. However, it would be possible for
# users with the ``admin`` role on a project to access the
# 'cyborg:device_profile:create' until enforce_scope=True is set by default.
# Once cyborg defaults ``cyborg.conf [oslo_policy] enforce_scope = True``,
# the the ``system_scope:all`` bits of these check strings
# can be removed since that will be handled automatically by scope_types in
# oslo.policy's RuleDefault objects.
SYSTEM_ADMIN = 'rule:system_admin_api'
SYSTEM_READER = 'rule:system_reader_api'
PROJECT_ADMIN = 'rule:project_admin_api'
PROJECT_MEMBER = 'rule:project_member_api'
PROJECT_READER = 'rule:project_reader_api'
PROJECT_MEMBER_OR_SYSTEM_ADMIN = 'rule:system_admin_or_owner'
PROJECT_READER_OR_SYSTEM_READER = 'rule:system_or_project_reader'
# NOTE(yumeng): Keystone already support implied roles means assignment
# of one role implies the assignment of another. New defaults roles
# `reader`, `member` also has been added in bootstrap. If the bootstrap
# process is re-run, and a `reader`, `member`, or `admin` role already
# exists, a role implication chain will be created: `admin` implies
# `member` implies `reader`.
# For example: If we give access to 'reader' it means the 'admin' and
# 'member' also get access.
# NOTE(yumeng) the rules listed include both old rules and new rules.
# legacy rules list: 'public_api','allow','deny','admin_api','is_admin',
# 'admin_or_owner','admin_or_user'.
# new rules list: system_admin_api,system_reader_api,project_admin_api,
# project_member_api, project_reader_api, system_admin_or_owner,
# system_or_project_reader .
default_policies = [
policy.RuleDefault(
name="system_admin_api",
check_str='role:admin and system_scope:all',
description="Default rule for System Admin APIs."),
policy.RuleDefault(
name="system_reader_api",
check_str="role:reader and system_scope:all",
description="Default rule for System level read only APIs."),
policy.RuleDefault(
name="project_admin_api",
check_str="role:admin and project_id:%(project_id)s",
description="Default rule for Project level admin APIs."),
policy.RuleDefault(
name="project_member_api",
check_str="role:member and project_id:%(project_id)s",
description="Default rule for Project level non admin APIs."),
policy.RuleDefault(
name="project_reader_api",
check_str="role:reader and project_id:%(project_id)s",
description="Default rule for Project level read only APIs."),
policy.RuleDefault(
name="system_admin_or_owner",
check_str="rule:system_admin_api or rule:project_member_api",
description="Default rule for system_admin+owner APIs."),
policy.RuleDefault(
name="system_or_project_reader",
check_str="rule:system_reader_api or rule:project_reader_api",
description="Default rule for System+Project read only APIs.")
]
DEPRECATED_REASON = """
Cyborg API policies are introducing new default roles with scope_type
capabilities. We will start to deprecate old policies from WALLABY release,
and are going to ignore all the old policies silently from X release.
Be sure to take these new defaults into consideration if you are
relying on overrides in your deployment for the policy API.
"""
deprecated_default = 'rule:admin_or_owner'
deprecated_is_admin = 'rule:is_admin'
deprecated_default_policies = [
# is_public_api is set in the environment from AuthTokenMiddleware
policy.RuleDefault(
name='public_api',
check_str='is_public_api:True',
description='legacy rule of Internal flag for public API routes',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
# The policy check "@" will always accept an access. The empty list
# (``[]``) or the empty string (``""``) is equivalent to the "@"
policy.RuleDefault(
name='allow',
check_str='@',
description='legacy rule: any access will be passed',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
# the policy check "!" will always reject an access.
policy.RuleDefault(
name='deny',
check_str='!',
description='legacy rule: all access will be forbidden',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
policy.RuleDefault(
name='default',
check_str='rule:admin_or_owner',
description='Legacy rule for default rule',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
policy.RuleDefault(
name='admin_api',
check_str='role:admin or role:administrator',
description='Legacy rule for cloud admin access',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
policy.RuleDefault(
name='is_admin',
check_str='rule:admin_api',
description='Full read/write API access',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
policy.RuleDefault(
name='admin_or_owner',
check_str='is_admin:True or project_id:%(project_id)s',
description='Admin or owner API access',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
policy.RuleDefault(
name='admin_or_user',
check_str='is_admin:True or user_id:%(user_id)s',
description='Admin or user API access',
deprecated_for_removal=True,
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY),
]
def list_policies():
return default_policies \
+ deprecated_default_policies

View File

@@ -19,7 +19,7 @@ import fixtures
from oslo_config import cfg from oslo_config import cfg
from oslo_policy import opts as policy_opts from oslo_policy import opts as policy_opts
from cyborg.common import policy as cyborg_policy from cyborg.common import authorize_wsgi as cyborg_policy
CONF = cfg.CONF CONF = cfg.CONF

View File

@@ -31,7 +31,7 @@ data_files =
[entry_points] [entry_points]
oslo.policy.policies = oslo.policy.policies =
cyborg.api = cyborg.common.policy:list_policies cyborg.api = cyborg.policies:list_policies
console_scripts = console_scripts =
cyborg-api = cyborg.cmd.api:main cyborg-api = cyborg.cmd.api:main