"Fossies" - the Fresh Open Source Software Archive

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