"Fossies" - the Fresh Open Source Software Archive

Member "keystone-17.0.0/keystone/auth/plugins/mapped.py" (13 May 2020, 15318 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 "mapped.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 import uuid
   15 
   16 import flask
   17 from oslo_log import log
   18 from pycadf import cadftaxonomy as taxonomy
   19 from urllib import parse
   20 
   21 from keystone.auth import plugins as auth_plugins
   22 from keystone.auth.plugins import base
   23 from keystone.common import provider_api
   24 from keystone import exception
   25 from keystone.federation import constants as federation_constants
   26 from keystone.federation import utils
   27 from keystone.i18n import _
   28 from keystone import notifications
   29 
   30 LOG = log.getLogger(__name__)
   31 
   32 METHOD_NAME = 'mapped'
   33 PROVIDERS = provider_api.ProviderAPIs
   34 
   35 
   36 class Mapped(base.AuthMethodHandler):
   37 
   38     def _get_token_ref(self, auth_payload):
   39         token_id = auth_payload['id']
   40         return PROVIDERS.token_provider_api.validate_token(token_id)
   41 
   42     def authenticate(self, auth_payload):
   43         """Authenticate mapped user and set an authentication context.
   44 
   45         :param auth_payload: the content of the authentication for a
   46                              given method
   47 
   48         In addition to ``user_id`` in ``response_data``, this plugin sets
   49         ``group_ids``, ``OS-FEDERATION:identity_provider`` and
   50         ``OS-FEDERATION:protocol``
   51 
   52         """
   53         if 'id' in auth_payload:
   54             token_ref = self._get_token_ref(auth_payload)
   55             response_data = handle_scoped_token(token_ref,
   56                                                 PROVIDERS.federation_api,
   57                                                 PROVIDERS.identity_api)
   58         else:
   59             response_data = handle_unscoped_token(auth_payload,
   60                                                   PROVIDERS.resource_api,
   61                                                   PROVIDERS.federation_api,
   62                                                   PROVIDERS.identity_api,
   63                                                   PROVIDERS.assignment_api,
   64                                                   PROVIDERS.role_api)
   65 
   66         return base.AuthHandlerResponse(status=True, response_body=None,
   67                                         response_data=response_data)
   68 
   69 
   70 def handle_scoped_token(token, federation_api, identity_api):
   71     response_data = {}
   72     utils.validate_expiration(token)
   73     token_audit_id = token.audit_id
   74     identity_provider = token.identity_provider_id
   75     protocol = token.protocol_id
   76     user_id = token.user_id
   77     group_ids = []
   78     for group_dict in token.federated_groups:
   79         group_ids.append(group_dict['id'])
   80     send_notification = functools.partial(
   81         notifications.send_saml_audit_notification, 'authenticate',
   82         user_id, group_ids, identity_provider, protocol,
   83         token_audit_id)
   84 
   85     utils.assert_enabled_identity_provider(federation_api, identity_provider)
   86 
   87     try:
   88         mapping = federation_api.get_mapping_from_idp_and_protocol(
   89             identity_provider, protocol)
   90         utils.validate_mapped_group_ids(group_ids, mapping['id'], identity_api)
   91 
   92     except Exception:
   93         # NOTE(topol): Diaper defense to catch any exception, so we can
   94         # send off failed authentication notification, raise the exception
   95         # after sending the notification
   96         send_notification(taxonomy.OUTCOME_FAILURE)
   97         raise
   98     else:
   99         send_notification(taxonomy.OUTCOME_SUCCESS)
  100 
  101     response_data['user_id'] = user_id
  102     response_data['group_ids'] = group_ids
  103     response_data[federation_constants.IDENTITY_PROVIDER] = identity_provider
  104     response_data[federation_constants.PROTOCOL] = protocol
  105 
  106     return response_data
  107 
  108 
  109 def handle_unscoped_token(auth_payload, resource_api, federation_api,
  110                           identity_api, assignment_api, role_api):
  111 
  112     def validate_shadow_mapping(shadow_projects, existing_roles, idp_domain_id,
  113                                 idp_id):
  114         # Validate that the roles in the shadow mapping actually exist. If
  115         # they don't we should bail early before creating anything.
  116         for shadow_project in shadow_projects:
  117             for shadow_role in shadow_project['roles']:
  118                 # The role in the project mapping must exist in order for it to
  119                 # be useful.
  120                 if shadow_role['name'] not in existing_roles:
  121                     LOG.error(
  122                         'Role %s was specified in the mapping but does '
  123                         'not exist. All roles specified in a mapping must '
  124                         'exist before assignment.',
  125                         shadow_role['name']
  126                     )
  127                     # NOTE(lbragstad): The RoleNotFound exception usually
  128                     # expects a role_id as the parameter, but in this case we
  129                     # only have a name so we'll pass that instead.
  130                     raise exception.RoleNotFound(shadow_role['name'])
  131                 role = existing_roles[shadow_role['name']]
  132                 if (role['domain_id'] is not None and
  133                         role['domain_id'] != idp_domain_id):
  134                     LOG.error(
  135                         'Role %(role)s is a domain-specific role and '
  136                         'cannot be assigned within %(domain)s.',
  137                         {'role': shadow_role['name'], 'domain': idp_domain_id}
  138                     )
  139                     raise exception.DomainSpecificRoleNotWithinIdPDomain(
  140                         role_name=shadow_role['name'],
  141                         identity_provider=idp_id
  142                     )
  143 
  144     def create_projects_from_mapping(shadow_projects, idp_domain_id,
  145                                      existing_roles, user, assignment_api,
  146                                      resource_api):
  147         for shadow_project in shadow_projects:
  148             try:
  149                 # Check and see if the project already exists and if it
  150                 # does not, try to create it.
  151                 project = resource_api.get_project_by_name(
  152                     shadow_project['name'], idp_domain_id
  153                 )
  154             except exception.ProjectNotFound:
  155                 LOG.info(
  156                     'Project %(project_name)s does not exist. It will be '
  157                     'automatically provisioning for user %(user_id)s.',
  158                     {'project_name': shadow_project['name'],
  159                      'user_id': user['id']}
  160                 )
  161                 project_ref = {
  162                     'id': uuid.uuid4().hex,
  163                     'name': shadow_project['name'],
  164                     'domain_id': idp_domain_id
  165                 }
  166                 project = resource_api.create_project(
  167                     project_ref['id'],
  168                     project_ref
  169                 )
  170 
  171             shadow_roles = shadow_project['roles']
  172             for shadow_role in shadow_roles:
  173                 assignment_api.create_grant(
  174                     existing_roles[shadow_role['name']]['id'],
  175                     user_id=user['id'],
  176                     project_id=project['id']
  177                 )
  178 
  179     def is_ephemeral_user(mapped_properties):
  180         return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL
  181 
  182     def build_ephemeral_user_context(user, mapped_properties,
  183                                      identity_provider, protocol):
  184         resp = {}
  185         resp['user_id'] = user['id']
  186         resp['group_ids'] = mapped_properties['group_ids']
  187         resp[federation_constants.IDENTITY_PROVIDER] = identity_provider
  188         resp[federation_constants.PROTOCOL] = protocol
  189 
  190         return resp
  191 
  192     def build_local_user_context(mapped_properties):
  193         resp = {}
  194         user_info = auth_plugins.UserAuthInfo.create(mapped_properties,
  195                                                      METHOD_NAME)
  196         resp['user_id'] = user_info.user_id
  197 
  198         return resp
  199 
  200     assertion = extract_assertion_data()
  201     try:
  202         identity_provider = auth_payload['identity_provider']
  203     except KeyError:
  204         raise exception.ValidationError(
  205             attribute='identity_provider', target='mapped')
  206     try:
  207         protocol = auth_payload['protocol']
  208     except KeyError:
  209         raise exception.ValidationError(
  210             attribute='protocol', target='mapped')
  211 
  212     utils.assert_enabled_identity_provider(federation_api, identity_provider)
  213 
  214     group_ids = None
  215     # NOTE(topol): The user is coming in from an IdP with a SAML assertion
  216     # instead of from a token, so we set token_id to None
  217     token_id = None
  218     # NOTE(marek-denis): This variable is set to None and there is a
  219     # possibility that it will be used in the CADF notification. This means
  220     # operation will not be mapped to any user (even ephemeral).
  221     user_id = None
  222 
  223     try:
  224         try:
  225             mapped_properties, mapping_id = apply_mapping_filter(
  226                 identity_provider, protocol, assertion, resource_api,
  227                 federation_api, identity_api)
  228         except exception.ValidationError as e:
  229             # if mapping is either invalid or yield no valid identity,
  230             # it is considered a failed authentication
  231             raise exception.Unauthorized(e)
  232 
  233         if is_ephemeral_user(mapped_properties):
  234             unique_id, display_name = (
  235                 get_user_unique_id_and_display_name(mapped_properties)
  236             )
  237             email = mapped_properties['user'].get('email')
  238             user = identity_api.shadow_federated_user(
  239                 identity_provider,
  240                 protocol, unique_id,
  241                 display_name,
  242                 email,
  243                 group_ids=mapped_properties['group_ids'])
  244 
  245             if 'projects' in mapped_properties:
  246                 idp_domain_id = federation_api.get_idp(
  247                     identity_provider
  248                 )['domain_id']
  249                 existing_roles = {
  250                     role['name']: role for role in role_api.list_roles()
  251                 }
  252                 # NOTE(lbragstad): If we are dealing with a shadow mapping,
  253                 # then we need to make sure we validate all pieces of the
  254                 # mapping and what it's saying to create. If there is something
  255                 # wrong with how the mapping is, we should bail early before we
  256                 # create anything.
  257                 validate_shadow_mapping(
  258                     mapped_properties['projects'],
  259                     existing_roles,
  260                     idp_domain_id,
  261                     identity_provider
  262                 )
  263                 create_projects_from_mapping(
  264                     mapped_properties['projects'],
  265                     idp_domain_id,
  266                     existing_roles,
  267                     user,
  268                     assignment_api,
  269                     resource_api
  270                 )
  271 
  272             user_id = user['id']
  273             group_ids = mapped_properties['group_ids']
  274             response_data = build_ephemeral_user_context(
  275                 user, mapped_properties, identity_provider, protocol)
  276         else:
  277             response_data = build_local_user_context(mapped_properties)
  278 
  279     except Exception:
  280         # NOTE(topol): Diaper defense to catch any exception, so we can
  281         # send off failed authentication notification, raise the exception
  282         # after sending the notification
  283         outcome = taxonomy.OUTCOME_FAILURE
  284         notifications.send_saml_audit_notification('authenticate',
  285                                                    user_id, group_ids,
  286                                                    identity_provider,
  287                                                    protocol, token_id,
  288                                                    outcome)
  289         raise
  290     else:
  291         outcome = taxonomy.OUTCOME_SUCCESS
  292         notifications.send_saml_audit_notification('authenticate',
  293                                                    user_id, group_ids,
  294                                                    identity_provider,
  295                                                    protocol, token_id,
  296                                                    outcome)
  297 
  298     return response_data
  299 
  300 
  301 def extract_assertion_data():
  302     assertion = dict(utils.get_assertion_params_from_env())
  303     return assertion
  304 
  305 
  306 def apply_mapping_filter(identity_provider, protocol, assertion,
  307                          resource_api, federation_api, identity_api):
  308     idp = federation_api.get_idp(identity_provider)
  309     utils.validate_idp(idp, protocol, assertion)
  310 
  311     mapped_properties, mapping_id = federation_api.evaluate(
  312         identity_provider, protocol, assertion)
  313 
  314     # NOTE(marek-denis): We update group_ids only here to avoid fetching
  315     # groups identified by name/domain twice.
  316     # NOTE(marek-denis): Groups are translated from name/domain to their
  317     # corresponding ids in the auth plugin, as we need information what
  318     # ``mapping_id`` was used as well as idenity_api and resource_api
  319     # objects.
  320     group_ids = mapped_properties['group_ids']
  321     utils.validate_mapped_group_ids(group_ids, mapping_id, identity_api)
  322     group_ids.extend(
  323         utils.transform_to_group_ids(
  324             mapped_properties['group_names'], mapping_id,
  325             identity_api, resource_api))
  326     mapped_properties['group_ids'] = list(set(group_ids))
  327     return mapped_properties, mapping_id
  328 
  329 
  330 def get_user_unique_id_and_display_name(mapped_properties):
  331     """Setup federated username.
  332 
  333     Function covers all the cases for properly setting user id, a primary
  334     identifier for identity objects. Initial version of the mapping engine
  335     assumed user is identified by ``name`` and his ``id`` is built from the
  336     name. We, however need to be able to accept local rules that identify user
  337     by either id or name/domain.
  338 
  339     The following use-cases are covered:
  340 
  341     1) If neither user_name nor user_id is set raise exception.Unauthorized
  342     2) If user_id is set and user_name not, set user_name equal to user_id
  343     3) If user_id is not set and user_name is, set user_id as url safe version
  344        of user_name.
  345 
  346     :param mapped_properties: Properties issued by a RuleProcessor.
  347     :type: dictionary
  348 
  349     :raises keystone.exception.Unauthorized: If neither `user_name` nor
  350         `user_id` is set.
  351     :returns: tuple with user identification
  352     :rtype: tuple
  353 
  354     """
  355     user = mapped_properties['user']
  356 
  357     user_id = user.get('id')
  358     user_name = user.get('name') or flask.request.remote_user
  359 
  360     if not any([user_id, user_name]):
  361         msg = _("Could not map user while setting ephemeral user identity. "
  362                 "Either mapping rules must specify user id/name or "
  363                 "REMOTE_USER environment variable must be set.")
  364         raise exception.Unauthorized(msg)
  365 
  366     elif not user_name:
  367         user['name'] = user_id
  368 
  369     elif not user_id:
  370         user_id = user_name
  371 
  372     if user_name:
  373         user['name'] = user_name
  374     user['id'] = parse.quote(user_id)
  375     return (user['id'], user['name'])