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:
Shaohe Feng 2016-10-01 12:02:52 +08:00 committed by ShaoHe Feng
parent a8efccc591
commit 518d89d7c1
6 changed files with 271 additions and 0 deletions

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

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

View 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"

View File

@ -96,6 +96,10 @@ class OperationNotPermitted(NotAuthorized):
_msg_fmt = _("Operation not permitted.")
class HTTPForbidden(NotAuthorized):
_msg_fmt = _("Access was denied to the following resource: %(resource)s")
class NotFound(NimbleException):
_msg_fmt = _("Resource could not be found.")
code = http_client.NOT_FOUND

224
nimble/common/policy.py Normal file
View 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)

View File

@ -34,6 +34,9 @@ nimble.engine.scheduler.filters =
oslo.config.opts =
nimble = nimble.conf.opts:list_opts
oslo.policy.policies =
nimble.api = nimble.common.policy:list_policies
console_scripts =
nimble-api = nimble.cmd.api:main
nimble-engine = nimble.cmd.engine:main

View File

@ -65,6 +65,12 @@ envdir = {toxworkdir}/venv
commands =
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]
# This environment is called from CI scripts to test and publish
# the API Ref to developer.openstack.org.