Add policy support to Nimble
Implements more fine-grained policy support within our API service, following the oslo policy-in-code spec, while maintaining compatibility with the previous default policy.json file. An empty policy.json file is included, along with a sample file listig all supported policy settings and their default values. A new tox target "genpolicy" has been added to ease automation of sample policy file generation. Any policy has be changed, please run "tox -e genpolicy" to update policy.json.sample. ref: http://docs.openstack.org/developer/oslo.policy/usage.html ref: pydoc oslo_policy.policy Change-Id: I3a971f2565c2f35665007461c1ae91eeb3b2de5a
This commit is contained in:
parent
a8efccc591
commit
518d89d7c1
4
etc/nimble/policy.json
Normal file
4
etc/nimble/policy.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# leave this file empty to use default policy defined in code.
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
30
etc/nimble/policy.json.sample
Normal file
30
etc/nimble/policy.json.sample
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Legacy rule for cloud admin access
|
||||||
|
"admin_api": "role:admin or role:administrator"
|
||||||
|
# Internal flag for public API routes
|
||||||
|
"public_api": "is_public_api:True"
|
||||||
|
# Show or mask secrets within instance information in API responses
|
||||||
|
"show_instance_secrets": "!"
|
||||||
|
# any access will be passed
|
||||||
|
"allow": "@"
|
||||||
|
# all access will be forbidden
|
||||||
|
"deny": "!"
|
||||||
|
# Full read/write API access
|
||||||
|
"is_admin": "rule:admin_api or (rule:is_member and role:nimble_admin)"
|
||||||
|
# Admin or owner API access
|
||||||
|
"admin_or_owner": "is_admin:True or project_id:%(project_id)s"
|
||||||
|
# Admin or user API access
|
||||||
|
"admin_or_user": "is_admin:True or user_id:%(user_id)s"
|
||||||
|
# Default API access rule
|
||||||
|
"default": "rule:admin_or_owner"
|
||||||
|
# Retrieve Instance records
|
||||||
|
"nimble:instance:get": "rule:default"
|
||||||
|
# View Instance power and provision state
|
||||||
|
"nimble:instance:get_states": "rule:default"
|
||||||
|
# Create Instance records
|
||||||
|
"nimble:instance:create": "rule:allow"
|
||||||
|
# Delete Instance records
|
||||||
|
"nimble:instance:delete": "rule:default"
|
||||||
|
# Update Instance records
|
||||||
|
"nimble:instance:update": "rule:default"
|
||||||
|
# Change Instance power status
|
||||||
|
"nimble:instance:set_power_state": "rule:default"
|
@ -96,6 +96,10 @@ class OperationNotPermitted(NotAuthorized):
|
|||||||
_msg_fmt = _("Operation not permitted.")
|
_msg_fmt = _("Operation not permitted.")
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPForbidden(NotAuthorized):
|
||||||
|
_msg_fmt = _("Access was denied to the following resource: %(resource)s")
|
||||||
|
|
||||||
|
|
||||||
class NotFound(NimbleException):
|
class NotFound(NimbleException):
|
||||||
_msg_fmt = _("Resource could not be found.")
|
_msg_fmt = _("Resource could not be found.")
|
||||||
code = http_client.NOT_FOUND
|
code = http_client.NOT_FOUND
|
||||||
|
224
nimble/common/policy.py
Normal file
224
nimble/common/policy.py
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# Copyright (c) 2016 Intel
|
||||||
|
# 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 Nimble."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
from oslo_concurrency import lockutils
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_policy import policy
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from nimble.common import exception
|
||||||
|
from nimble.common.i18n import _LW
|
||||||
|
|
||||||
|
_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'),
|
||||||
|
# Generic default to hide instance secrets
|
||||||
|
policy.RuleDefault('show_instance_secrets',
|
||||||
|
'!',
|
||||||
|
description='Show or mask secrets within instance information in API responses'), # noqa
|
||||||
|
# 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 or (rule:is_member and role:nimble_admin)', # noqa
|
||||||
|
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.
|
||||||
|
|
||||||
|
instance_policies = [
|
||||||
|
policy.RuleDefault('nimble:instance:get',
|
||||||
|
'rule:default',
|
||||||
|
description='Retrieve Instance records'),
|
||||||
|
policy.RuleDefault('nimble:instance:get_states',
|
||||||
|
'rule:default',
|
||||||
|
description='View Instance power and provision state'),
|
||||||
|
policy.RuleDefault('nimble:instance:create',
|
||||||
|
'rule:allow',
|
||||||
|
description='Create Instance records'),
|
||||||
|
policy.RuleDefault('nimble:instance:delete',
|
||||||
|
'rule:default',
|
||||||
|
description='Delete Instance records'),
|
||||||
|
policy.RuleDefault('nimble:instance:update',
|
||||||
|
'rule:default',
|
||||||
|
description='Update Instance records'),
|
||||||
|
policy.RuleDefault('nimble:instance:set_power_state',
|
||||||
|
'rule:default',
|
||||||
|
description='Change Instance power status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_policies():
|
||||||
|
policies = (default_policies
|
||||||
|
+ instance_policies)
|
||||||
|
return policies
|
||||||
|
|
||||||
|
|
||||||
|
@lockutils.synchronized('policy_enforcer', 'nimble-')
|
||||||
|
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 instance of Policy 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, *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.
|
||||||
|
|
||||||
|
Beginning with the Newton cycle, this should be used in place of 'enforce'.
|
||||||
|
"""
|
||||||
|
enforcer = get_enforcer()
|
||||||
|
try:
|
||||||
|
return enforcer.authorize(rule, target, creds, do_raise=True,
|
||||||
|
*args, **kwargs)
|
||||||
|
except policy.PolicyNotAuthorized:
|
||||||
|
raise exception.HTTPForbidden(resource=rule)
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE(Shaohe Feng): This decorator MUST appear first (the outermost
|
||||||
|
# decorator) on an API method for it to work correctly
|
||||||
|
def authorize_wsgi(api_name, act=None):
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
example:
|
||||||
|
from magnum.common import policy
|
||||||
|
class InstancesController(rest.RestController):
|
||||||
|
....
|
||||||
|
@policy.authorize_wsgi("nimble:instance", "delete")
|
||||||
|
@wsme_pecan.wsexpose(None, types.uuid_or_name, status_code=204)
|
||||||
|
def delete(self, bay_ident):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
def wraper(fn):
|
||||||
|
action = "%s:%s" % (api_name, (act or fn.func_name))
|
||||||
|
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
context = pecan.request.context
|
||||||
|
credentials = context.to_dict()
|
||||||
|
target = {'project_id': context.project_id,
|
||||||
|
'user_id': context.user_id}
|
||||||
|
|
||||||
|
authorize(action, target, credentials)
|
||||||
|
return fn(self, *args, **kwargs)
|
||||||
|
return handle
|
||||||
|
return wraper
|
||||||
|
|
||||||
|
|
||||||
|
def check(rule, target, creds, *args, **kwargs):
|
||||||
|
"""A shortcut for policy.Enforcer.enforce()
|
||||||
|
|
||||||
|
Checks authorization of a rule against the target and credentials
|
||||||
|
and returns True or False.
|
||||||
|
"""
|
||||||
|
enforcer = get_enforcer()
|
||||||
|
return enforcer.enforce(rule, target, creds, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs):
|
||||||
|
"""A shortcut for policy.Enforcer.enforce()
|
||||||
|
|
||||||
|
Checks authorization of a rule against the target and credentials.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# NOTE: this method is obsoleted by authorize(), but retained for
|
||||||
|
# backwards compatibility in case it has been used downstream.
|
||||||
|
# It may be removed in the Pike cycle.
|
||||||
|
LOG.warning(_LW(
|
||||||
|
"Deprecation warning: calls to nimble.common.policy.enforce() "
|
||||||
|
"should be replaced with authorize(). This method may be removed "
|
||||||
|
"in a future release."))
|
||||||
|
|
||||||
|
enforcer = get_enforcer()
|
||||||
|
return enforcer.enforce(rule, target, creds, do_raise=do_raise,
|
||||||
|
exc=exc, *args, **kwargs)
|
@ -34,6 +34,9 @@ nimble.engine.scheduler.filters =
|
|||||||
oslo.config.opts =
|
oslo.config.opts =
|
||||||
nimble = nimble.conf.opts:list_opts
|
nimble = nimble.conf.opts:list_opts
|
||||||
|
|
||||||
|
oslo.policy.policies =
|
||||||
|
nimble.api = nimble.common.policy:list_policies
|
||||||
|
|
||||||
console_scripts =
|
console_scripts =
|
||||||
nimble-api = nimble.cmd.api:main
|
nimble-api = nimble.cmd.api:main
|
||||||
nimble-engine = nimble.cmd.engine:main
|
nimble-engine = nimble.cmd.engine:main
|
||||||
|
6
tox.ini
6
tox.ini
@ -65,6 +65,12 @@ envdir = {toxworkdir}/venv
|
|||||||
commands =
|
commands =
|
||||||
oslo-config-generator --config-file=tools/config/nimble-config-generator.conf
|
oslo-config-generator --config-file=tools/config/nimble-config-generator.conf
|
||||||
|
|
||||||
|
[testenv:genpolicy]
|
||||||
|
sitepackages = False
|
||||||
|
envdir = {toxworkdir}/venv
|
||||||
|
commands =
|
||||||
|
oslopolicy-sample-generator --namespace=nimble.api --output-file=etc/nimble/policy.json.sample
|
||||||
|
|
||||||
[testenv:api-ref]
|
[testenv:api-ref]
|
||||||
# This environment is called from CI scripts to test and publish
|
# This environment is called from CI scripts to test and publish
|
||||||
# the API Ref to developer.openstack.org.
|
# the API Ref to developer.openstack.org.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user