aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/auth/plugins/mapped.py
blob: 220ff013790f423bc29c44d23fb4db61cb1d499c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import functools

from oslo_log import log
from pycadf import cadftaxonomy as taxonomy
from six.moves.urllib import parse

from keystone import auth
from keystone.auth import plugins as auth_plugins
from keystone.common import dependency
from keystone.contrib.federation import constants as federation_constants
from keystone.contrib.federation import utils
from keystone import exception
from keystone.i18n import _
from keystone.models import token_model
from keystone import notifications


LOG = log.getLogger(__name__)

METHOD_NAME = 'mapped'


@dependency.requires('federation_api', 'identity_api',
                     'resource_api', 'token_provider_api')
class Mapped(auth.AuthMethodHandler):

    def _get_token_ref(self, auth_payload):
        token_id = auth_payload['id']
        response = self.token_provider_api.validate_token(token_id)
        return token_model.KeystoneToken(token_id=token_id,
                                         token_data=response)

    def authenticate(self, context, auth_payload, auth_context):
        """Authenticate mapped user and set an authentication context.

        :param context: keystone's request context
        :param auth_payload: the content of the authentication for a
                             given method
        :param auth_context: user authentication context, a dictionary
                             shared by all plugins.

        In addition to ``user_id`` in ``auth_context``, this plugin sets
        ``group_ids``, ``OS-FEDERATION:identity_provider`` and
        ``OS-FEDERATION:protocol``

        """

        if 'id' in auth_payload:
            token_ref = self._get_token_ref(auth_payload)
            handle_scoped_token(context, auth_payload, auth_context, token_ref,
                                self.federation_api,
                                self.identity_api,
                                self.token_provider_api)
        else:
            handle_unscoped_token(context, auth_payload, auth_context,
                                  self.resource_api, self.federation_api,
                                  self.identity_api)


def handle_scoped_token(context, auth_payload, auth_context, token_ref,
                        federation_api, identity_api, token_provider_api):
    utils.validate_expiration(token_ref)
    token_audit_id = token_ref.audit_id
    identity_provider = token_ref.federation_idp_id
    protocol = token_ref.federation_protocol_id
    user_id = token_ref.user_id
    group_ids = token_ref.federation_group_ids
    send_notification = functools.partial(
        notifications.send_saml_audit_notification, 'authenticate',
        context, user_id, group_ids, identity_provider, protocol,
        token_audit_id)

    utils.assert_enabled_identity_provider(federation_api, identity_provider)

    try:
        mapping = federation_api.get_mapping_from_idp_and_protocol(
            identity_provider, protocol)
        utils.validate_groups(group_ids, mapping['id'], identity_api)

    except Exception:
        # NOTE(topol): Diaper defense to catch any exception, so we can
        # send off failed authentication notification, raise the exception
        # after sending the notification
        send_notification(taxonomy.OUTCOME_FAILURE)
        raise
    else:
        send_notification(taxonomy.OUTCOME_SUCCESS)

    auth_context['user_id'] = user_id
    auth_context['group_ids'] = group_ids
    auth_context[federation_constants.IDENTITY_PROVIDER] = identity_provider
    auth_context[federation_constants.PROTOCOL] = protocol


