"Fossies" - the Fresh Open Source Software Archive

Member "keystone-17.0.0/keystone/common/rbac_enforcer/enforcer.py" (13 May 2020, 21673 Bytes) of package /linux/misc/openstack/keystone-17.0.0.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 "enforcer.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.1_vs_17.0.0.

    1 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    2 # not use this file except in compliance with the License. You may obtain
    3 # a copy of the License at
    4 #
    5 #      http://www.apache.org/licenses/LICENSE-2.0
    6 #
    7 # Unless required by applicable law or agreed to in writing, software
    8 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    9 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   10 # License for the specific language governing permissions and limitations
   11 # under the License.
   12 
   13 import functools
   14 
   15 import flask
   16 from oslo_log import log
   17 from oslo_policy import policy as common_policy
   18 from oslo_utils import strutils
   19 
   20 from keystone.common import authorization
   21 from keystone.common import context
   22 from keystone.common import policies
   23 from keystone.common import provider_api
   24 from keystone.common import utils
   25 import keystone.conf
   26 from keystone import exception
   27 from keystone.i18n import _
   28 
   29 
   30 CONF = keystone.conf.CONF
   31 LOG = log.getLogger(__name__)
   32 PROVIDER_APIS = provider_api.ProviderAPIs
   33 
   34 
   35 _POSSIBLE_TARGET_ACTIONS = frozenset([
   36     rule.name for
   37     rule in policies.list_rules() if not rule.deprecated_for_removal
   38 ])
   39 _ENFORCEMENT_CHECK_ATTR = 'keystone:RBAC:enforcement_called'
   40 
   41 
   42 class RBACEnforcer(object):
   43     """Enforce RBAC on API calls."""
   44 
   45     __shared_state__ = {}
   46     __ENFORCER = None
   47     ACTION_STORE_ATTR = 'keystone:RBAC:action_name'
   48     # FOR TESTS ONLY
   49     suppress_deprecation_warnings = False
   50 
   51     def __init__(self):
   52         # NOTE(morgan): All Enforcer Instances use the same shared state;
   53         # BORG pattern.
   54         self.__dict__ = self.__shared_state__
   55 
   56     def _check_deprecated_rule(self, action):
   57         def _name_is_changing(rule):
   58             deprecated_rule = rule.deprecated_rule
   59             return (deprecated_rule and
   60                     deprecated_rule.name != rule.name and
   61                     deprecated_rule.name in self._enforcer.file_rules)
   62 
   63         def _check_str_is_changing(rule):
   64             deprecated_rule = rule.deprecated_rule
   65             return (deprecated_rule and
   66                     deprecated_rule.check_str != rule.check_str and
   67                     rule.name not in self._enforcer.file_rules)
   68 
   69         def _is_deprecated_for_removal(rule):
   70             return (rule.deprecated_for_removal and
   71                     rule.name in self._enforcer.file_rules)
   72 
   73         def _emit_warning():
   74             if not self._enforcer._warning_emitted:
   75                 LOG.warning("Deprecated policy rules found. Use "
   76                             "oslopolicy-policy-generator and "
   77                             "oslopolicy-policy-upgrade to detect and resolve "
   78                             "deprecated policies in your configuration.")
   79                 self._enforcer._warning_emitted = True
   80 
   81         registered_rule = self._enforcer.registered_rules.get(action)
   82 
   83         if not registered_rule:
   84             return
   85         if (_name_is_changing(registered_rule) or
   86                 _check_str_is_changing(registered_rule) or
   87                 _is_deprecated_for_removal(registered_rule)):
   88             _emit_warning()
   89 
   90     def _enforce(self, credentials, action, target, do_raise=True):
   91         """Verify that the action is valid on the target in this context.
   92 
   93         This method is for cases that exceed the base enforcer
   94         functionality (notably for compatibility with `@protected` style
   95         decorators.
   96 
   97         :param credentials: user credentials
   98         :param action: string representing the action to be checked, which
   99                        should be colon separated for clarity.
  100         :param target: dictionary representing the object of the action for
  101                        object creation this should be a dictionary
  102                        representing the location of the object e.g.
  103                        {'project_id': object.project_id}
  104         :raises keystone.exception.Forbidden: If verification fails.
  105 
  106         Actions should be colon separated for clarity. For example:
  107 
  108         * identity:list_users
  109         """
  110         # Add the exception arguments if asked to do a raise
  111         extra = {}
  112         if do_raise:
  113             extra.update(exc=exception.ForbiddenAction, action=action,
  114                          do_raise=do_raise)
  115 
  116         try:
  117             result = self._enforcer.enforce(
  118                 rule=action, target=target, creds=credentials, **extra)
  119             self._check_deprecated_rule(action)
  120             return result
  121         except common_policy.InvalidScope:
  122             raise exception.ForbiddenAction(action=action)
  123 
  124     def _reset(self):
  125         # NOTE(morgan): Used for TEST purposes only.
  126         self.__ENFORCER = None
  127 
  128     @property
  129     def _enforcer(self):
  130         # The raw oslo-policy enforcer object
  131         if self.__ENFORCER is None:
  132             self.__ENFORCER = common_policy.Enforcer(CONF)
  133             # NOTE(cmurphy) when running in the keystone server, suppress
  134             # deprecation warnings for individual policy rules. Instead, we log
  135             # a single notification at enforcement time indicating the
  136             # oslo.policy tools the operator can use to detect and resolve
  137             # deprecated policies. If there is no request context here, that
  138             # means external tooling such as the oslo.policy tools are running
  139             # this code, in which case we do want the full deprecation warnings
  140             # emitted for individual polcy rules.
  141             if flask.has_request_context():
  142                 self.__ENFORCER.suppress_deprecation_warnings = True
  143             # NOTE(cmurphy) Tests may explicitly disable these warnings to
  144             # prevent an explosion of test logs
  145             if self.suppress_deprecation_warnings:
  146                 self.__ENFORCER.suppress_deprecation_warnings = True
  147             self.register_rules(self.__ENFORCER)
  148             self.__ENFORCER._warning_emitted = False
  149         return self.__ENFORCER
  150 
  151     @staticmethod
  152     def _extract_filter_values(filters):
  153         """Extract filter data from query params for RBAC enforcement."""
  154         filters = filters or []
  155         target = {i: flask.request.args[i] for
  156                   i in filters if i in flask.request.args}
  157         if target:
  158             if LOG.logger.getEffectiveLevel() <= log.DEBUG:
  159                 LOG.debug(
  160                     'RBAC: Adding query filter params (%s)',
  161                     ', '.join(['%s=%s' % (k, v) for k, v in target.items()]))
  162         return target
  163 
  164     @staticmethod
  165     def _extract_member_target_data(member_target_type, member_target):
  166         """Build some useful target data.
  167 
  168         :param member_target_type: what type of target, e.g. 'user'
  169         :type member_target_type: str or None
  170         :param member_target: reference of the target data
  171         :type member_target: dict or None
  172         :returns: constructed target dict or empty dict
  173         :rtype: dict
  174         """
  175         ret_dict = {}
  176         if ((member_target is not None and member_target_type is None) or
  177                 (member_target is None and member_target_type is not None)):
  178             LOG.warning('RBAC: Unknown target type or target reference. '
  179                         'Rejecting as unauthorized. '
  180                         '(member_target_type=%(target_type)r, '
  181                         'member_target=%(target_ref)r)',
  182                         {'target_type': member_target_type,
  183                          'target_ref': member_target})
  184             # Fast exit.
  185             return ret_dict
  186 
  187         if member_target is not None and member_target_type is not None:
  188             ret_dict['target'] = {member_target_type: member_target}
  189         else:
  190             # Try and do some magic loading based upon the resource we've
  191             # matched in our route. This is mostly so we can have a level of
  192             # automatic pulling in the resource; strictly for some added
  193             # DRY capabilities. In an ideal world the target is always passed
  194             # in explicitly.
  195             if flask.request.endpoint:
  196                 # This only works for cases of Flask-RESTful, or carefully
  197                 # crafted endpoints that live on a class. Ultimately, there
  198                 # should be more protection against something wonky
  199                 # here.
  200                 resource = flask.current_app.view_functions[
  201                     flask.request.endpoint].view_class
  202                 try:
  203                     member_name = getattr(resource, 'member_key', None)
  204                 except ValueError:
  205                     # NOTE(morgan): In the case that the ResourceBase keystone
  206                     # class is used, we raise a value error when member_key
  207                     # has not been set on the class. This is perfectly
  208                     # normal and acceptable. Set member_name to None as though
  209                     # it wasn't set.
  210                     member_name = None
  211                 func = getattr(
  212                     resource, 'get_member_from_driver', None)
  213                 if member_name is not None and callable(func):
  214                     key = '%s_id' % member_name
  215                     if key in (flask.request.view_args or {}):
  216                         # NOTE(morgan): For most correct setup, instantiate the
  217                         # view_class. There is no current support for passing
  218                         # extra args to the constructor of the view_class like
  219                         # .as_view() method would actually do. In this case
  220                         # perform a simple instantiation to represent the
  221                         # `self` pass to the unbound method.
  222                         #
  223                         # TODO(morgan): add (future) support for passing class
  224                         # instantiation args.
  225                         ret_dict['target'] = {
  226                             member_name: func(flask.request.view_args[key])
  227                         }
  228         return ret_dict
  229 
  230     @staticmethod
  231     def _extract_policy_check_credentials():
  232         # Pull out the auth context
  233         return flask.request.environ.get(authorization.AUTH_CONTEXT_ENV, {})
  234 
  235     @classmethod
  236     def _extract_subject_token_target_data(cls):
  237         ret_dict = {}
  238         window_seconds = 0
  239         # NOTE(morgan): Populate the subject token data into
  240         # the policy dict at "target.token". In all liklyhood
  241         # it is un-interesting to populate this data outside
  242         # of the auth paths.
  243         target = 'token'
  244         subject_token = flask.request.headers.get('X-Subject-Token')
  245         access_rules_support = flask.request.headers.get(
  246             authorization.ACCESS_RULES_HEADER)
  247         if subject_token is not None:
  248             allow_expired = (strutils.bool_from_string(
  249                 flask.request.args.get('allow_expired', False),
  250                 default=False))
  251             if allow_expired:
  252                 window_seconds = CONF.token.allow_expired_window
  253             token = PROVIDER_APIS.token_provider_api.validate_token(
  254                 subject_token,
  255                 window_seconds=window_seconds,
  256                 access_rules_support=access_rules_support
  257             )
  258             # TODO(morgan): Expand extracted data from the subject token.
  259             ret_dict[target] = {}
  260             ret_dict[target]['user_id'] = token.user_id
  261             try:
  262                 user_domain_id = token.user['domain_id']
  263             except exception.UnexpectedError:
  264                 user_domain_id = None
  265             if user_domain_id:
  266                 ret_dict[target].setdefault('user', {})
  267                 ret_dict[target]['user'].setdefault('domain', {})
  268                 ret_dict[target]['user']['domain']['id'] = user_domain_id
  269         return ret_dict
  270 
  271     @staticmethod
  272     def _get_oslo_req_context():
  273         return flask.request.environ.get(context.REQUEST_CONTEXT_ENV, None)
  274 
  275     @classmethod
  276     def _assert_is_authenticated(cls):
  277         ctx = cls._get_oslo_req_context()
  278         if ctx is None:
  279             LOG.warning('RBAC: Error reading the request context generated by '
  280                         'the Auth Middleware (there is no context). Rejecting '
  281                         'request as unauthorized.')
  282             raise exception.Unauthorized(
  283                 _('Internal error processing authentication and '
  284                   'authorization.'))
  285         if not ctx.authenticated:
  286             raise exception.Unauthorized(
  287                 _('auth_context did not decode anything useful'))
  288 
  289     @classmethod
  290     def _shared_admin_auth_token_set(cls):
  291         ctx = cls._get_oslo_req_context()
  292         return getattr(ctx, 'is_admin', False)
  293 
  294     @classmethod
  295     def enforce_call(cls, enforcer=None, action=None, target_attr=None,
  296                      member_target_type=None, member_target=None,
  297                      filters=None, build_target=None):
  298         """Enforce RBAC on the current request.
  299 
  300         This will do some legwork and then instantiate the Enforcer if an
  301         enforcer is not passed in.
  302 
  303         :param enforcer: A pre-instantiated Enforcer object (optional)
  304         :type enforcer: :class:`RBACEnforcer`
  305         :param action: the name of the rule/policy enforcement to be checked
  306                        against, e.g. `identity:get_user` (optional may be
  307                        replaced by decorating the method/function with
  308                        `policy_enforcer_action`.
  309         :type action: str
  310         :param target_attr: complete override of the target data. This will
  311                             replace all other generated target data meaning
  312                             `member_target_type` and `member_target` are
  313                             ignored. This will also prevent extraction of
  314                             data from the X-Subject-Token. The `target` dict
  315                             should contain a series of key-value pairs such
  316                             as `{'user': user_ref_dict}`.
  317         :type target_attr: dict
  318         :param member_target_type: the type of the target, e.g. 'user'. Both
  319                                    this and `member_target` must be passed if
  320                                    either is passed.
  321         :type member_target_type: str
  322         :param member_target: the (dict form) reference of the member object.
  323                               Both this and `member_target_type` must be passed
  324                               if either is passed.
  325         :type member_target: dict
  326         :param filters: A variable number of optional string filters, these are
  327                         used to extract values from the query params. The
  328                         filters are added to the request data that is passed to
  329                         the enforcer and may be used to determine policy
  330                         action. In practice these are mainly supplied in the
  331                         various "list" APIs and are un-used in the default
  332                         supplied policies.
  333         :type filters: iterable
  334         :param build_target: A function to build the target for enforcement.
  335                              This is explicitly done after authentication
  336                              in order to not leak existance data before
  337                              auth.
  338         :type build_target: function
  339         """
  340         # NOTE(morgan) everything in the policy_dict may be used by the policy
  341         # DSL to action on RBAC and request information/response data.
  342         policy_dict = {}
  343 
  344         # If "action" has not explicitly been overridden, see if it is set in
  345         # Flask.g app-context (per-request thread local) meaning the
  346         # @policy_enforcer_action decorator was used.
  347         action = action or getattr(flask.g, cls.ACTION_STORE_ATTR, None)
  348         if action not in _POSSIBLE_TARGET_ACTIONS:
  349             LOG.warning('RBAC: Unknown enforcement action name `%s`. '
  350                         'Rejecting as Forbidden, this is a programming error '
  351                         'and a bug should be filed with as much information '
  352                         'about the request that caused this as possible.',
  353                         action)
  354             # NOTE(morgan): While this is an internal error, a 500 is never
  355             # desirable, we have handled the case and the most appropriate
  356             # response here is to issue a 403 (FORBIDDEN) to any API calling
  357             # enforce_call with an inappropriate action/name to look up the
  358             # policy rule. This is simply a short-circuit as the enforcement
  359             # code raises a 403 on an unknown action (in keystone) by default.
  360             raise exception.Forbidden(
  361                 message=_(
  362                     'Internal RBAC enforcement error, invalid rule (action) '
  363                     'name.'))
  364 
  365         # Mark flask.g as "enforce_call" has been called. This should occur
  366         # before anything except the "is this a valid action" check, ensuring
  367         # all proper "after request" checks pass, showing that the API has
  368         # enforcement.
  369         setattr(flask.g, _ENFORCEMENT_CHECK_ATTR, True)
  370 
  371         # Assert we are actually authenticated
  372         cls._assert_is_authenticated()
  373 
  374         # Check if "is_admin", this is in support of the old "admin auth token"
  375         # middleware with a shared "admin" token for auth
  376         if cls._shared_admin_auth_token_set():
  377             LOG.warning('RBAC: Bypassing authorization')
  378             return
  379 
  380         # NOTE(morgan): !!! ORDER OF THESE OPERATIONS IS IMPORTANT !!!
  381         # The lowest priority values are set first and the highest priority
  382         # values are set last.
  383 
  384         # Populate the input attributes (view args) directly to the policy
  385         # dict. This is to allow the policy engine to have access to the
  386         # view args for substitution. This is to mirror the old @protected
  387         # mechanism and ensure current policy files continue to work as
  388         # expected.
  389         policy_dict.update(flask.request.view_args)
  390 
  391         # Get the Target Data Set.
  392         if target_attr is None and build_target is None:
  393             try:
  394                 policy_dict.update(cls._extract_member_target_data(
  395                     member_target_type, member_target))
  396             except exception.NotFound:
  397                 # DEBUG LOG and bubble up the 404 error. This is expected
  398                 # behavior. This likely should be specific in each API. This
  399                 # should be revisited in the future and each API should make
  400                 # the explicit "existence" checks before enforcement.
  401                 LOG.debug('Extracting inferred target data resulted in '
  402                           '"NOT FOUND (404)".')
  403                 raise
  404             except Exception as e:  # nosec
  405                 # NOTE(morgan): Errors should never bubble up at this point,
  406                 # if there is an error getting the target, log it and move
  407                 # on. Raise an explicit 403, we have failed policy checks.
  408                 LOG.warning('Unable to extract inferred target data during '
  409                             'enforcement')
  410                 LOG.debug(e, exc_info=True)
  411                 raise exception.ForbiddenAction(action=action)
  412 
  413             # Special Case, extract and add subject_token data.
  414             subj_token_target_data = cls._extract_subject_token_target_data()
  415             if subj_token_target_data:
  416                 policy_dict.setdefault('target', {}).update(
  417                     subj_token_target_data)
  418         else:
  419             if target_attr and build_target:
  420                 raise ValueError('Programming Error: A target_attr or '
  421                                  'build_target must be provided, but not both')
  422 
  423             policy_dict['target'] = target_attr or build_target()
  424 
  425         # Pull the data from the submitted json body to generate
  426         # appropriate input/target attributes, we take an explicit copy here
  427         # to ensure we're not somehow corrupting
  428         json_input = flask.request.get_json(force=True, silent=True) or {}
  429         policy_dict.update(json_input.copy())
  430 
  431         # Generate the filter_attr dataset.
  432         policy_dict.update(cls._extract_filter_values(filters))
  433 
  434         flattened = utils.flatten_dict(policy_dict)
  435         if LOG.logger.getEffectiveLevel() <= log.DEBUG:
  436             # LOG the Args
  437             args_str = ', '.join(
  438                 ['%s=%s' % (k, v) for
  439                  k, v in (flask.request.view_args or {}).items()])
  440             args_str = strutils.mask_password(args_str)
  441             LOG.debug('RBAC: Authorizing `%(action)s(%(args)s)`',
  442                       {'action': action, 'args': args_str})
  443 
  444         ctxt = cls._get_oslo_req_context()
  445         # Instantiate the enforcer object if needed.
  446         enforcer_obj = enforcer or cls()
  447         enforcer_obj._enforce(
  448             credentials=ctxt, action=action, target=flattened)
  449         LOG.debug('RBAC: Authorization granted')
  450 
  451     @classmethod
  452     def policy_enforcer_action(cls, action):
  453         """Decorator to set policy enforcement action name."""
  454         if action not in _POSSIBLE_TARGET_ACTIONS:
  455             raise ValueError('PROGRAMMING ERROR: Action must reference a '
  456                              'valid Keystone policy enforcement name.')
  457 
  458         def wrapper(f):
  459             @functools.wraps(f)
  460             def inner(*args, **kwargs):
  461                 # Set the action in g on a known attr so we can reference it
  462                 # later.
  463                 setattr(flask.g, cls.ACTION_STORE_ATTR, action)
  464                 return f(*args, **kwargs)
  465             return inner
  466         return wrapper
  467 
  468     @staticmethod
  469     def register_rules(enforcer):
  470         enforcer.register_defaults(policies.list_rules())