"Fossies" - the Fresh Open Source Software Archive

Member "neutron-14.0.3/neutron/policy.py" (22 Oct 2019, 20997 Bytes) of package /linux/misc/openstack/neutron-14.0.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "policy.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 14.0.2_vs_14.0.3.

    1 # Copyright (c) 2012 OpenStack Foundation.
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 
   16 import collections
   17 import itertools
   18 import re
   19 import sys
   20 
   21 from neutron_lib.api import attributes
   22 from neutron_lib.api.definitions import network as net_apidef
   23 from neutron_lib import constants
   24 from neutron_lib import context
   25 from neutron_lib import exceptions
   26 from neutron_lib.plugins import directory
   27 from oslo_config import cfg
   28 from oslo_db import exception as db_exc
   29 from oslo_log import log as logging
   30 from oslo_policy import policy
   31 from oslo_utils import excutils
   32 import six
   33 import stevedore
   34 
   35 from neutron._i18n import _
   36 from neutron.common import cache_utils as cache
   37 from neutron.common import constants as const
   38 
   39 
   40 LOG = logging.getLogger(__name__)
   41 
   42 _ENFORCER = None
   43 ADMIN_CTX_POLICY = 'context_is_admin'
   44 ADVSVC_CTX_POLICY = 'context_is_advsvc'
   45 
   46 # Identify the attribute used by a resource to reference another resource
   47 _RESOURCE_FOREIGN_KEYS = {
   48     net_apidef.COLLECTION_NAME: 'network_id'
   49 }
   50 
   51 
   52 def reset():
   53     global _ENFORCER
   54     if _ENFORCER:
   55         _ENFORCER.clear()
   56         _ENFORCER = None
   57 
   58 
   59 def register_rules(enforcer):
   60     extmgr = stevedore.extension.ExtensionManager('neutron.policies',
   61                                                   invoke_on_load=True)
   62     policies = [list(e.obj) for e in extmgr.extensions]
   63     LOG.debug('Loaded default policies from %s '
   64               'under neutron.policies entry points',
   65               [e.name for e in extmgr.extensions])
   66     enforcer.register_defaults(itertools.chain(*policies))
   67 
   68 
   69 def init(conf=cfg.CONF, policy_file=None):
   70     """Init an instance of the Enforcer class."""
   71 
   72     global _ENFORCER
   73     if not _ENFORCER:
   74         _ENFORCER = policy.Enforcer(conf, policy_file=policy_file)
   75         register_rules(_ENFORCER)
   76         _ENFORCER.load_rules(True)
   77 
   78 
   79 def refresh(policy_file=None):
   80     """Reset policy and init a new instance of Enforcer."""
   81     reset()
   82     init(policy_file=policy_file)
   83 
   84 
   85 def get_resource_and_action(action, pluralized=None):
   86     """Return resource and enforce_attr_based_check(boolean) per
   87        resource and action extracted from api operation.
   88     """
   89     data = action.split(':', 1)[0].split('_', 1)
   90     resource = pluralized or ("%ss" % data[-1])
   91     enforce_attr_based_check = data[0] not in ('get', 'delete')
   92     return (resource, enforce_attr_based_check)
   93 
   94 
   95 def set_rules(policies, overwrite=True):
   96     """Set rules based on the provided dict of rules.
   97 
   98     :param policies: New policies to use. It should be an instance of dict.
   99     :param overwrite: Whether to overwrite current rules or update them
  100                           with the new rules.
  101     """
  102 
  103     LOG.debug("Loading policies from file: %s", _ENFORCER.policy_path)
  104     init()
  105     _ENFORCER.set_rules(policies, overwrite)
  106 
  107 
  108 def _is_attribute_explicitly_set(attribute_name, resource, target, action):
  109     """Verify that an attribute is present and is explicitly set."""
  110     if target.get(const.ATTRIBUTES_TO_UPDATE):
  111         # In the case of update, the function should not pay attention to a
  112         # default value of an attribute, but check whether it was explicitly
  113         # marked as being updated instead.
  114         return (attribute_name in target[const.ATTRIBUTES_TO_UPDATE] and
  115                 target[attribute_name] is not constants.ATTR_NOT_SPECIFIED)
  116     result = (attribute_name in target and
  117               target[attribute_name] is not constants.ATTR_NOT_SPECIFIED)
  118     if result and 'default' in resource[attribute_name]:
  119         return target[attribute_name] != resource[attribute_name]['default']
  120     return result
  121 
  122 
  123 def _should_validate_sub_attributes(attribute, sub_attr):
  124     """Verify that sub-attributes are iterable and should be validated."""
  125     validate = attribute.get('validate')
  126     return (validate and isinstance(sub_attr, collections.Iterable) and
  127             any([k.startswith('type:dict') and
  128                  v for (k, v) in validate.items()]))
  129 
  130 
  131 def _build_subattr_match_rule(attr_name, attr, action, target):
  132     """Create the rule to match for sub-attribute policy checks."""
  133     # TODO(salv-orlando): Instead of relying on validator info, introduce
  134     # typing for API attributes
  135     # Expect a dict as type descriptor
  136     validate = attr['validate']
  137     key = [k for k in validate.keys() if k.startswith('type:dict')]
  138     if not key:
  139         LOG.warning("Unable to find data type descriptor for attribute %s",
  140                     attr_name)
  141         return
  142     data = validate[key[0]]
  143     if not isinstance(data, dict):
  144         LOG.debug("Attribute type descriptor is not a dict. Unable to "
  145                   "generate any sub-attr policy rule for %s.",
  146                   attr_name)
  147         return
  148     sub_attr_rules = [policy.RuleCheck('rule', '%s:%s:%s' %
  149                                        (action, attr_name,
  150                                         sub_attr_name)) for
  151                       sub_attr_name in data if sub_attr_name in
  152                       target[attr_name]]
  153     return policy.AndCheck(sub_attr_rules)
  154 
  155 
  156 def _build_list_of_subattrs_rule(attr_name, attribute_value, action):
  157     rules = []
  158     for sub_attr in attribute_value:
  159         if isinstance(sub_attr, dict):
  160             for k in sub_attr:
  161                 rules.append(policy.RuleCheck(
  162                     'rule', '%s:%s:%s' % (action, attr_name, k)))
  163     if rules:
  164         return policy.AndCheck(rules)
  165 
  166 
  167 def _process_rules_list(rules, match_rule):
  168     """Recursively walk a policy rule to extract a list of match entries."""
  169     if isinstance(match_rule, policy.RuleCheck):
  170         rules.append(match_rule.match)
  171     elif isinstance(match_rule, policy.AndCheck):
  172         for rule in match_rule.rules:
  173             _process_rules_list(rules, rule)
  174     return rules
  175 
  176 
  177 def _build_match_rule(action, target, pluralized):
  178     """Create the rule to match for a given action.
  179 
  180     The policy rule to be matched is built in the following way:
  181     1) add entries for matching permission on objects
  182     2) add an entry for the specific action (e.g.: create_network)
  183     3) add an entry for attributes of a resource for which the action
  184        is being executed (e.g.: create_network:shared)
  185     4) add an entry for sub-attributes of a resource for which the
  186        action is being executed
  187        (e.g.: create_router:external_gateway_info:network_id)
  188     """
  189     match_rule = policy.RuleCheck('rule', action)
  190     resource, enforce_attr_based_check = get_resource_and_action(
  191         action, pluralized)
  192     if enforce_attr_based_check:
  193         # assigning to variable with short name for improving readability
  194         res_map = attributes.RESOURCES
  195         if resource in res_map:
  196             for attribute_name in res_map[resource]:
  197                 if _is_attribute_explicitly_set(attribute_name,
  198                                                 res_map[resource],
  199                                                 target, action):
  200                     attribute = res_map[resource][attribute_name]
  201                     if 'enforce_policy' in attribute:
  202                         attr_rule = policy.RuleCheck(
  203                             'rule', '%s:%s' % (action, attribute_name))
  204                         # Build match entries for sub-attributes
  205                         if _should_validate_sub_attributes(
  206                                 attribute, target[attribute_name]):
  207                             attr_rule = policy.AndCheck(
  208                                 [attr_rule, _build_subattr_match_rule(
  209                                     attribute_name, attribute,
  210                                     action, target)])
  211 
  212                         attribute_value = target[attribute_name]
  213                         if isinstance(attribute_value, list):
  214                             subattr_rule = _build_list_of_subattrs_rule(
  215                                 attribute_name, attribute_value, action)
  216                             if subattr_rule:
  217                                 attr_rule = policy.AndCheck(
  218                                     [attr_rule, subattr_rule])
  219 
  220                         match_rule = policy.AndCheck([match_rule, attr_rule])
  221     return match_rule
  222 
  223 
  224 # This check is registered as 'tenant_id' so that it can override
  225 # GenericCheck which was used for validating parent resource ownership.
  226 # This will prevent us from having to handling backward compatibility
  227 # for policy.json
  228 # TODO(salv-orlando): Reinstate GenericCheck for simple tenant_id checks
  229 @policy.register('tenant_id')
  230 class OwnerCheck(policy.Check):
  231     """Resource ownership check.
  232 
  233     This check verifies the owner of the current resource, or of another
  234     resource referenced by the one under analysis.
  235     In the former case it falls back to a regular GenericCheck, whereas
  236     in the latter case it leverages the plugin to load the referenced
  237     resource and perform the check.
  238     """
  239     def __init__(self, kind, match):
  240         # Process the match
  241         try:
  242             self.target_field = re.findall(r'^\%\((.*)\)s$',
  243                                            match)[0]
  244         except IndexError:
  245             err_reason = (_("Unable to identify a target field from:%s. "
  246                             "Match should be in the form %%(<field_name>)s") %
  247                           match)
  248             LOG.exception(err_reason)
  249             raise exceptions.PolicyInitError(
  250                 policy="%s:%s" % (kind, match),
  251                 reason=err_reason)
  252         self._cache = cache._get_memory_cache_region(expiration_time=5)
  253         super(OwnerCheck, self).__init__(kind, match)
  254 
  255     @cache.cache_method_results
  256     def _extract(self, resource_type, resource_id, field):
  257         # NOTE(salv-orlando): This check currently assumes the parent
  258         # resource is handled by the core plugin. It might be worth
  259         # having a way to map resources to plugins so to make this
  260         # check more general
  261         plugin = directory.get_plugin()
  262         if resource_type in const.EXT_PARENT_RESOURCE_MAPPING:
  263             plugin = directory.get_plugin(
  264                 const.EXT_PARENT_RESOURCE_MAPPING[resource_type])
  265         f = getattr(plugin, 'get_%s' % resource_type)
  266         # f *must* exist, if not found it is better to let neutron
  267         # explode. Check will be performed with admin context
  268         try:
  269             data = f(context.get_admin_context(),
  270                      resource_id,
  271                      fields=[field])
  272         except exceptions.NotFound as e:
  273             # NOTE(kevinbenton): a NotFound exception can occur if a
  274             # list operation is happening at the same time as one of
  275             # the parents and its children being deleted. So we issue
  276             # a RetryRequest so the API will redo the lookup and the
  277             # problem items will be gone.
  278             raise db_exc.RetryRequest(e)
  279         except Exception:
  280             with excutils.save_and_reraise_exception():
  281                 LOG.exception('Policy check error while calling %s!', f)
  282         return data[field]
  283 
  284     def __call__(self, target, creds, enforcer):
  285         if self.target_field not in target:
  286             # policy needs a plugin check
  287             # target field is in the form resource:field
  288             # however if they're not separated by a colon, use an underscore
  289             # as a separator for backward compatibility
  290 
  291             def do_split(separator):
  292                 parent_res, parent_field = self.target_field.split(
  293                     separator, 1)
  294                 return parent_res, parent_field
  295 
  296             for separator in (':', '_'):
  297                 try:
  298                     parent_res, parent_field = do_split(separator)
  299                     break
  300                 except ValueError:
  301                     LOG.debug("Unable to find ':' as separator in %s.",
  302                               self.target_field)
  303             else:
  304                 # If we are here split failed with both separators
  305                 err_reason = (_("Unable to find resource name in %s") %
  306                               self.target_field)
  307                 LOG.error(err_reason)
  308                 raise exceptions.PolicyCheckError(
  309                     policy="%s:%s" % (self.kind, self.match),
  310                     reason=err_reason)
  311             parent_foreign_key = _RESOURCE_FOREIGN_KEYS.get(
  312                 "%ss" % parent_res, None)
  313             if parent_res == const.EXT_PARENT_PREFIX:
  314                 for resource in const.EXT_PARENT_RESOURCE_MAPPING:
  315                     key = "%s_%s_id" % (const.EXT_PARENT_PREFIX, resource)
  316                     if key in target:
  317                         parent_foreign_key = key
  318                         parent_res = resource
  319                         break
  320             if not parent_foreign_key:
  321                 err_reason = (_("Unable to verify match:%(match)s as the "
  322                                 "parent resource: %(res)s was not found") %
  323                               {'match': self.match, 'res': parent_res})
  324                 LOG.error(err_reason)
  325                 raise exceptions.PolicyCheckError(
  326                     policy="%s:%s" % (self.kind, self.match),
  327                     reason=err_reason)
  328 
  329             target[self.target_field] = self._extract(
  330                 parent_res, target[parent_foreign_key], parent_field)
  331 
  332         match = self.match % target
  333         if self.kind in creds:
  334             return match == six.text_type(creds[self.kind])
  335         return False
  336 
  337 
  338 @policy.register('field')
  339 class FieldCheck(policy.Check):
  340     def __init__(self, kind, match):
  341         # Process the match
  342         resource, field_value = match.split(':', 1)
  343         field, value = field_value.split('=', 1)
  344 
  345         super(FieldCheck, self).__init__(kind, '%s:%s:%s' %
  346                                          (resource, field, value))
  347 
  348         # Value might need conversion - we need help from the attribute map
  349         try:
  350             attr = attributes.RESOURCES[resource][field]
  351             conv_func = attr['convert_to']
  352         except KeyError:
  353             conv_func = lambda x: x
  354 
  355         self.field = field
  356         self.resource = resource
  357         self.value = conv_func(value)
  358         self.regex = re.compile(value[1:]) if value.startswith('~') else None
  359 
  360     def __call__(self, target_dict, cred_dict, enforcer):
  361         target_value = self._get_target_value(target_dict)
  362         # target_value might be a boolean, explicitly compare with None
  363         if target_value is None:
  364             return False
  365         if self.regex:
  366             return bool(self.regex.match(target_value))
  367         return target_value == self.value
  368 
  369     def _get_target_value(self, target_dict):
  370         if self.field in target_dict:
  371             return target_dict[self.field]
  372         # NOTE(slaweq): In case that target field is "networks:shared" we need
  373         # to treat it in "special" way as it may be used for resources other
  374         # than network, e.g. for port or subnet
  375         target_value = None
  376         if self.resource == "networks" and self.field == constants.SHARED:
  377             target_network_id = target_dict.get("network_id")
  378             if not target_network_id:
  379                 LOG.debug("Unable to find network_id field in target: "
  380                           "%(target_dict)s",
  381                           {'field': self.field, 'target_dict': target_dict})
  382                 return
  383             project_id = target_dict.get('project_id')
  384             ctx = (context.Context(tenant_id=project_id) if project_id
  385                    else context.get_admin_context())
  386             plugin = directory.get_plugin()
  387             network = plugin.get_network(ctx, target_network_id)
  388             target_value = network.get(self.field)
  389         if target_value is None:
  390             LOG.debug("Unable to find requested field: %(field)s in target: "
  391                       "%(target_dict)s",
  392                       {'field': self.field, 'target_dict': target_dict})
  393         return target_value
  394 
  395 
  396 def _prepare_check(context, action, target, pluralized):
  397     """Prepare rule, target, and credentials for the policy engine."""
  398     # Compare with None to distinguish case in which target is {}
  399     if target is None:
  400         target = {}
  401     match_rule = _build_match_rule(action, target, pluralized)
  402     credentials = context.to_policy_values()
  403     return match_rule, target, credentials
  404 
  405 
  406 def log_rule_list(match_rule):
  407     if LOG.isEnabledFor(logging.DEBUG):
  408         rules = _process_rules_list([], match_rule)
  409         LOG.debug("Enforcing rules: %s", rules)
  410 
  411 
  412 def check(context, action, target, plugin=None, might_not_exist=False,
  413           pluralized=None):
  414     """Verifies that the action is valid on the target in this context.
  415 
  416     :param context: neutron context
  417     :param action: string representing the action to be checked
  418         this should be colon separated for clarity.
  419     :param target: dictionary representing the object of the action
  420         for object creation this should be a dictionary representing the
  421         location of the object e.g. ``{'project_id': context.project_id}``
  422     :param plugin: currently unused and deprecated.
  423         Kept for backward compatibility.
  424     :param might_not_exist: If True the policy check is skipped (and the
  425         function returns True) if the specified policy does not exist.
  426         Defaults to false.
  427     :param pluralized: pluralized case of resource
  428         e.g. firewall_policy -> pluralized = "firewall_policies"
  429 
  430     :return: Returns True if access is permitted else False.
  431     """
  432     # If we already know the context has admin rights do not perform an
  433     # additional check and authorize the operation
  434     if context.is_admin:
  435         return True
  436     if might_not_exist and not (_ENFORCER.rules and action in _ENFORCER.rules):
  437         return True
  438     match_rule, target, credentials = _prepare_check(context,
  439                                                      action,
  440                                                      target,
  441                                                      pluralized)
  442     result = _ENFORCER.enforce(match_rule,
  443                                target,
  444                                credentials,
  445                                pluralized=pluralized)
  446     return result
  447 
  448 
  449 def enforce(context, action, target, plugin=None, pluralized=None):
  450     """Verifies that the action is valid on the target in this context.
  451 
  452     :param context: neutron context
  453     :param action: string representing the action to be checked
  454         this should be colon separated for clarity.
  455     :param target: dictionary representing the object of the action
  456         for object creation this should be a dictionary representing the
  457         location of the object e.g. ``{'project_id': context.project_id}``
  458     :param plugin: currently unused and deprecated.
  459         Kept for backward compatibility.
  460     :param pluralized: pluralized case of resource
  461         e.g. firewall_policy -> pluralized = "firewall_policies"
  462 
  463     :raises oslo_policy.policy.PolicyNotAuthorized:
  464             if verification fails.
  465     """
  466     # If we already know the context has admin rights do not perform an
  467     # additional check and authorize the operation
  468     if context.is_admin:
  469         return True
  470     rule, target, credentials = _prepare_check(context,
  471                                                action,
  472                                                target,
  473                                                pluralized)
  474     try:
  475         result = _ENFORCER.enforce(rule, target, credentials, action=action,
  476                                    do_raise=True)
  477     except policy.PolicyNotAuthorized:
  478         with excutils.save_and_reraise_exception():
  479             log_rule_list(rule)
  480             LOG.debug("Failed policy check for '%s'", action)
  481     return result
  482 
  483 
  484 def get_enforcer():
  485     # NOTE(amotoki): This was borrowed from nova/policy.py.
  486     # This method is for use by oslo.policy CLI scripts. Those scripts need the
  487     # 'output-file' and 'namespace' options, but having those in sys.argv means
  488     # loading the neutron config options will fail as those are not expected to
  489     # be present. So we pass in an arg list with those stripped out.
  490     conf_args = []
  491     # Start at 1 because cfg.CONF expects the equivalent of sys.argv[1:]
  492     i = 1
  493     while i < len(sys.argv):
  494         if sys.argv[i].strip('-') in ['namespace', 'output-file']:
  495             i += 2
  496             continue
  497         conf_args.append(sys.argv[i])
  498         i += 1
  499 
  500     cfg.CONF(conf_args, project='neutron')
  501     init()
  502     return _ENFORCER