"Fossies" - the Fresh Open Source Software Archive

Member "keystone-18.0.0/keystone/federation/utils.py" (14 Oct 2020, 34528 Bytes) of package /linux/misc/openstack/keystone-18.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 "utils.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 17.0.0_vs_18.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 """Utilities for Federation Extension."""
   14 
   15 import ast
   16 import re
   17 
   18 import flask
   19 import jsonschema
   20 from oslo_config import cfg
   21 from oslo_log import log
   22 from oslo_serialization import jsonutils
   23 from oslo_utils import timeutils
   24 
   25 from keystone.common import provider_api
   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 PROVIDERS = provider_api.ProviderAPIs
   34 
   35 
   36 class UserType(object):
   37     """User mapping type."""
   38 
   39     EPHEMERAL = 'ephemeral'
   40     LOCAL = 'local'
   41 
   42 
   43 ROLE_PROPERTIES = {
   44     "type": "array",
   45     "items": {
   46         "type": "object",
   47         "required": ["name"],
   48         "properties": {
   49             "name": {
   50                 "type": "string"
   51             },
   52             "additionalProperties": False
   53         }
   54     }
   55 }
   56 
   57 
   58 MAPPING_SCHEMA = {
   59     "type": "object",
   60     "required": ['rules'],
   61     "properties": {
   62         "rules": {
   63             "minItems": 1,
   64             "type": "array",
   65             "items": {
   66                 "type": "object",
   67                 "required": ['local', 'remote'],
   68                 "additionalProperties": False,
   69                 "properties": {
   70                     "local": {
   71                         "type": "array",
   72                         "items": {
   73                             "type": "object",
   74                             "additionalProperties": False,
   75                             "properties": {
   76                                 "user": {
   77                                     "type": "object",
   78                                     "properties": {
   79                                         "id": {"type": "string"},
   80                                         "name": {"type": "string"},
   81                                         "email": {"type": "string"},
   82                                         "domain": {
   83                                             "$ref": "#/definitions/domain"
   84                                         },
   85                                         "type": {
   86                                             "type": "string",
   87                                             "enum": [UserType.EPHEMERAL,
   88                                                      UserType.LOCAL]
   89                                         }
   90                                     },
   91                                     "additionalProperties": False
   92                                 },
   93                                 "projects": {
   94                                     "type": "array",
   95                                     "items": {
   96                                         "type": "object",
   97                                         "required": ["name", "roles"],
   98                                         "additionalProperties": False,
   99                                         "properties": {
  100                                             "name": {"type": "string"},
  101                                             "roles": ROLE_PROPERTIES
  102                                         }
  103                                     }
  104                                 },
  105                                 "group": {
  106                                     "type": "object",
  107                                     "oneOf": [
  108                                         {"$ref": "#/definitions/group_by_id"},
  109                                         {"$ref": "#/definitions/group_by_name"}
  110                                     ]
  111                                 },
  112                                 "groups": {
  113                                     "type": "string"
  114                                 },
  115                                 "group_ids": {
  116                                     "type": "string"
  117                                 },
  118                                 "domain": {"$ref": "#/definitions/domain"},
  119                             }
  120                         }
  121                     },
  122                     "remote": {
  123                         "minItems": 1,
  124                         "type": "array",
  125                         "items": {
  126                             "type": "object",
  127                             "oneOf": [
  128                                 {"$ref": "#/definitions/empty"},
  129                                 {"$ref": "#/definitions/any_one_of"},
  130                                 {"$ref": "#/definitions/not_any_of"},
  131                                 {"$ref": "#/definitions/blacklist"},
  132                                 {"$ref": "#/definitions/whitelist"}
  133                             ],
  134                         }
  135                     }
  136                 }
  137             }
  138         }
  139     },
  140     "definitions": {
  141         "empty": {
  142             "type": "object",
  143             "required": ['type'],
  144             "properties": {
  145                 "type": {
  146                     "type": "string"
  147                 },
  148             },
  149             "additionalProperties": False,
  150         },
  151         "any_one_of": {
  152             "type": "object",
  153             "additionalProperties": False,
  154             "required": ['type', 'any_one_of'],
  155             "properties": {
  156                 "type": {
  157                     "type": "string"
  158                 },
  159                 "any_one_of": {
  160                     "type": "array"
  161                 },
  162                 "regex": {
  163                     "type": "boolean"
  164                 }
  165             }
  166         },
  167         "not_any_of": {
  168             "type": "object",
  169             "additionalProperties": False,
  170             "required": ['type', 'not_any_of'],
  171             "properties": {
  172                 "type": {
  173                     "type": "string"
  174                 },
  175                 "not_any_of": {
  176                     "type": "array"
  177                 },
  178                 "regex": {
  179                     "type": "boolean"
  180                 }
  181             }
  182         },
  183         "blacklist": {
  184             "type": "object",
  185             "additionalProperties": False,
  186             "required": ['type', 'blacklist'],
  187             "properties": {
  188                 "type": {
  189                     "type": "string"
  190                 },
  191                 "blacklist": {
  192                     "type": "array"
  193                 },
  194                 "regex": {
  195                     "type": "boolean"
  196                 }
  197             }
  198         },
  199         "whitelist": {
  200             "type": "object",
  201             "additionalProperties": False,
  202             "required": ['type', 'whitelist'],
  203             "properties": {
  204                 "type": {
  205                     "type": "string"
  206                 },
  207                 "whitelist": {
  208                     "type": "array"
  209                 },
  210                 "regex": {
  211                     "type": "boolean"
  212                 }
  213             }
  214         },
  215         "domain": {
  216             "type": "object",
  217             "properties": {
  218                 "id": {"type": "string"},
  219                 "name": {"type": "string"}
  220             },
  221             "additionalProperties": False
  222         },
  223         "group_by_id": {
  224             "type": "object",
  225             "properties": {
  226                 "id": {"type": "string"}
  227             },
  228             "additionalProperties": False,
  229             "required": ["id"]
  230         },
  231         "group_by_name": {
  232             "type": "object",
  233             "properties": {
  234                 "name": {"type": "string"},
  235                 "domain": {"$ref": "#/definitions/domain"}
  236             },
  237             "additionalProperties": False,
  238             "required": ["name", "domain"]
  239         }
  240     }
  241 }
  242 
  243 
  244 class DirectMaps(object):
  245     """An abstraction around the remote matches.
  246 
  247     Each match is treated internally as a list.
  248     """
  249 
  250     def __init__(self):
  251         self._matches = []
  252 
  253     def __str__(self):
  254         """return the direct map array as a string."""
  255         return '%s' % self._matches
  256 
  257     def add(self, values):
  258         """Add a matched value to the list of matches.
  259 
  260         :param list value: the match to save
  261 
  262         """
  263         self._matches.append(values)
  264 
  265     def __getitem__(self, idx):
  266         """Used by Python when executing ``''.format(*DirectMaps())``."""
  267         value = self._matches[idx]
  268         if isinstance(value, list) and len(value) == 1:
  269             return value[0]
  270         else:
  271             return value
  272 
  273 
  274 def validate_mapping_structure(ref):
  275     v = jsonschema.Draft4Validator(MAPPING_SCHEMA)
  276 
  277     messages = ''
  278     for error in sorted(v.iter_errors(ref), key=str):
  279         messages = messages + error.message + "\n"
  280 
  281     if messages:
  282         raise exception.ValidationError(messages)
  283 
  284 
  285 def validate_expiration(token):
  286     token_expiration_datetime = timeutils.normalize_time(
  287         timeutils.parse_isotime(token.expires_at)
  288     )
  289     if timeutils.utcnow() > token_expiration_datetime:
  290         raise exception.Unauthorized(_('Federation token is expired'))
  291 
  292 
  293 def get_remote_id_parameter(idp, protocol):
  294     # NOTE(marco-fargetta): Since we support any protocol ID, we attempt to
  295     # retrieve the remote_id_attribute of the protocol ID. It will look up
  296     # first if the remote_id_attribute exists.
  297     protocol_ref = PROVIDERS.federation_api.get_protocol(idp['id'], protocol)
  298     remote_id_parameter = protocol_ref.get('remote_id_attribute')
  299     if remote_id_parameter:
  300         return remote_id_parameter
  301     else:
  302         # If it's not registered in the config, then register the option and
  303         # try again. This allows the user to register protocols other than
  304         # oidc and saml2.
  305         try:
  306             remote_id_parameter = CONF[protocol]['remote_id_attribute']
  307         except AttributeError:
  308             # TODO(dolph): Move configuration registration to keystone.conf
  309             CONF.register_opt(cfg.StrOpt('remote_id_attribute'),
  310                               group=protocol)
  311             try:
  312                 remote_id_parameter = CONF[protocol]['remote_id_attribute']
  313             except AttributeError:  # nosec
  314                 # No remote ID attr, will be logged and use the default
  315                 # instead.
  316                 pass
  317     if not remote_id_parameter:
  318         LOG.debug('Cannot find "remote_id_attribute" in configuration '
  319                   'group %s. Trying default location in '
  320                   'group federation.', protocol)
  321         remote_id_parameter = CONF.federation.remote_id_attribute
  322 
  323     return remote_id_parameter
  324 
  325 
  326 def validate_idp(idp, protocol, assertion):
  327     """The IdP providing the assertion should be registered for the mapping."""
  328     remote_id_parameter = get_remote_id_parameter(idp, protocol)
  329     if not remote_id_parameter or not idp['remote_ids']:
  330         LOG.debug('Impossible to identify the IdP %s ', idp['id'])
  331         # If nothing is defined, the administrator may want to
  332         # allow the mapping of every IdP
  333         return
  334     try:
  335         idp_remote_identifier = assertion[remote_id_parameter]
  336     except KeyError:
  337         msg = _('Could not find Identity Provider identifier in '
  338                 'environment')
  339         raise exception.ValidationError(msg)
  340     if idp_remote_identifier not in idp['remote_ids']:
  341         msg = _('Incoming identity provider identifier not included '
  342                 'among the accepted identifiers.')
  343         raise exception.Forbidden(msg)
  344 
  345 
  346 def validate_mapped_group_ids(group_ids, mapping_id, identity_api):
  347     """Iterate over group ids and make sure they are present in the backend.
  348 
  349     This call is not transactional.
  350     :param group_ids: IDs of the groups to be checked
  351     :type group_ids: list of str
  352 
  353     :param mapping_id: id of the mapping used for this operation
  354     :type mapping_id: str
  355 
  356     :param identity_api: Identity Manager object used for communication with
  357                          backend
  358     :type identity_api: identity.Manager
  359 
  360     :raises keystone.exception.MappedGroupNotFound: If the group returned by
  361         mapping was not found in the backend.
  362 
  363     """
  364     for group_id in group_ids:
  365         try:
  366             identity_api.get_group(group_id)
  367         except exception.GroupNotFound:
  368             raise exception.MappedGroupNotFound(
  369                 group_id=group_id, mapping_id=mapping_id)
  370 
  371 
  372 # TODO(marek-denis): Optimize this function, so the number of calls to the
  373 # backend are minimized.
  374 def transform_to_group_ids(group_names, mapping_id,
  375                            identity_api, resource_api):
  376     """Transform groups identified by name/domain to their ids.
  377 
  378     Function accepts list of groups identified by a name and domain giving
  379     a list of group ids in return. A message is logged if the group doesn't
  380     exist in the backend.
  381 
  382     Example of group_names parameter::
  383 
  384         [
  385             {
  386                 "name": "group_name",
  387                 "domain": {
  388                     "id": "domain_id"
  389                 },
  390             },
  391             {
  392                 "name": "group_name_2",
  393                 "domain": {
  394                     "name": "domain_name"
  395                 }
  396             }
  397         ]
  398 
  399     :param group_names: list of group identified by name and its domain.
  400     :type group_names: list
  401 
  402     :param mapping_id: id of the mapping used for mapping assertion into
  403         local credentials
  404     :type mapping_id: str
  405 
  406     :param identity_api: identity_api object
  407     :param resource_api: resource manager object
  408 
  409     :returns: generator object with group ids
  410 
  411     """
  412     def resolve_domain(domain):
  413         """Return domain id.
  414 
  415         Input is a dictionary with a domain identified either by a ``id`` or a
  416         ``name``. In the latter case system will attempt to fetch domain object
  417         from the backend.
  418 
  419         :returns: domain's id
  420         :rtype: str
  421 
  422         """
  423         domain_id = (domain.get('id') or
  424                      resource_api.get_domain_by_name(
  425                      domain.get('name')).get('id'))
  426         return domain_id
  427 
  428     for group in group_names:
  429         try:
  430             group_dict = identity_api.get_group_by_name(
  431                 group['name'], resolve_domain(group['domain']))
  432             yield group_dict['id']
  433         except exception.GroupNotFound:
  434             LOG.debug('Group %s has no entry in the backend',
  435                       group['name'])
  436 
  437 
  438 def get_assertion_params_from_env():
  439     LOG.debug('Environment variables: %s', flask.request.environ)
  440     prefix = CONF.federation.assertion_prefix
  441     for k, v in list(flask.request.environ.items()):
  442         if not k.startswith(prefix):
  443             continue
  444         # These bytes may be decodable as ISO-8859-1 according to Section
  445         # 3.2.4 of RFC 7230. Let's assume that our web server plugins are
  446         # correctly encoding the data.
  447         if not isinstance(v, str) and getattr(v, 'decode', False):
  448             v = v.decode('ISO-8859-1')
  449         yield (k, v)
  450 
  451 
  452 class RuleProcessor(object):
  453     """A class to process assertions and mapping rules."""
  454 
  455     class _EvalType(object):
  456         """Mapping rule evaluation types."""
  457 
  458         ANY_ONE_OF = 'any_one_of'
  459         NOT_ANY_OF = 'not_any_of'
  460         BLACKLIST = 'blacklist'
  461         WHITELIST = 'whitelist'
  462 
  463     def __init__(self, mapping_id, rules):
  464         """Initialize RuleProcessor.
  465 
  466         Example rules can be found at:
  467         :class:`keystone.tests.mapping_fixtures`
  468 
  469         :param mapping_id: id for the mapping
  470         :type mapping_id: string
  471         :param rules: rules from a mapping
  472         :type rules: dict
  473 
  474         """
  475         self.mapping_id = mapping_id
  476         self.rules = rules
  477 
  478     def process(self, assertion_data):
  479         """Transform assertion to a dictionary.
  480 
  481         The dictionary contains mapping of user name and group ids
  482         based on mapping rules.
  483 
  484         This function will iterate through the mapping rules to find
  485         assertions that are valid.
  486 
  487         :param assertion_data: an assertion containing values from an IdP
  488         :type assertion_data: dict
  489 
  490         Example assertion_data::
  491 
  492             {
  493                 'Email': 'testacct@example.com',
  494                 'UserName': 'testacct',
  495                 'FirstName': 'Test',
  496                 'LastName': 'Account',
  497                 'orgPersonType': 'Tester'
  498             }
  499 
  500         :returns: dictionary with user and group_ids
  501 
  502         The expected return structure is::
  503 
  504             {
  505                 'name': 'foobar',
  506                 'group_ids': ['abc123', 'def456'],
  507                 'group_names': [
  508                     {
  509                         'name': 'group_name_1',
  510                         'domain': {
  511                             'name': 'domain1'
  512                         }
  513                     },
  514                     {
  515                         'name': 'group_name_1_1',
  516                         'domain': {
  517                             'name': 'domain1'
  518                         }
  519                     },
  520                     {
  521                         'name': 'group_name_2',
  522                         'domain': {
  523                             'id': 'xyz132'
  524                         }
  525                     }
  526                 ]
  527             }
  528 
  529         """
  530         # Assertions will come in as string key-value pairs, and will use a
  531         # semi-colon to indicate multiple values, i.e. groups.
  532         # This will create a new dictionary where the values are arrays, and
  533         # any multiple values are stored in the arrays.
  534         LOG.debug('assertion data: %s', assertion_data)
  535         assertion = {n: v.split(';') for n, v in assertion_data.items()
  536                      if isinstance(v, str)}
  537         LOG.debug('assertion: %s', assertion)
  538         identity_values = []
  539 
  540         LOG.debug('rules: %s', self.rules)
  541         for rule in self.rules:
  542             direct_maps = self._verify_all_requirements(rule['remote'],
  543                                                         assertion)
  544 
  545             # If the compare comes back as None, then the rule did not apply
  546             # to the assertion data, go on to the next rule
  547             if direct_maps is None:
  548                 continue
  549 
  550             # If there are no direct mappings, then add the local mapping
  551             # directly to the array of saved values. However, if there is
  552             # a direct mapping, then perform variable replacement.
  553             if not direct_maps:
  554                 identity_values += rule['local']
  555             else:
  556                 for local in rule['local']:
  557                     new_local = self._update_local_mapping(local, direct_maps)
  558                     identity_values.append(new_local)
  559 
  560         LOG.debug('identity_values: %s', identity_values)
  561         mapped_properties = self._transform(identity_values)
  562         LOG.debug('mapped_properties: %s', mapped_properties)
  563         return mapped_properties
  564 
  565     def _normalize_groups(self, identity_value):
  566         # In this case, identity_value['groups'] is a string
  567         # representation of a list, and we want a real list.  This is
  568         # due to the way we do direct mapping substitutions today (see
  569         # function _update_local_mapping() )
  570         if 'name' in identity_value['groups']:
  571             try:
  572                 group_names_list = ast.literal_eval(
  573                     identity_value['groups'])
  574             except (ValueError, SyntaxError):
  575                 group_names_list = [identity_value['groups']]
  576 
  577             def convert_json(group):
  578                 if group.startswith('JSON:'):
  579                     return jsonutils.loads(group.lstrip('JSON:'))
  580                 return group
  581 
  582             group_dicts = [convert_json(g) for g in group_names_list]
  583             for g in group_dicts:
  584                 if 'domain' not in g:
  585                     msg = _("Invalid rule: %(identity_value)s. Both "
  586                             "'groups' and 'domain' keywords must be "
  587                             "specified.")
  588                     msg = msg % {'identity_value': identity_value}
  589                     raise exception.ValidationError(msg)
  590         else:
  591             if 'domain' not in identity_value:
  592                 msg = _("Invalid rule: %(identity_value)s. Both "
  593                         "'groups' and 'domain' keywords must be "
  594                         "specified.")
  595                 msg = msg % {'identity_value': identity_value}
  596                 raise exception.ValidationError(msg)
  597             try:
  598                 group_names_list = ast.literal_eval(
  599                     identity_value['groups'])
  600             except (ValueError, SyntaxError):
  601                 group_names_list = [identity_value['groups']]
  602             domain = identity_value['domain']
  603             group_dicts = [{'name': name, 'domain': domain} for name in
  604                            group_names_list]
  605         return group_dicts
  606 
  607     def _transform(self, identity_values):
  608         """Transform local mappings, to an easier to understand format.
  609 
  610         Transform the incoming array to generate the return value for
  611         the process function. Generating content for Keystone tokens will
  612         be easier if some pre-processing is done at this level.
  613 
  614         :param identity_values: local mapping from valid evaluations
  615         :type identity_values: array of dict
  616 
  617         Example identity_values::
  618 
  619             [
  620                 {
  621                     'group': {'id': '0cd5e9'},
  622                     'user': {
  623                         'email': 'bob@example.com'
  624                     },
  625                 },
  626                 {
  627                     'groups': ['member', 'admin', tester'],
  628                     'domain': {
  629                         'name': 'default_domain'
  630                     }
  631                 },
  632                 {
  633                     'group_ids': ['abc123', 'def456', '0cd5e9']
  634                 }
  635             ]
  636 
  637         :returns: dictionary with user name, group_ids and group_names.
  638         :rtype: dict
  639 
  640         """
  641         def extract_groups(groups_by_domain):
  642             for groups in list(groups_by_domain.values()):
  643                 for group in list({g['name']: g for g in groups}.values()):
  644                     yield group
  645 
  646         def normalize_user(user):
  647             """Parse and validate user mapping."""
  648             user_type = user.get('type')
  649 
  650             if user_type and user_type not in (UserType.EPHEMERAL,
  651                                                UserType.LOCAL):
  652                 msg = _("User type %s not supported") % user_type
  653                 raise exception.ValidationError(msg)
  654 
  655             if user_type is None:
  656                 user['type'] = UserType.EPHEMERAL
  657 
  658         # initialize the group_ids as a set to eliminate duplicates
  659         user = {}
  660         group_ids = set()
  661         group_names = list()
  662         groups_by_domain = dict()
  663         projects = []
  664 
  665         # if mapping yield no valid identity values, we should bail right away
  666         # instead of continuing on with a normalized bogus user
  667         if not identity_values:
  668             msg = ("Could not map any federated user properties to identity "
  669                    "values. Check debug logs or the mapping used for "
  670                    "additional details.")
  671             tr_msg = _("Could not map any federated user properties to "
  672                        "identity values. Check debug logs or the mapping "
  673                        "used for additional details.")
  674             LOG.warning(msg)
  675             raise exception.ValidationError(tr_msg)
  676 
  677         for identity_value in identity_values:
  678             if 'user' in identity_value:
  679                 # if a mapping outputs more than one user name, log it
  680                 if user:
  681                     LOG.warning('Ignoring user name')
  682                 else:
  683                     user = identity_value.get('user')
  684             if 'group' in identity_value:
  685                 group = identity_value['group']
  686                 if 'id' in group:
  687                     group_ids.add(group['id'])
  688                 elif 'name' in group:
  689                     domain = (group['domain'].get('name') or
  690                               group['domain'].get('id'))
  691                     groups_by_domain.setdefault(domain, list()).append(group)
  692                 group_names.extend(extract_groups(groups_by_domain))
  693             if 'groups' in identity_value:
  694                 group_dicts = self._normalize_groups(identity_value)
  695                 group_names.extend(group_dicts)
  696             if 'group_ids' in identity_value:
  697                 # If identity_values['group_ids'] is a string representation
  698                 # of a list, parse it to a real list. Also, if the provided
  699                 # group_ids parameter contains only one element, it will be
  700                 # parsed as a simple string, and not a list or the
  701                 # representation of a list.
  702                 try:
  703                     group_ids.update(
  704                         ast.literal_eval(identity_value['group_ids']))
  705                 except (ValueError, SyntaxError):
  706                     group_ids.update([identity_value['group_ids']])
  707             if 'projects' in identity_value:
  708                 projects = identity_value['projects']
  709 
  710         normalize_user(user)
  711 
  712         return {'user': user,
  713                 'group_ids': list(group_ids),
  714                 'group_names': group_names,
  715                 'projects': projects}
  716 
  717     def _update_local_mapping(self, local, direct_maps):
  718         """Replace any {0}, {1} ... values with data from the assertion.
  719 
  720         :param local: local mapping reference that needs to be updated
  721         :type local: dict
  722         :param direct_maps: identity values used to update local
  723         :type direct_maps: keystone.federation.utils.DirectMaps
  724 
  725         Example local::
  726 
  727             {'user': {'name': '{0} {1}', 'email': '{2}'}}
  728 
  729         Example direct_maps::
  730 
  731             [['Bob'], ['Thompson'], ['bob@example.com']]
  732 
  733         :returns: new local mapping reference with replaced values.
  734 
  735         The expected return structure is::
  736 
  737             {'user': {'name': 'Bob Thompson', 'email': 'bob@example.org'}}
  738 
  739         :raises keystone.exception.DirectMappingError: when referring to a
  740             remote match from a local section of a rule
  741 
  742         """
  743         LOG.debug('direct_maps: %s', direct_maps)
  744         LOG.debug('local: %s', local)
  745         new = {}
  746         for k, v in local.items():
  747             if isinstance(v, dict):
  748                 new_value = self._update_local_mapping(v, direct_maps)
  749             elif isinstance(v, list):
  750                 new_value = [self._update_local_mapping(item, direct_maps)
  751                              for item in v]
  752             else:
  753                 try:
  754                     new_value = v.format(*direct_maps)
  755                 except IndexError:
  756                     raise exception.DirectMappingError(
  757                         mapping_id=self.mapping_id)
  758 
  759             new[k] = new_value
  760         return new
  761 
  762     def _verify_all_requirements(self, requirements, assertion):
  763         """Compare remote requirements of a rule against the assertion.
  764 
  765         If a value of ``None`` is returned, the rule with this assertion
  766         doesn't apply.
  767         If an array of zero length is returned, then there are no direct
  768         mappings to be performed, but the rule is valid.
  769         Otherwise, then it will first attempt to filter the values according
  770         to blacklist or whitelist rules and finally return the values in
  771         order, to be directly mapped.
  772 
  773         :param requirements: list of remote requirements from rules
  774         :type requirements: list
  775 
  776         Example requirements::
  777 
  778             [
  779                 {
  780                     "type": "UserName"
  781                 },
  782                 {
  783                     "type": "orgPersonType",
  784                     "any_one_of": [
  785                         "Customer"
  786                     ]
  787                 },
  788                 {
  789                     "type": "ADFS_GROUPS",
  790                     "whitelist": [
  791                         "g1", "g2", "g3", "g4"
  792                     ]
  793                 }
  794             ]
  795 
  796         :param assertion: dict of attributes from an IdP
  797         :type assertion: dict
  798 
  799         Example assertion::
  800 
  801             {
  802                 'UserName': ['testacct'],
  803                 'LastName': ['Account'],
  804                 'orgPersonType': ['Tester'],
  805                 'Email': ['testacct@example.com'],
  806                 'FirstName': ['Test'],
  807                 'ADFS_GROUPS': ['g1', 'g2']
  808             }
  809 
  810         :returns: identity values used to update local
  811         :rtype: keystone.federation.utils.DirectMaps or None
  812 
  813         """
  814         direct_maps = DirectMaps()
  815 
  816         for requirement in requirements:
  817             requirement_type = requirement['type']
  818             direct_map_values = assertion.get(requirement_type)
  819             regex = requirement.get('regex', False)
  820 
  821             if not direct_map_values:
  822                 return None
  823 
  824             any_one_values = requirement.get(self._EvalType.ANY_ONE_OF)
  825             if any_one_values is not None:
  826                 if self._evaluate_requirement(any_one_values,
  827                                               direct_map_values,
  828                                               self._EvalType.ANY_ONE_OF,
  829                                               regex):
  830                     continue
  831                 else:
  832                     return None
  833 
  834             not_any_values = requirement.get(self._EvalType.NOT_ANY_OF)
  835             if not_any_values is not None:
  836                 if self._evaluate_requirement(not_any_values,
  837                                               direct_map_values,
  838                                               self._EvalType.NOT_ANY_OF,
  839                                               regex):
  840                     continue
  841                 else:
  842                     return None
  843 
  844             # If 'any_one_of' or 'not_any_of' are not found, then values are
  845             # within 'type'. Attempt to find that 'type' within the assertion,
  846             # and filter these values if 'whitelist' or 'blacklist' is set.
  847             blacklisted_values = requirement.get(self._EvalType.BLACKLIST)
  848             whitelisted_values = requirement.get(self._EvalType.WHITELIST)
  849 
  850             # If a blacklist or whitelist is used, we want to map to the
  851             # whole list instead of just its values separately.
  852             if blacklisted_values is not None:
  853                 direct_map_values = (
  854                     self._evaluate_requirement(blacklisted_values,
  855                                                direct_map_values,
  856                                                self._EvalType.BLACKLIST,
  857                                                regex))
  858             elif whitelisted_values is not None:
  859                 direct_map_values = (
  860                     self._evaluate_requirement(whitelisted_values,
  861                                                direct_map_values,
  862                                                self._EvalType.WHITELIST,
  863                                                regex))
  864 
  865             direct_maps.add(direct_map_values)
  866 
  867             LOG.debug('updating a direct mapping: %s', direct_map_values)
  868 
  869         return direct_maps
  870 
  871     def _evaluate_values_by_regex(self, values, assertion_values):
  872         return [
  873             assertion for assertion in assertion_values
  874             if any([re.search(regex, assertion) for regex in values])
  875         ]
  876 
  877     def _evaluate_requirement(self, values, assertion_values,
  878                               eval_type, regex):
  879         """Evaluate the incoming requirement and assertion.
  880 
  881         Filter the incoming assertions against the requirement values. If regex
  882         is specified, the assertion list is filtered by checking if any of the
  883         requirement regexes matches. Otherwise, the list is filtered by string
  884         equality with any of the allowed values.
  885 
  886         Once the assertion values are filtered, the output is determined by the
  887         evaluation type:
  888             any_one_of: return True if there are any matches, False otherwise
  889             not_any_of: return True if there are no matches, False otherwise
  890             blacklist: return the incoming values minus any matches
  891             whitelist: return only the matched values
  892 
  893         :param values: list of allowed values, defined in the requirement
  894         :type values: list
  895         :param assertion_values: The values from the assertion to evaluate
  896         :type assertion_values: list/string
  897         :param eval_type: determine how to evaluate requirements
  898         :type eval_type: string
  899         :param regex: perform evaluation with regex
  900         :type regex: boolean
  901 
  902         :returns: list of filtered assertion values (if evaluation type is
  903                   'blacklist' or 'whitelist'), or boolean indicating if the
  904                   assertion values fulfill the requirement (if evaluation type
  905                   is 'any_one_of' or 'not_any_of')
  906 
  907         """
  908         if regex:
  909             matches = self._evaluate_values_by_regex(values, assertion_values)
  910         else:
  911             matches = set(values).intersection(set(assertion_values))
  912 
  913         if eval_type == self._EvalType.ANY_ONE_OF:
  914             return bool(matches)
  915         elif eval_type == self._EvalType.NOT_ANY_OF:
  916             return not bool(matches)
  917         elif eval_type == self._EvalType.BLACKLIST:
  918             return list(set(assertion_values).difference(set(matches)))
  919         elif eval_type == self._EvalType.WHITELIST:
  920             return list(matches)
  921         else:
  922             raise exception.UnexpectedError(
  923                 _('Unexpected evaluation type "%(eval_type)s"') % {
  924                     'eval_type': eval_type})
  925 
  926 
  927 def assert_enabled_identity_provider(federation_api, idp_id):
  928     identity_provider = federation_api.get_idp(idp_id)
  929     if identity_provider.get('enabled') is not True:
  930         msg = 'Identity Provider %(idp)s is disabled' % {
  931             'idp': idp_id}
  932         tr_msg = _('Identity Provider %(idp)s is disabled') % {
  933             'idp': idp_id}
  934         LOG.debug(msg)
  935         raise exception.Forbidden(tr_msg)
  936 
  937 
  938 def assert_enabled_service_provider_object(service_provider):
  939     if service_provider.get('enabled') is not True:
  940         sp_id = service_provider['id']
  941         msg = 'Service Provider %(sp)s is disabled' % {'sp': sp_id}
  942         tr_msg = _('Service Provider %(sp)s is disabled') % {'sp': sp_id}
  943         LOG.debug(msg)
  944         raise exception.Forbidden(tr_msg)