Browse Source
this patch introduces an oslo.policy-based API access policy enforcement engine to ironic-inspector. As part of implementation, a proper oslo.context-based request context is also generated and assigned to each request. Short overview of changes: - added custom RequestContext class - extends oslo.context to handle of "is_public_api" flag (False by default) - added context to request in each API route - '/continue' api sets the "is_public_api" flag to True - added documented definitions for API access policies and their defaults - added enforcement of these policies on API requests - added oslo.policy-specific entry points to setup.cfg - added autogenerated policy sample file with defaults - added documentation with autogenerated policies Change-Id: Iff6f98fa9950d78608f0a7c325d132c11a1383b3 Closes-Bug: #1719812changes/26/507826/15
20 changed files with 557 additions and 56 deletions
@ -0,0 +1,9 @@
|
||||
======== |
||||
Policies |
||||
======== |
||||
|
||||
The following is an overview of all available policies in **ironic inspector**. |
||||
For a sample configuration file, refer to :doc:`sample-policy`. |
||||
|
||||
.. show-policy:: |
||||
:config-file: policy-generator.conf |
@ -0,0 +1,13 @@
|
||||
======================= |
||||
Ironic Inspector Policy |
||||
======================= |
||||
|
||||
The following is a sample **ironic-inspector** policy file, autogenerated from |
||||
Ironic Inspector when this documentation is built. |
||||
To avoid issues, make sure your version of **ironic-inspector** |
||||
matches that of the example policy file. |
||||
|
||||
The sample policy can also be downloaded as a :download:`file |
||||
</_static/ironic-inspector.policy.yaml.sample>`. |
||||
|
||||
.. literalinclude:: /_static/ironic-inspector.policy.yaml.sample |
@ -0,0 +1,45 @@
|
||||
# |
||||
# 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_context import context |
||||
|
||||
|
||||
class RequestContext(context.RequestContext): |
||||
"""Extends security contexts from the oslo.context library.""" |
||||
|
||||
def __init__(self, is_public_api=False, **kwargs): |
||||
"""Initialize the RequestContext |
||||
|
||||
:param is_public_api: Specifies whether the request should be processed |
||||
without authentication. |
||||
:param kwargs: additional arguments passed to oslo.context. |
||||
""" |
||||
super(RequestContext, self).__init__(**kwargs) |
||||
self.is_public_api = is_public_api |
||||
|
||||
def to_policy_values(self): |
||||
policy_values = super(RequestContext, self).to_policy_values() |
||||
policy_values.update({'is_public_api': self.is_public_api}) |
||||
return policy_values |
||||
|
||||
@classmethod |
||||
def from_dict(cls, values, **kwargs): |
||||
kwargs.setdefault('is_public_api', values.get('is_public_api', False)) |
||||
return super(RequestContext, RequestContext).from_dict(values, |
||||
**kwargs) |
||||
|
||||
@classmethod |
||||
def from_environ(cls, environ, **kwargs): |
||||
kwargs.setdefault('is_public_api', environ.get('is_public_api', False)) |
||||
return super(RequestContext, RequestContext).from_environ(environ, |
||||
**kwargs) |
@ -0,0 +1,217 @@
|
||||
# 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 |
||||
import sys |
||||
|
||||
from oslo_concurrency import lockutils |
||||
from oslo_config import cfg |
||||
from oslo_policy import policy |
||||
|
||||
CONF = cfg.CONF |
||||
|
||||
_ENFORCER = None |
||||
|
||||
default_policies = [ |
||||
policy.RuleDefault( |
||||
'is_admin', |
||||
'role:admin or role:administrator or role:baremetal_admin', |
||||
description='Full read/write API access'), |
||||
policy.RuleDefault( |
||||
'is_observer', |
||||
'role:baremetal_observer', |
||||
description='Read-only API access'), |
||||
policy.RuleDefault( |
||||
'public_api', |
||||
'is_public_api:True', |
||||
description='Internal flag for public API routes'), |
||||
policy.RuleDefault( |
||||
'default', |
||||
'!', |
||||
description='Default API access policy'), |
||||
] |
||||
|
||||
api_version_policies = [ |
||||
policy.DocumentedRuleDefault( |
||||
'introspection', |
||||
'rule:public_api', |
||||
'Access the API root for available versions information', |
||||
[{'path': '/', 'method': 'GET'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:version', |
||||
'rule:public_api', |
||||
'Access the versioned API root for version information', |
||||
[{'path': '/{version}', 'method': 'GET'}] |
||||
), |
||||
] |
||||
|
||||
|
||||
introspection_policies = [ |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:continue', |
||||
'rule:public_api', |
||||
'Ramdisk callback to continue introspection', |
||||
[{'path': '/continue', 'method': 'POST'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:status', |
||||
'rule:is_admin or rule:is_observer', |
||||
'Get introspection status', |
||||
[{'path': '/introspection', 'method': 'GET'}, |
||||
{'path': '/introspection/{node_id}', 'method': 'GET'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:start', |
||||
'rule:is_admin', |
||||
'Start introspection', |
||||
[{'path': '/introspection/{node_id}', 'method': 'POST'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:abort', |
||||
'rule:is_admin', |
||||
'Abort introspection', |
||||
[{'path': '/introspection/{node_id}/abort', 'method': 'POST'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:data', |
||||
'rule:is_admin', |
||||
'Get introspection data', |
||||
[{'path': '/introspection/{node_id}/data', 'method': 'GET'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:reapply', |
||||
'rule:is_admin', |
||||
'Reapply introspection on stored data', |
||||
[{'path': '/introspection/{node_id}/data/unprocessed', |
||||
'method': 'POST'}] |
||||
), |
||||
] |
||||
|
||||
rule_policies = [ |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:rule:get', |
||||
'rule:is_admin', |
||||
'Get introspection rule(s)', |
||||
[{'path': '/rules', 'method': 'GET'}, |
||||
{'path': '/rules/{rule_id}', 'method': 'GET'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:rule:delete', |
||||
'rule:is_admin', |
||||
'Delete introspection rule(s)', |
||||
[{'path': '/rules', 'method': 'DELETE'}, |
||||
{'path': '/rules/{rule_id}', 'method': 'DELETE'}] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
'introspection:rule:create', |
||||
'rule:is_admin', |
||||
'Create introspection rule', |
||||
[{'path': '/rules', 'method': 'POST'}] |
||||
), |
||||
] |
||||
|
||||
|
||||
def list_policies(): |
||||
"""Get list of all policies defined in code. |
||||
|
||||
Used to register them all at runtime, |
||||
and by oslo-config-generator to generate sample policy files. |
||||
""" |
||||
policies = itertools.chain( |
||||
default_policies, |
||||
api_version_policies, |
||||
introspection_policies, |
||||
rule_policies) |
||||
return policies |
||||
|
||||
|
||||
@lockutils.synchronized('policy_enforcer') |
||||
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 |
||||
_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 |
||||
|
||||
|
||||
def get_oslo_policy_enforcer(): |
||||
"""Get the enforcer instance to generate policy files. |
||||
|
||||
This method is for use by oslopolicy CLI scripts. |
||||
Those scripts need the 'output-file' and 'namespace' options, |
||||
but having those in sys.argv means loading the inspector config options |
||||
will fail as those are not expected to be present. |
||||
So we pass in an arg list with those stripped out. |
||||
""" |
||||
|
||||
conf_args = [] |
||||
# Start at 1 because cfg.CONF expects the equivalent of sys.argv[1:] |
||||
i = 1 |
||||
while i < len(sys.argv): |
||||
if sys.argv[i].strip('-') in ['namespace', 'output-file']: |
||||
# e.g. --namespace <somestring> |
||||
i += 2 |
||||
continue |
||||
conf_args.append(sys.argv[i]) |
||||
i += 1 |
||||
|
||||
cfg.CONF(conf_args, project='ironic-inspector') |
||||
|
||||
return get_enforcer() |
||||
|
||||
|
||||
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. |
||||
args and kwargs are passed directly to oslo.policy Enforcer.authorize |
||||
Always returns True if CONF.auth_strategy != keystone. |
||||
|
||||
:param rule: name of a registered oslo.policy rule |
||||
:param target: dict-like structure to check rule against |
||||
:param creds: dict of policy values from request |
||||
:returns: True if request is authorized against given policy, |
||||
False otherwise |
||||
:raises: oslo_policy.policy.PolicyNotRegistered if supplied policy |
||||
is not registered in oslo_policy |
||||
""" |
||||
if CONF.auth_strategy != 'keystone': |
||||
return True |
||||
enforcer = get_enforcer() |
||||
rule = CONF.oslo_policy.policy_default_rule if rule is None else rule |
||||
return enforcer.authorize(rule, target, creds, *args, **kwargs) |
@ -0,0 +1,40 @@
|
||||
# |
||||
# 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 os |
||||
|
||||
import fixtures |
||||
from oslo_config import cfg |
||||
from oslo_policy import opts as policy_opts |
||||
|
||||
from ironic_inspector import policy as inspector_policy |
||||
|
||||
CONF = cfg.CONF |
||||
|
||||
policy_data = """{ |
||||
} |
||||
""" |
||||
|
||||
|
||||
class PolicyFixture(fixtures.Fixture): |
||||
def setUp(self): |
||||
super(PolicyFixture, self).setUp() |
||||
self.policy_dir = self.useFixture(fixtures.TempDir()) |
||||
self.policy_file_name = os.path.join(self.policy_dir.path, |
||||
'policy.json') |
||||
with open(self.policy_file_name, 'w') as policy_file: |
||||
policy_file.write(policy_data) |
||||
policy_opts.set_defaults(CONF) |
||||
CONF.set_override('policy_file', self.policy_file_name, 'oslo_policy') |
||||
inspector_policy._ENFORCER = None |
||||
self.addCleanup(inspector_policy.get_enforcer().clear) |
@ -0,0 +1,3 @@
|
||||
[DEFAULT] |
||||
output_file = policy.yaml.sample |
||||
namespace = ironic_inspector.api |
@ -0,0 +1,59 @@
|
||||
# Full read/write API access |
||||
#"is_admin": "role:admin or role:administrator or role:baremetal_admin" |
||||
|
||||
# Read-only API access |
||||
#"is_observer": "role:baremetal_observer" |
||||
|
||||
# Internal flag for public API routes |
||||
#"public_api": "is_public_api:True" |
||||
|
||||
# Default API access policy |
||||
#"default": "!" |
||||
|
||||
# Access the API root for available versions information |
||||
# GET / |
||||
#"introspection": "rule:public_api" |
||||
|
||||
# Access the versioned API root for version information |
||||
# GET /{version} |
||||
#"introspection:version": "rule:public_api" |
||||
|
||||
# Ramdisk callback to continue introspection |
||||
# POST /continue |
||||
#"introspection:continue": "rule:public_api" |
||||
|
||||
# Get introspection status |
||||
# GET /introspection |
||||
# GET /introspection/{node_id} |
||||
#"introspection:status": "rule:is_admin or rule:is_observer" |
||||
|
||||
# Start introspection |
||||
# POST /introspection/{node_id} |
||||
#"introspection:start": "rule:is_admin" |
||||
|
||||
# Abort introspection |
||||
# POST /introspection/{node_id}/abort |
||||
#"introspection:abort": "rule:is_admin" |
||||
|
||||
# Get introspection data |
||||
# GET /introspection/{node_id}/data |
||||
#"introspection:data": "rule:is_admin" |
||||
|
||||
# Reapply introspection on stored data |
||||
# POST /introspection/{node_id}/data/unprocessed |
||||
#"introspection:reapply": "rule:is_admin" |
||||
|
||||
# Get introspection rule(s) |
||||
# GET /rules |
||||
# GET /rules/{rule_id} |
||||
#"introspection:rule:get": "rule:is_admin" |
||||
|
||||
# Delete introspection rule(s) |
||||
# DELETE /rules |
||||
# DELETE /rules/{rule_id} |
||||
#"introspection:rule:delete": "rule:is_admin" |
||||
|
||||
# Create introspection rule |
||||
# POST /rules |
||||
#"introspection:rule:create": "rule:is_admin" |
||||
|
@ -0,0 +1,35 @@
|
||||
--- |
||||
features: |
||||
- | |
||||
Added an API access policy enforcment (based on oslo.policy rules). |
||||
Similar to other OpenStack services, operators now can configure |
||||
fine-grained access policies using ``policy.yaml`` file. |
||||
See example ``policy.yaml.sample`` file included in the code tree |
||||
for the list of available policies and their default rules. |
||||
This file can also be generated from the code tree |
||||
with ``tox -egenpolicy`` command. |
||||
|
||||
See ``oslo.policy`` package documentation for more information |
||||
on using and configuring API access policies. |
||||
|
||||
upgrade: |
||||
- | |
||||
Due to the choice of default values for API access policies rules, |
||||
some API parts of the ironic-inspector service will become available |
||||
to wider range of users after upgrade: |
||||
|
||||
- general access to the whole API is by default granted to a user |
||||
with either ``admin``, ``administrator`` or ``baremetal_admin`` |
||||
role (previously it allowed access only to a user with ``admin`` |
||||
role) |
||||
- listing of current introspections and showing a given |
||||
introspection is by default also allowed to the user with the |
||||
``baremetal_observer`` role |
||||
|
||||
If these access policies are not suiting a given deployment before |
||||
upgrade, operator will have to create a ``policy.json`` file |
||||
in the inspector configuration folder (usually ``/etc/inspector``) |
||||
that redefines the API rules as required. |
||||
|
||||
See ``oslo.policy`` package documentation for more information |
||||
on using and configuring API access policies. |
Loading…
Reference in new issue