def handle_unscoped_token(context, auth_payload, auth_context,
                          resource_api, federation_api, identity_api):

    def is_ephemeral_user(mapped_properties):
        return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL

    def build_ephemeral_user_context(auth_context, user, mapped_properties,
                                     identity_provider, protocol):
        auth_context['user_id'] = user['id']
        auth_context['group_ids'] = mapped_properties['group_ids']
        auth_context[federation_constants.IDENTITY_PROVIDER] = (
            identity_provider)
        auth_context[federation_constants.PROTOCOL] = protocol

    def build_local_user_context(auth_context, mapped_properties):
        user_info = auth_plugins.UserAuthInfo.create(mapped_properties,
                                                     METHOD_NAME)
        auth_context['user_id'] = user_info.user_id

    assertion = extract_assertion_data(context)
    identity_provider = auth_payload['identity_provider']
    protocol = auth_payload['protocol']

    utils.assert_enabled_identity_provider(federation_api, identity_provider)

    group_ids = None
    # NOTE(topol): The user is coming in from an IdP with a SAML assertion
    # instead of from a token, so we set token_id to None
    token_id = None
    # NOTE(marek-denis): This variable is set to None and there is a
    # possibility that it will be used in the CADF notification. This means
    # operation will not be mapped to any user (even ephemeral).
    user_id = None

    try:
        mapped_properties, mapping_id = apply_mapping_filter(
            identity_provider, protocol, assertion, resource_api,
            federation_api, identity_api)

        if is_ephemeral_user(mapped_properties):
            user = setup_username(context, mapped_properties)
            user_id = user['id']
            group_ids = mapped_properties['group_ids']
            utils.validate_groups_cardinality(group_ids, mapping_id)
            build_ephemeral_user_context(auth_context, user,
                                         mapped_properties,
                                         identity_provider, protocol)
        else:
            build_local_user_context(auth_context, mapped_properties)

    except Exception:
        # NOTE(topol): Diaper defense to catch any exception, so we can
        # send off failed authentication notification, raise the exception
        # after sending the notification
        outcome = taxonomy.OUTCOME_FAILURE
        notifications.send_saml_audit_notification('authenticate', context,
                                                   user_id, group_ids,
                                                   identity_provider,
                                                   protocol, token_id,
                                                   outcome)
        raise
    else:
        outcome = taxonomy.OUTCOME_SUCCESS
        notifications.send_saml_audit_notification('authenticate', context,
                                                   user_id, group_ids,
                                                   identity_provider,
                                                   protocol, token_id,
                                                   outcome)


def extract_assertion_data(context):
    assertion = dict(utils.get_assertion_params_from_env(context))
    return assertion


def apply_mapping_filter(identity_provider, protocol, assertion,
                         resource_api, federation_api, identity_api):
    idp = federation_api.get_idp(identity_provider)
    utils.validate_idp(idp, protocol, assertion)

    mapped_properties, mapping_id = federation_api.evaluate(
        identity_provider, protocol, assertion)

    # NOTE(marek-denis): We update group_ids only here to avoid fetching
    # groups identified by name/domain twice.
    # NOTE(marek-denis): Groups are translated from name/domain to their
    # corresponding ids in the auth plugin, as we need information what
    # ``mapping_id`` was used as well as idenity_api and resource_api
    # objects.
    group_ids = mapped_properties['group_ids']
    utils.validate_groups_in_backend(group_ids,
                                     mapping_id,
                                     identity_api)
    group_ids.extend(
        utils.transform_to_group_ids(
            mapped_properties['group_names'], mapping_id,
            identity_api, resource_api))
    mapped_properties['group_ids'] = list(set(group_ids))
    return mapped_properties, mapping_id


def setup_username(context, mapped_properties):
    """Setup federated username.

    Function covers all the cases for properly setting user id, a primary
    identifier for identity objects. Initial version of the mapping engine
    assumed user is identified by ``name`` and his ``id`` is built from the
    name. We, however need to be able to accept local rules that identify user
    by either id or name/domain.

    The following use-cases are covered:

    1) If neither user_name nor user_id is set raise exception.Unauthorized
    2) If user_id is set and user_name not, set user_name equal to user_id
    3) If user_id is not set and user_name is, set user_id as url safe version
       of user_name.

    :param context: authentication context
    :param mapped_properties: Properties issued by a RuleProcessor.
    :type: dictionary

    :raises: exception.Unauthorized
    :returns: dictionary with user identification
    :rtype: dict

    """
    user = mapped_properties['user']

    user_id = user.get('id')
    user_name = user.get('name') or context['environment'].get('REMOTE_USER')

    if not any([user_id, user_name]):
        msg = _("Could not map user while setting ephemeral user identity. "
                "Either mapping rules must specify user id/name or "
                "REMOTE_USER environment variable must be set.")
        raise exception.Unauthorized(msg)

    elif not user_name:
        user['name'] = user_id

    elif not user_id:
        user_id = user_name

    user['id'] = parse.quote(user_id)

    return user