
olso.policy 4.5.0[1] changed the config options policy_file default value to 'policy.yaml', which means it is changed for all the OpenStack services and they do not need to override the default anymore. NOTE: There is no change in behaviour here, oslo.policy provides the same configuration that services have overridden till now. [1] https://review.opendev.org/c/openstack/releases/+/934012 [2] https://review.opendev.org/c/openstack/requirements/+/934295 Change-Id: I547c2f8d3679b657ae14a08145c84c4c81f0733b
715 lines
29 KiB
Python
715 lines
29 KiB
Python
# Copyright (c) 2012 OpenStack Foundation.
|
|
# 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 collections import abc
|
|
import copy
|
|
import re
|
|
import sys
|
|
|
|
from oslo_config import cfg
|
|
from oslo_db import exception as db_exc
|
|
from oslo_log import log as logging
|
|
from oslo_policy import opts
|
|
from oslo_policy import policy
|
|
from oslo_utils import excutils
|
|
from oslo_utils import importutils
|
|
|
|
from tacker._i18n import _
|
|
from tacker.api.v1 import attributes
|
|
from tacker.common import exceptions
|
|
from tacker.common.utils import is_valid_area
|
|
from tacker import policies
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
_ENFORCER = None
|
|
ADMIN_CTX_POLICY = 'context_is_admin'
|
|
|
|
# TODO(gmann): Remove setting the default value of config options:
|
|
# - 'enforce_scope', and 'enforce_new_defaults' once cinder is ready with the
|
|
# new RBAC (oslo_policy enable them by default)
|
|
DEFAULT_POLICY_FILE = 'policy.yaml'
|
|
opts.set_defaults(
|
|
cfg.CONF,
|
|
enforce_scope=False,
|
|
enforce_new_defaults=False)
|
|
|
|
|
|
def reset():
|
|
global _ENFORCER
|
|
if _ENFORCER:
|
|
_ENFORCER.clear()
|
|
_ENFORCER = None
|
|
|
|
|
|
def init(conf=cfg.CONF, policy_file=None, suppress_deprecation_warnings=False):
|
|
"""Init an instance of the Enforcer class."""
|
|
|
|
global _ENFORCER
|
|
if not _ENFORCER:
|
|
_ENFORCER = policy.Enforcer(conf, policy_file=policy_file)
|
|
# NOTE(gmann): Explicitly disable the warnings for policies
|
|
# changing their default check_str. During policy-defaults-refresh
|
|
# work, all the policy defaults have been changed and warning for
|
|
# each policy started filling the logs limit for various tool.
|
|
# Once we move to new defaults only world then we can enable these
|
|
# warning again.
|
|
_ENFORCER.suppress_default_change_warnings = True
|
|
if suppress_deprecation_warnings:
|
|
_ENFORCER.suppress_deprecation_warnings = True
|
|
register_rules(_ENFORCER)
|
|
_ENFORCER.load_rules()
|
|
|
|
|
|
def _pre_enhanced_policy_check(target, credentials, target_chk=True):
|
|
"""Preprocesses target and credentials for enhanced tacker policy.
|
|
|
|
This method does the following things:
|
|
1) Convert special roles to enhanced policy attributes in credentials.
|
|
Note: Special roles are roles that have prefixes 'AREA_', 'VENDOR_',
|
|
or 'TENANT_'.
|
|
Example::
|
|
|
|
Before conversion:
|
|
credentials = {
|
|
'roles': [
|
|
'AREA_area_A@region_A',
|
|
'VENDOR_company_A',
|
|
'TENANT_default'
|
|
]
|
|
}
|
|
After conversion:
|
|
credentials = {
|
|
'roles': [
|
|
'AREA_area_A@region_A',
|
|
'VENDOR_company_A',
|
|
'TENANT_default'
|
|
],
|
|
'area': ['*', 'area_A@region_A'],
|
|
'vendor': ['*', 'company_A'],
|
|
'tenant: ['*', 'default']
|
|
}
|
|
|
|
2) Check whether using special role(ex: `all`) in target
|
|
As not assume using such in the further procedure, this method return here
|
|
in case check is NG.
|
|
However, once a object with the special role set as an attribute has been
|
|
registered, this check will prevent access to that object.
|
|
Therefore, for only user who has credentials with admin role or special
|
|
role(ex: `Tenant_all`) this check is skipped.
|
|
|
|
3) Convert special value `all` to the corresponding attribute value in
|
|
target.
|
|
|
|
:param target: a dictionary of the attributes of the object
|
|
being accessed.
|
|
:param credentials: The information about the user performing the action.
|
|
:param target_chk: boolean
|
|
True: execute check of 2)
|
|
False: skip check of 2)
|
|
:return tgt: The preprocessed target is returned.
|
|
:return user_attrs: The preprocessed credentials is returned.
|
|
:raises tacker.common.exceptions.PolicyTargetCheckError
|
|
if check of 2) is NG
|
|
"""
|
|
if not cfg.CONF.oslo_policy.enhanced_tacker_policy:
|
|
return target, credentials
|
|
|
|
if target is None:
|
|
tgt = {}
|
|
else:
|
|
tgt = copy.copy(target)
|
|
|
|
LOG.debug(f'target: {target}')
|
|
|
|
special_roles = {'area': r'all@\w+', 'vendor': 'all', 'tenant': 'all'}
|
|
convert_map = {
|
|
'area_': 'area',
|
|
'vendor_': 'vendor',
|
|
'tenant_': 'tenant'
|
|
}
|
|
user_attrs = {
|
|
'area': ['*'],
|
|
'vendor': ['*'],
|
|
'tenant': ['*']
|
|
}
|
|
# 1) Convert special roles to enhanced policy attributes in credentials.
|
|
for role in credentials.get('roles'):
|
|
role = role.lower()
|
|
for prefix, key in convert_map.items():
|
|
if role.startswith(prefix):
|
|
attr = role[len(prefix):]
|
|
if attr:
|
|
user_attrs[key].append(attr)
|
|
|
|
# 2) Check whether using special role(ex: `all`) in target,
|
|
if target_chk and not credentials.get('is_admin', False):
|
|
for k, v in tgt.items():
|
|
if (k in special_roles and
|
|
re.match(special_roles[k], v)):
|
|
chk_all = False
|
|
# Not NG in case credentials has special role
|
|
for uv in user_attrs[k]:
|
|
if re.match(special_roles[k], uv):
|
|
# In case 'area':
|
|
# OK case1 : credential is all@all and
|
|
# target is all@region_A
|
|
# OK case2 : credential is all@region_A and
|
|
# target is all@region_A
|
|
# NG case : credential is all@region_A and
|
|
# target is all@region_B
|
|
if k == 'area' and uv != 'all@all' and uv != v.lower():
|
|
continue
|
|
chk_all = True
|
|
if not chk_all:
|
|
msg = f'`{v}` of {k} in target is special role'
|
|
LOG.error(msg)
|
|
raise exceptions.PolicyTargetCheckError(reason=msg)
|
|
|
|
common_keys = user_attrs.keys() & tgt.keys()
|
|
|
|
# 3) Convert special value `all` to the corresponding attribute value in
|
|
# target.
|
|
for key in common_keys:
|
|
tgt[key] = tgt[key].lower()
|
|
attrs = user_attrs.get(key)
|
|
if tgt.get(key) == '*':
|
|
continue
|
|
to_remove = []
|
|
orig_attrs = copy.deepcopy(attrs)
|
|
if 'area' == key:
|
|
if not is_valid_area(tgt.get(key)):
|
|
continue
|
|
for attr in orig_attrs:
|
|
if not is_valid_area(attr):
|
|
continue
|
|
if 'all@all' == attr:
|
|
# example:
|
|
# target = {'area': 'area_A@region_A'}
|
|
# then:
|
|
# 'all@all' -> 'area_A@region_A'
|
|
to_remove.append('all@all')
|
|
attrs.append(tgt.get(key))
|
|
elif attr.startswith('all@'):
|
|
to_remove.append(attr)
|
|
area, region = attr.split('@', 1)
|
|
t_area, t_region = tgt.get(key).split('@', 1)
|
|
if region == t_region:
|
|
# example:
|
|
# target = {'area': 'area_A@region_A'}
|
|
# then:
|
|
# 'all@region_A' -> 'area_A@region_A'
|
|
attrs.append(f'{t_area}@{region}')
|
|
# else:
|
|
# example:
|
|
# target = {'area': 'area_A@region_B'}
|
|
# then:
|
|
# 'all@region_A' -> to be removed.
|
|
|
|
else:
|
|
for attr in orig_attrs:
|
|
if 'all' == attr:
|
|
# example:
|
|
# target = {'vendor': 'company_A'}
|
|
# then:
|
|
# 'all' -> 'company_A'
|
|
to_remove.append('all')
|
|
attrs.append(tgt.get(key))
|
|
for item in to_remove:
|
|
attrs.remove(item)
|
|
|
|
user_attrs.update(credentials)
|
|
|
|
return tgt, user_attrs
|
|
|
|
|
|
def authorize(context, action, target, do_raise=True, exc=None):
|
|
|
|
init()
|
|
credentials = context.to_policy_values()
|
|
# NOTE(gmann): For system, scope token, oslo.policy check
|
|
# for a key 'system' in creds. The oslo.context library uses
|
|
# `system_scope` instead. Because we are converting the context
|
|
# attribute to creds via context.to_policy_values which does not
|
|
# convert 'system_scope' key. There are two ways to solve this:
|
|
# 1. Pass full context to oslo.policy and their it convert this key
|
|
# but Tacker has special case of enhanced policy conversion
|
|
# (via _pre_enhanced_policy_check(), method which sets its own
|
|
# key in creds. So passing full context to oslo.policy make enhance
|
|
# policy conversion more complex.
|
|
# 2. Set 'system' key in creds explicitly. This is easy and more
|
|
# readable way.
|
|
if context.system_scope:
|
|
credentials['system'] = context.system_scope
|
|
if not exc:
|
|
exc = exceptions.PolicyNotAuthorized
|
|
|
|
try:
|
|
target, credentials = _pre_enhanced_policy_check(target, credentials)
|
|
except exceptions.PolicyTargetCheckError:
|
|
LOG.error('Policy check for %(action)s failed with target check '
|
|
'%(target)s', {'action': action, 'target': target})
|
|
raise exc(action=action)
|
|
|
|
try:
|
|
result = _ENFORCER.authorize(action, target, credentials,
|
|
do_raise=do_raise, exc=exc, action=action)
|
|
except policy.PolicyNotRegistered:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error('Policy not registered')
|
|
except policy.InvalidScope:
|
|
LOG.debug('Policy check for %(action)s failed with scope check '
|
|
'%(credentials)s',
|
|
{'action': action,
|
|
'credentials': credentials})
|
|
raise exc(action=action)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error('Policy check for %(action)s failed with credentials '
|
|
'%(credentials)s',
|
|
{'action': action, 'credentials': credentials})
|
|
|
|
return result
|
|
|
|
|
|
def refresh(policy_file=None):
|
|
"""Reset policy and init a new instance of Enforcer."""
|
|
reset()
|
|
init(policy_file=policy_file)
|
|
|
|
|
|
def get_resource_and_action(action, pluralized=None):
|
|
"""Return resource and enforce_attr_based_check(boolean).
|
|
|
|
It is per resource and action extracted from api operation.
|
|
"""
|
|
|
|
data = action.split(':', 1)[0].split('_', 1)
|
|
resource = pluralized or ("%ss" % data[-1])
|
|
enforce_attr_based_check = data[0] not in ('get', 'delete')
|
|
return (resource, enforce_attr_based_check)
|
|
|
|
|
|
def set_rules(policies, overwrite=True):
|
|
"""Set rules based on the provided dict of rules.
|
|
|
|
:param policies: New policies to use. It should be an instance of dict.
|
|
:param overwrite: Whether to overwrite current rules or update them
|
|
with the new rules.
|
|
"""
|
|
|
|
LOG.debug("Loading policies from file: %s", _ENFORCER.policy_path)
|
|
init()
|
|
_ENFORCER.set_rules(policies, overwrite)
|
|
|
|
|
|
def _is_attribute_explicitly_set(attribute_name, resource, target, action):
|
|
"""Verify that an attribute is present and is explicitly set."""
|
|
if 'update' in action:
|
|
# In the case of update, the function should not pay attention to a
|
|
# default value of an attribute, but check whether it was explicitly
|
|
# marked as being updated instead.
|
|
return (attribute_name in target[attributes.ATTRIBUTES_TO_UPDATE] and
|
|
target[attribute_name] is not attributes.ATTR_NOT_SPECIFIED)
|
|
return ('default' in resource[attribute_name] and
|
|
attribute_name in target and
|
|
target[attribute_name] is not attributes.ATTR_NOT_SPECIFIED and
|
|
target[attribute_name] != resource[attribute_name]['default'])
|
|
|
|
|
|
def _should_validate_sub_attributes(attribute, sub_attr):
|
|
"""Verify that sub-attributes are iterable and should be validated."""
|
|
validate = attribute.get('validate')
|
|
return (validate and isinstance(sub_attr, abc.Iterable) and
|
|
any([k.startswith('type:dict') and
|
|
v for (k, v) in validate.items()]))
|
|
|
|
|
|
def _build_subattr_match_rule(attr_name, attr, action, target):
|
|
"""Create the rule to match for sub-attribute policy checks."""
|
|
# TODO(salv-orlando): Instead of relying on validator info, introduce
|
|
# typing for API attributes
|
|
# Expect a dict as type descriptor
|
|
validate = attr['validate']
|
|
key = list(filter(lambda k: k.startswith('type:dict'), validate.keys()))
|
|
if not key:
|
|
LOG.warning("Unable to find data type descriptor for attribute %s",
|
|
attr_name)
|
|
return
|
|
data = validate[key[0]]
|
|
if not isinstance(data, dict):
|
|
LOG.debug("Attribute type descriptor is not a dict. Unable to "
|
|
"generate any sub-attr policy rule for %s.",
|
|
attr_name)
|
|
return
|
|
sub_attr_rules = [policy.RuleCheck('rule', '%s:%s:%s' %
|
|
(action, attr_name,
|
|
sub_attr_name)) for
|
|
sub_attr_name in data if sub_attr_name in
|
|
target[attr_name]]
|
|
return policy.AndCheck(sub_attr_rules)
|
|
|
|
|
|
def _process_rules_list(rules, match_rule):
|
|
"""Recursively walk a policy rule to extract a list of match entries."""
|
|
if isinstance(match_rule, policy.RuleCheck):
|
|
rules.append(match_rule.match)
|
|
elif isinstance(match_rule, policy.AndCheck):
|
|
for rule in match_rule.rules:
|
|
_process_rules_list(rules, rule)
|
|
return rules
|
|
|
|
|
|
def _build_match_rule(action, target, pluralized):
|
|
"""Create the rule to match for a given action.
|
|
|
|
The policy rule to be matched is built in the following way:
|
|
1) add entries for matching permission on objects
|
|
2) add an entry for the specific action (e.g.: create_network)
|
|
3) add an entry for attributes of a resource for which the action
|
|
is being executed (e.g.: create_network:shared)
|
|
4) add an entry for sub-attributes of a resource for which the
|
|
action is being executed
|
|
(e.g.: create_router:external_gateway_info:network_id)
|
|
"""
|
|
match_rule = policy.RuleCheck('rule', action)
|
|
resource, enforce_attr_based_check = get_resource_and_action(
|
|
action, pluralized)
|
|
if enforce_attr_based_check:
|
|
# assigning to variable with short name for improving readability
|
|
res_map = attributes.RESOURCE_ATTRIBUTE_MAP
|
|
if resource in res_map:
|
|
for attribute_name in res_map[resource]:
|
|
if _is_attribute_explicitly_set(attribute_name,
|
|
res_map[resource],
|
|
target, action):
|
|
attribute = res_map[resource][attribute_name]
|
|
if 'enforce_policy' in attribute:
|
|
attr_rule = policy.RuleCheck('rule', '%s:%s' %
|
|
(action, attribute_name))
|
|
# Build match entries for sub-attributes
|
|
if _should_validate_sub_attributes(
|
|
attribute, target[attribute_name]):
|
|
attr_rule = policy.AndCheck(
|
|
[attr_rule, _build_subattr_match_rule(
|
|
attribute_name, attribute,
|
|
action, target)])
|
|
match_rule = policy.AndCheck([match_rule, attr_rule])
|
|
return match_rule
|
|
|
|
|
|
# This check is registered as 'tenant_id' so that it can override
|
|
# GenericCheck which was used for validating parent resource ownership.
|
|
# This will prevent us from having to handling backward compatibility
|
|
# for policy.yaml
|
|
# TODO(salv-orlando): Reinstate GenericCheck for simple tenant_id checks
|
|
@policy.register('tenant_id')
|
|
class OwnerCheck(policy.Check):
|
|
"""Resource ownership check.
|
|
|
|
This check verifies the owner of the current resource, or of another
|
|
resource referenced by the one under analysis.
|
|
In the former case it falls back to a regular GenericCheck, whereas
|
|
in the latter case it leverages the plugin to load the referenced
|
|
resource and perform the check.
|
|
"""
|
|
def __init__(self, kind, match):
|
|
# Process the match
|
|
try:
|
|
self.target_field = re.findall(r'^\%\((.*)\)s$',
|
|
match)[0]
|
|
except IndexError:
|
|
err_reason = (_("Unable to identify a target field from: %s. "
|
|
"Match should be in the form %%(<field_name>)s") %
|
|
match)
|
|
LOG.exception(err_reason)
|
|
raise exceptions.PolicyInitError(
|
|
policy="%s:%s" % (kind, match),
|
|
reason=err_reason)
|
|
super(OwnerCheck, self).__init__(kind, match)
|
|
|
|
def __call__(self, target, creds, enforcer):
|
|
if self.target_field not in target:
|
|
# policy needs a plugin check
|
|
# target field is in the form resource:field
|
|
# however if they're not separated by a colon, use an underscore
|
|
# as a separator for backward compatibility
|
|
|
|
def do_split(separator):
|
|
parent_res, parent_field = self.target_field.split(
|
|
separator, 1)
|
|
return parent_res, parent_field
|
|
|
|
for separator in (':', '_'):
|
|
try:
|
|
parent_res, parent_field = do_split(separator)
|
|
break
|
|
except ValueError:
|
|
LOG.debug("Unable to find ':' as separator in %s.",
|
|
self.target_field)
|
|
else:
|
|
# If we are here split failed with both separators
|
|
err_reason = ("Unable to find resource name in %s" %
|
|
self.target_field)
|
|
LOG.error(err_reason)
|
|
raise exceptions.PolicyCheckError(
|
|
policy="%s:%s" % (self.kind, self.match),
|
|
reason=err_reason)
|
|
parent_foreign_key = attributes.RESOURCE_FOREIGN_KEYS.get(
|
|
"%ss" % parent_res, None)
|
|
if not parent_foreign_key:
|
|
err_reason = ("Unable to verify match:%(match)s as the "
|
|
"parent resource: %(res)s was not found" %
|
|
{'match': self.match, 'res': parent_res})
|
|
LOG.error(err_reason)
|
|
raise exceptions.PolicyCheckError(
|
|
policy="%s:%s" % (self.kind, self.match),
|
|
reason=err_reason)
|
|
# NOTE(salv-orlando): This check currently assumes the parent
|
|
# resource is handled by the core plugin. It might be worth
|
|
# having a way to map resources to plugins so to make this
|
|
# check more general
|
|
# NOTE(ihrachys): if import is put in global, circular
|
|
# import failure occurs
|
|
manager = importutils.import_module('tacker.manager')
|
|
f = getattr(manager.TackerManager.get_instance().plugin,
|
|
'get_%s' % parent_res)
|
|
# f *must* exist, if not found it is better to let tacker
|
|
# explode. Check will be performed with admin context
|
|
context = importutils.import_module('tacker.context')
|
|
try:
|
|
data = f(context.get_admin_context(),
|
|
target[parent_foreign_key],
|
|
fields=[parent_field])
|
|
target[self.target_field] = data[parent_field]
|
|
except exceptions.NotFound as e:
|
|
# NOTE(kevinbenton): a NotFound exception can occur if a
|
|
# list operation is happening at the same time as one of
|
|
# the parents and its children being deleted. So we issue
|
|
# a RetryRequest so the API will redo the lookup and the
|
|
# problem items will be gone.
|
|
raise db_exc.RetryRequest(e)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.exception('Policy check error while calling %s!', f)
|
|
match = self.match % target
|
|
if self.kind in creds:
|
|
return match == str(creds[self.kind])
|
|
return False
|
|
|
|
|
|
@policy.register('field')
|
|
class FieldCheck(policy.Check):
|
|
def __init__(self, kind, match):
|
|
# Process the match
|
|
resource, field_value = match.split(':', 1)
|
|
field, value = field_value.split('=', 1)
|
|
|
|
super(FieldCheck, self).__init__(kind, '%s:%s:%s' %
|
|
(resource, field, value))
|
|
|
|
# Value might need conversion - we need help from the attribute map
|
|
try:
|
|
attr = attributes.RESOURCE_ATTRIBUTE_MAP[resource][field]
|
|
conv_func = attr['convert_to']
|
|
except KeyError:
|
|
conv_func = lambda x: x # noqa: E731
|
|
|
|
self.field = field
|
|
self.value = conv_func(value)
|
|
self.regex = re.compile(value[1:]) if value.startswith('~') else None
|
|
|
|
def __call__(self, target_dict, cred_dict, enforcer):
|
|
target_value = target_dict.get(self.field)
|
|
# target_value might be a boolean, explicitly compare with None
|
|
if target_value is None:
|
|
LOG.debug("Unable to find requested field: %(field)s in target: "
|
|
"%(target_dict)s",
|
|
{'field': self.field, 'target_dict': target_dict})
|
|
return False
|
|
if self.regex:
|
|
return bool(self.regex.match(target_value))
|
|
return target_value == self.value
|
|
|
|
|
|
def _prepare_check(context, action, target, pluralized):
|
|
"""Prepare rule, target, and credentials for the policy engine."""
|
|
# Compare with None to distinguish case in which target is {}
|
|
if target is None:
|
|
target = {}
|
|
match_rule = _build_match_rule(action, target, pluralized)
|
|
credentials = context.to_dict()
|
|
return match_rule, target, credentials
|
|
|
|
|
|
def log_rule_list(match_rule):
|
|
if LOG.isEnabledFor(logging.DEBUG):
|
|
rules = _process_rules_list([], match_rule)
|
|
LOG.debug("Enforcing rules: %s", rules)
|
|
|
|
|
|
def check(context, action, target, plugin=None, might_not_exist=False,
|
|
pluralized=None):
|
|
"""Verifies that the action is valid on the target in this context.
|
|
|
|
:param context: tacker context
|
|
:param action: string representing the action to be checked
|
|
this should be colon separated for clarity.
|
|
:param target: dictionary representing the object of the action
|
|
for object creation this should be a dictionary representing the
|
|
location of the object e.g. ``{'project_id': context.project_id}``
|
|
:param plugin: currently unused and deprecated.
|
|
Kept for backward compatibility.
|
|
:param might_not_exist: If True the policy check is skipped (and the
|
|
function returns True) if the specified policy does not exist.
|
|
Defaults to false.
|
|
:param pluralized: pluralized case of resource
|
|
e.g. firewall_policy -> pluralized = "firewall_policies"
|
|
|
|
:return: Returns True if access is permitted else False.
|
|
"""
|
|
# If we already know the context has admin rights do not perform an
|
|
# additional check and authorize the operation
|
|
if context.is_admin:
|
|
return True
|
|
if might_not_exist and not (_ENFORCER.rules and action in _ENFORCER.rules):
|
|
return True
|
|
match_rule, target, credentials = _prepare_check(context,
|
|
action,
|
|
target,
|
|
pluralized)
|
|
target = copy.copy(target)
|
|
if 'area' not in target:
|
|
area = target.get('extra', {}).get('area')
|
|
if area:
|
|
target.update({'area': area})
|
|
if 'tenant_id' in target:
|
|
target['project_id'] = target['tenant_id']
|
|
try:
|
|
target, credentials = _pre_enhanced_policy_check(target, credentials)
|
|
except exceptions.PolicyTargetCheckError:
|
|
LOG.error('Policy check for %(action)s failed with target check '
|
|
'%(target)s', {'action': action, 'target': target})
|
|
return False
|
|
|
|
result = _ENFORCER.enforce(match_rule,
|
|
target,
|
|
credentials,
|
|
pluralized=pluralized)
|
|
# logging applied rules in case of failure
|
|
if not result:
|
|
log_rule_list(match_rule)
|
|
return result
|
|
|
|
|
|
def enforce(context, action, target, plugin=None, pluralized=None,
|
|
exc=exceptions.PolicyNotAuthorized):
|
|
"""Verifies that the action is valid on the target in this context.
|
|
|
|
:param context: tacker context
|
|
:param action: string representing the action to be checked
|
|
this should be colon separated for clarity.
|
|
:param target: dictionary representing the object of the action
|
|
for object creation this should be a dictionary representing the
|
|
location of the object e.g. ``{'project_id': context.project_id}``
|
|
:param plugin: currently unused and deprecated.
|
|
Kept for backward compatibility.
|
|
:param pluralized: pluralized case of resource
|
|
e.g. firewall_policy -> pluralized = "firewall_policies"
|
|
:param exc: Class of the exception to raise if the check fails.
|
|
If not specified, :class:`PolicyNotAuthorized` will be used.
|
|
|
|
:raises tacker.common.exceptions.PolicyNotAuthorized or exc specified by
|
|
caller:
|
|
if verification fails.
|
|
"""
|
|
# If we already know the context has admin rights do not perform an
|
|
# additional check and authorize the operation
|
|
if context.is_admin:
|
|
return True
|
|
rule, target, credentials = _prepare_check(context,
|
|
action,
|
|
target,
|
|
pluralized)
|
|
target = copy.copy(target)
|
|
if 'area' not in target:
|
|
area = target.get('extra', {}).get('area')
|
|
if area:
|
|
target.update({'area': area})
|
|
if 'tenant_id' in target:
|
|
target['project_id'] = target['tenant_id']
|
|
|
|
# About target_chk:
|
|
# Set target_chk=False, as not execute target check.
|
|
# The reason is that matching with the behavior of other APIs
|
|
# that pass through the 'authorize' method.
|
|
# ex:
|
|
# 1 openstack vim register with area set special role
|
|
# (this method is passed through, for policy check)
|
|
# -> target_chk=True, policy check `NG`
|
|
# 2 instantiate with request parameter area set special role
|
|
# (`authorize` method is passed through, for policy check)
|
|
# -> target_chk=True, policy check `OK`
|
|
# -> There is a difference in the behavior of 1 and 2
|
|
target, credentials = _pre_enhanced_policy_check(
|
|
target, credentials, target_chk=False)
|
|
try:
|
|
result = _ENFORCER.enforce(rule, target, credentials, action=action,
|
|
do_raise=True, exc=exc)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
log_rule_list(rule)
|
|
LOG.error("Failed policy check for '%s'", action)
|
|
return result
|
|
|
|
|
|
def check_is_admin(context):
|
|
"""Verify context has admin rights according to policy settings."""
|
|
init()
|
|
# the target is user-self
|
|
credentials = context.to_policy_values()
|
|
try:
|
|
return _ENFORCER.authorize(ADMIN_CTX_POLICY, credentials, credentials)
|
|
except policy.PolicyNotRegistered:
|
|
return False
|
|
|
|
|
|
def register_rules(enforcer):
|
|
enforcer.register_defaults(policies.list_rules())
|
|
|
|
|
|
def get_enforcer():
|
|
# NOTE(amotoki): This was borrowed from nova/policy.py.
|
|
# This method is for use by oslo.policy CLI scripts. Those scripts need the
|
|
# 'output-file' and 'namespace' options, but having those in sys.argv means
|
|
# loading the tacker 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']:
|
|
i += 2
|
|
continue
|
|
conf_args.append(sys.argv[i])
|
|
i += 1
|
|
|
|
# 'project' must be 'tacker' so that get_enforcer looks at
|
|
# /etc/tacker/policy.yaml by default.
|
|
cfg.CONF(conf_args, project='tacker')
|
|
init()
|
|
return _ENFORCER
|