5
from keystone.common import cms
6
from keystone.common import controller
7
from keystone.common import dependency
8
from keystone.common import logging
9
from keystone.common import utils
10
from keystone import config
11
from keystone import exception
12
from keystone.openstack.common import timeutils
13
from keystone.token import core
16
LOG = logging.getLogger(__name__)
17
DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
20
class ExternalAuthNotApplicable(Exception):
21
"""External authentication is not applicable"""
25
@dependency.requires('catalog_api', 'trust_api', 'token_api')
26
class Auth(controller.V2Controller):
27
def ca_cert(self, context, auth=None):
28
ca_file = open(CONF.signing.ca_certs, 'r')
33
def signing_cert(self, context, auth=None):
34
cert_file = open(CONF.signing.certfile, 'r')
35
data = cert_file.read()
39
def authenticate(self, context, auth=None):
40
"""Authenticate credentials and return a token.
42
Accept auth as a dict that looks like::
46
"passwordCredentials":{
47
"username":"test_user",
50
"tenantName":"customer-x"
54
In this case, tenant is optional, if not provided the token will be
55
considered "unscoped" and can later be used to get a scoped token.
57
Alternatively, this call accepts auth with only a token and tenant
58
that will return a token that is scoped to that tenant.
62
raise exception.ValidationError(attribute='auth',
63
target='request body')
65
auth_token_data = None
68
# Try to authenticate using a token
69
auth_info = self._authenticate_token(
72
# Try external authentication
74
auth_info = self._authenticate_external(
76
except ExternalAuthNotApplicable:
77
# Try local authentication
78
auth_info = self._authenticate_local(
81
user_ref, tenant_ref, metadata_ref, expiry = auth_info
82
core.validate_auth_info(self, context, user_ref, tenant_ref)
83
trust_id = metadata_ref.get('trust_id')
84
user_ref = self._filter_domain_id(user_ref)
86
tenant_ref = self._filter_domain_id(tenant_ref)
87
auth_token_data = self._get_auth_token_data(user_ref,
93
catalog_ref = self.catalog_api.get_catalog(
95
user_id=user_ref['id'],
96
tenant_id=tenant_ref['id'],
97
metadata=metadata_ref)
101
auth_token_data['id'] = 'placeholder'
104
for role_id in metadata_ref.get('roles', []):
105
role_ref = self.identity_api.get_role(context, role_id)
106
roles_ref.append(dict(name=role_ref['name']))
108
token_data = Auth.format_token(auth_token_data, roles_ref)
110
service_catalog = Auth.format_catalog(catalog_ref)
111
token_data['access']['serviceCatalog'] = service_catalog
113
if CONF.signing.token_format == 'UUID':
114
token_id = uuid.uuid4().hex
115
elif CONF.signing.token_format == 'PKI':
117
token_id = cms.cms_sign_token(json.dumps(token_data),
118
CONF.signing.certfile,
119
CONF.signing.keyfile)
120
except subprocess.CalledProcessError:
121
raise exception.UnexpectedError(_(
122
'Unable to sign token.'))
124
raise exception.UnexpectedError(_(
125
'Invalid value for token_format: %s.'
126
' Allowed values are PKI or UUID.') %
127
CONF.signing.token_format)
129
self.token_api.create_token(
130
context, token_id, dict(key=token_id,
132
expires=auth_token_data['expires'],
135
metadata=metadata_ref,
137
except Exception as e:
138
# an identical token may have been created already.
139
# if so, return the token_data as it is also identical
141
self.token_api.get_token(context=context,
143
except exception.TokenNotFound:
146
token_data['access']['token']['id'] = token_id
150
def _authenticate_token(self, context, auth):
151
"""Try to authenticate using an already existing token.
153
Returns auth_token_data, (user_ref, tenant_ref, metadata_ref)
155
if 'token' not in auth:
156
raise exception.ValidationError(
157
attribute='token', target='auth')
159
if "id" not in auth['token']:
160
raise exception.ValidationError(
161
attribute="id", target="token")
163
old_token = auth['token']['id']
164
if len(old_token) > CONF.max_token_size:
165
raise exception.ValidationSizeError(attribute='token',
166
size=CONF.max_token_size)
169
old_token_ref = self.token_api.get_token(context=context,
171
except exception.NotFound as e:
172
raise exception.Unauthorized(e)
174
#A trust token cannot be used to get another token
175
if 'trust' in old_token_ref:
176
raise exception.Forbidden()
177
if 'trust_id' in old_token_ref['metadata']:
178
raise exception.Forbidden()
180
user_ref = old_token_ref['user']
181
user_id = user_ref['id']
182
if not CONF.trust.enabled and 'trust_id' in auth:
183
raise exception.Forbidden('Trusts are disabled.')
184
elif CONF.trust.enabled and 'trust_id' in auth:
185
trust_ref = self.trust_api.get_trust(context, auth['trust_id'])
186
if trust_ref is None:
187
raise exception.Forbidden()
188
if user_id != trust_ref['trustee_user_id']:
189
raise exception.Forbidden()
190
if ('expires' in trust_ref) and (trust_ref['expires']):
191
expiry = trust_ref['expires']
192
if expiry < timeutils.parse_isotime(timeutils.isotime()):
193
raise exception.Forbidden()()
194
user_id = trust_ref['trustor_user_id']
195
trustor_user_ref = (self.identity_api.get_user(
197
user_id=trust_ref['trustor_user_id']))
198
if not trustor_user_ref['enabled']:
199
raise exception.Forbidden()()
200
trustee_user_ref = self.identity_api.get_user(
201
context, trust_ref['trustee_user_id'])
202
if not trustee_user_ref['enabled']:
203
raise exception.Forbidden()()
204
if trust_ref['impersonation'] == 'True':
205
current_user_ref = trustor_user_ref
207
current_user_ref = trustee_user_ref
210
current_user_ref = self.identity_api.get_user(context=context,
213
tenant_id = self._get_project_id_from_auth(context, auth)
215
tenant_ref = self._get_project_ref(context, user_id, tenant_id)
216
metadata_ref = self._get_metadata_ref(context, user_id, tenant_id)
218
# TODO (henry-nash) If no tenant was specified, instead check
219
# for a domain and find any related user/group roles
221
self._append_roles(metadata_ref,
222
self._get_group_metadata_ref(
223
context, user_id, tenant_id))
225
expiry = old_token_ref['expires']
226
if CONF.trust.enabled and 'trust_id' in auth:
227
trust_id = auth['trust_id']
229
for role in trust_ref['roles']:
230
if not 'roles' in metadata_ref:
231
raise exception.Forbidden()()
232
if role['id'] in metadata_ref['roles']:
233
trust_roles.append(role['id'])
235
raise exception.Forbidden()
236
if 'expiry' in trust_ref and trust_ref['expiry']:
237
trust_expiry = timeutils.parse_isotime(trust_ref['expiry'])
238
if trust_expiry < expiry:
239
expiry = trust_expiry
240
metadata_ref['roles'] = trust_roles
241
metadata_ref['trustee_user_id'] = trust_ref['trustee_user_id']
242
metadata_ref['trust_id'] = trust_id
244
auth_token_data = self._get_auth_token_data(current_user_ref,
248
return (current_user_ref, tenant_ref, metadata_ref, expiry)
250
def _authenticate_local(self, context, auth):
251
"""Try to authenticate against the identity backend.
253
Returns auth_token_data, (user_ref, tenant_ref, metadata_ref)
255
if 'passwordCredentials' not in auth:
256
raise exception.ValidationError(
257
attribute='passwordCredentials', target='auth')
259
if "password" not in auth['passwordCredentials']:
260
raise exception.ValidationError(
261
attribute='password', target='passwordCredentials')
263
password = auth['passwordCredentials']['password']
264
max_pw_size = utils.MAX_PASSWORD_LENGTH
265
if password and len(password) > max_pw_size:
266
raise exception.ValidationSizeError(attribute='password',
269
if ("userId" not in auth['passwordCredentials'] and
270
"username" not in auth['passwordCredentials']):
271
raise exception.ValidationError(
272
attribute='username or userId',
273
target='passwordCredentials')
275
user_id = auth['passwordCredentials'].get('userId', None)
276
if user_id and len(user_id) > CONF.max_param_size:
277
raise exception.ValidationSizeError(attribute='userId',
278
size=CONF.max_param_size)
280
username = auth['passwordCredentials'].get('username', '')
281
if len(username) > CONF.max_param_size:
282
raise exception.ValidationSizeError(attribute='username',
283
size=CONF.max_param_size)
287
user_ref = self.identity_api.get_user_by_name(
288
context=context, user_name=username,
289
domain_id=DEFAULT_DOMAIN_ID)
290
user_id = user_ref['id']
291
except exception.UserNotFound as e:
292
raise exception.Unauthorized(e)
294
tenant_id = self._get_project_id_from_auth(context, auth)
297
auth_info = self.identity_api.authenticate(
302
except AssertionError as e:
303
raise exception.Unauthorized(e)
304
(user_ref, tenant_ref, metadata_ref) = auth_info
306
# By now we will have authorized and if a tenant/project was
307
# specified, we will have obtained its metadata. In this case
308
# we just need to add in any group roles.
310
# TODO (henry-nash) If no tenant was specified, instead check
311
# for a domain and find any related user/group roles
313
self._append_roles(metadata_ref,
314
self._get_group_metadata_ref(
315
context, user_id, tenant_id))
317
expiry = core.default_expire_time()
318
return (user_ref, tenant_ref, metadata_ref, expiry)
320
def _authenticate_external(self, context, auth):
321
"""Try to authenticate an external user via REMOTE_USER variable.
323
Returns auth_token_data, (user_ref, tenant_ref, metadata_ref)
325
if 'REMOTE_USER' not in context:
326
raise ExternalAuthNotApplicable()
328
username = context['REMOTE_USER']
330
user_ref = self.identity_api.get_user_by_name(
331
context=context, user_name=username,
332
domain_id=DEFAULT_DOMAIN_ID)
333
user_id = user_ref['id']
334
except exception.UserNotFound as e:
335
raise exception.Unauthorized(e)
337
tenant_id = self._get_project_id_from_auth(context, auth)
339
tenant_ref = self._get_project_ref(context, user_id, tenant_id)
340
metadata_ref = self._get_metadata_ref(context, user_id, tenant_id)
342
# TODO (henry-nash) If no tenant was specified, instead check
343
# for a domain and find any related user/group roles
345
self._append_roles(metadata_ref,
346
self._get_group_metadata_ref(
347
context, user_id, tenant_id))
349
expiry = core.default_expire_time()
350
return (user_ref, tenant_ref, metadata_ref, expiry)
352
def _get_auth_token_data(self, user, tenant, metadata, expiry):
353
return dict(dict(user=user,
358
def _get_project_id_from_auth(self, context, auth):
359
"""Extract tenant information from auth dict.
361
Returns a valid tenant_id if it exists, or None if not specified.
363
tenant_id = auth.get('tenantId', None)
364
if tenant_id and len(tenant_id) > CONF.max_param_size:
365
raise exception.ValidationSizeError(attribute='tenantId',
366
size=CONF.max_param_size)
368
tenant_name = auth.get('tenantName', None)
369
if tenant_name and len(tenant_name) > CONF.max_param_size:
370
raise exception.ValidationSizeError(attribute='tenantName',
371
size=CONF.max_param_size)
375
tenant_ref = self.identity_api.get_project_by_name(
376
context=context, tenant_name=tenant_name,
377
domain_id=DEFAULT_DOMAIN_ID)
378
tenant_id = tenant_ref['id']
379
except exception.ProjectNotFound as e:
380
raise exception.Unauthorized(e)
383
def _get_domain_id_from_auth(self, context, auth):
384
"""Extract domain information from v3 auth dict.
386
Returns a valid domain_id if it exists, or None if not specified.
388
# FIXME(henry-nash): This is a placeholder that needs to be
389
# only called in the v3 context, and the auth.get calls
390
# converted to the v3 format
391
domain_id = auth.get('domainId', None)
392
domain_name = auth.get('domainName', None)
395
domain_ref = self.identity_api._get_domain_by_name(
396
context=context, domain_name=domain_name)
397
domain_id = domain_ref['id']
398
except exception.DomainNotFound as e:
399
raise exception.Unauthorized(e)
402
def _get_project_ref(self, context, user_id, tenant_id):
403
"""Returns the tenant_ref for the user's tenant"""
406
tenants = self.identity_api.get_projects_for_user(context, user_id)
407
if tenant_id not in tenants:
408
msg = 'User %s is unauthorized for tenant %s' % (
411
raise exception.Unauthorized(msg)
414
tenant_ref = self.identity_api.get_project(context=context,
416
except exception.ProjectNotFound as e:
417
exception.Unauthorized(e)
420
def _get_metadata_ref(self, context, user_id=None, tenant_id=None,
421
domain_id=None, group_id=None):
422
"""Returns metadata_ref for a user or group in a tenant or domain"""
425
if (user_id or group_id) and (tenant_id or domain_id):
427
metadata_ref = self.identity_api.get_metadata(
428
context=context, user_id=user_id, tenant_id=tenant_id,
429
domain_id=domain_id, group_id=group_id)
430
except exception.MetadataNotFound:
434
def _get_group_metadata_ref(self, context, user_id,
435
tenant_id=None, domain_id=None):
436
"""Return any metadata for this project/domain due to group grants"""
437
group_refs = self.identity_api.list_groups_for_user(context=context,
441
metadata_ref.update(self._get_metadata_ref(context,
444
domain_id=domain_id))
447
def _append_roles(self, metadata, additional_metadata):
449
Update the roles in metadata to be the union of the roles from
450
both of the passed metadatas
453
first = set(metadata.get('roles', []))
454
second = set(additional_metadata.get('roles', []))
455
metadata['roles'] = list(first.union(second))
457
def _get_token_ref(self, context, token_id, belongs_to=None):
458
"""Returns a token if a valid one exists.
460
Optionally, limited to a token owned by a specific tenant.
463
# TODO(termie): this stuff should probably be moved to middleware
464
self.assert_admin(context)
465
data = self.token_api.get_token(context=context,
468
if data.get('tenant') is None:
469
raise exception.Unauthorized(
470
_('Token does not belong to specified tenant.'))
471
if data['tenant'].get('id') != belongs_to:
472
raise exception.Unauthorized(
473
_('Token does not belong to specified tenant.'))
476
def _assert_default_domain(self, context, token_ref):
477
""" Make sure we are operating on default domain only. """
478
if token_ref.get('token_data'):
480
msg = _('Non-default domain is not supported')
481
# user in a non-default is prohibited
482
if (token_ref['token_data']['token']['user']['domain']['id'] !=
484
raise exception.Unauthorized(msg)
485
# domain scoping is prohibited
486
if token_ref['token_data']['token'].get('domain'):
487
raise exception.Unauthorized(
488
_('Domain scoped token is not supported'))
489
# project in non-default domain is prohibited
490
if token_ref['token_data']['token'].get('project'):
491
project = token_ref['token_data']['token']['project']
492
project_domain_id = project['domain']['id']
493
# scoped to project in non-default domain is prohibited
494
if project_domain_id != DEFAULT_DOMAIN_ID:
495
raise exception.Unauthorized(msg)
496
# if token is scoped to trust, both trustor and trustee must
497
# be in the default domain. Furthermore, the delegated project
498
# must also be in the default domain
499
metadata_ref = token_ref['metadata']
500
if CONF.trust.enabled and 'trust_id' in metadata_ref:
501
trust_ref = self.trust_api.get_trust(context,
502
metadata_ref['trust_id'])
503
trustee_user_ref = self.identity_api.get_user(
504
context, trust_ref['trustee_user_id'])
505
if trustee_user_ref['domain_id'] != DEFAULT_DOMAIN_ID:
506
raise exception.Unauthorized(msg)
507
trustor_user_ref = self.identity_api.get_user(
508
context, trust_ref['trustor_user_id'])
509
if trustor_user_ref['domain_id'] != DEFAULT_DOMAIN_ID:
510
raise exception.Unauthorized(msg)
511
project_ref = self.identity_api.get_project(
512
context, trust_ref['project_id'])
513
if project_ref['domain_id'] != DEFAULT_DOMAIN_ID:
514
raise exception.Unauthorized(msg)
517
def validate_token_head(self, context, token_id):
518
"""Check that a token is valid.
520
Optionally, also ensure that it is owned by a specific tenant.
522
Identical to ``validate_token``, except does not return a response.
525
belongs_to = context['query_string'].get('belongsTo')
526
token_ref = self._get_token_ref(context, token_id, belongs_to)
528
self._assert_default_domain(context, token_ref)
531
def validate_token(self, context, token_id):
532
"""Check that a token is valid.
534
Optionally, also ensure that it is owned by a specific tenant.
536
Returns metadata about the token along any associated roles.
539
belongs_to = context['query_string'].get('belongsTo')
540
token_ref = self._get_token_ref(context, token_id, belongs_to)
541
self._assert_default_domain(context, token_ref)
543
# TODO(termie): optimize this call at some point and put it into the
544
# the return for metadata
545
# fill out the roles in the metadata
546
metadata_ref = token_ref['metadata']
548
for role_id in metadata_ref.get('roles', []):
549
roles_ref.append(self.identity_api.get_role(context, role_id))
551
# Get a service catalog if possible
552
# This is needed for on-behalf-of requests
554
if token_ref.get('tenant'):
555
catalog_ref = self.catalog_api.get_catalog(
557
user_id=token_ref['user']['id'],
558
tenant_id=token_ref['tenant']['id'],
559
metadata=metadata_ref)
560
return Auth.format_token(token_ref, roles_ref, catalog_ref)
562
def delete_token(self, context, token_id):
563
"""Delete a token, effectively invalidating it for authz."""
564
# TODO(termie): this stuff should probably be moved to middleware
565
self.assert_admin(context)
566
self.token_api.delete_token(context=context, token_id=token_id)
568
def revocation_list(self, context, auth=None):
569
self.assert_admin(context)
570
tokens = self.token_api.list_revoked_tokens(context)
573
expires = t['expires']
574
if not (expires and isinstance(expires, unicode)):
575
t['expires'] = timeutils.isotime(expires)
576
data = {'revoked': tokens}
577
json_data = json.dumps(data)
578
signed_text = cms.cms_sign_text(json_data,
579
CONF.signing.certfile,
580
CONF.signing.keyfile)
582
return {'signed': signed_text}
584
def endpoints(self, context, token_id):
585
"""Return a list of endpoints available to the token."""
586
self.assert_admin(context)
588
token_ref = self._get_token_ref(context, token_id)
591
if token_ref.get('tenant'):
592
catalog_ref = self.catalog_api.get_catalog(
594
user_id=token_ref['user']['id'],
595
tenant_id=token_ref['tenant']['id'],
596
metadata=token_ref['metadata'])
598
return Auth.format_endpoint_list(catalog_ref)
601
def format_authenticate(cls, token_ref, roles_ref, catalog_ref):
602
o = Auth.format_token(token_ref, roles_ref)
603
o['access']['serviceCatalog'] = Auth.format_catalog(catalog_ref)
607
def format_token(cls, token_ref, roles_ref, catalog_ref=None):
608
user_ref = token_ref['user']
609
metadata_ref = token_ref['metadata']
610
expires = token_ref['expires']
611
if expires is not None:
612
if not isinstance(expires, unicode):
613
expires = timeutils.isotime(expires)
614
o = {'access': {'token': {'id': token_ref['id'],
616
'issued_at': timeutils.strtime()
618
'user': {'id': user_ref['id'],
619
'name': user_ref['name'],
620
'username': user_ref['name'],
622
'roles_links': metadata_ref.get('roles_links',
627
if 'tenant' in token_ref and token_ref['tenant']:
628
token_ref['tenant']['enabled'] = True
629
o['access']['token']['tenant'] = token_ref['tenant']
630
if catalog_ref is not None:
631
o['access']['serviceCatalog'] = Auth.format_catalog(catalog_ref)
633
if 'is_admin' in metadata_ref:
634
o['access']['metadata'] = {'is_admin':
635
metadata_ref['is_admin']}
637
o['access']['metadata'] = {'is_admin': 0}
638
if 'roles' in metadata_ref:
639
o['access']['metadata']['roles'] = metadata_ref['roles']
640
if CONF.trust.enabled and 'trust_id' in metadata_ref:
641
o['access']['trust'] = {'trustee_user_id':
642
metadata_ref['trustee_user_id'],
643
'id': metadata_ref['trust_id']
648
def format_catalog(cls, catalog_ref):
649
"""Munge catalogs from internal to output format
650
Internal catalogs look like:
660
The legacy api wants them to look like
662
[{'name': $SERVICE[name],
665
'tenantId': $tenant_id,
669
'endpoints_links': [],
677
for region, region_ref in catalog_ref.iteritems():
678
for service, service_ref in region_ref.iteritems():
679
new_service_ref = services.get(service, {})
680
new_service_ref['name'] = service_ref.pop('name')
681
new_service_ref['type'] = service
682
new_service_ref['endpoints_links'] = []
683
service_ref['region'] = region
685
endpoints_ref = new_service_ref.get('endpoints', [])
686
endpoints_ref.append(service_ref)
688
new_service_ref['endpoints'] = endpoints_ref
689
services[service] = new_service_ref
691
return services.values()
694
def format_endpoint_list(cls, catalog_ref):
695
"""Formats a list of endpoints according to Identity API v2.
697
The v2.0 API wants an endpoint list to look like::
703
'name': $SERVICE[name],
705
'tenantId': $tenant_id,
709
'endpoints_links': [],
717
for region_name, region_ref in catalog_ref.iteritems():
718
for service_type, service_ref in region_ref.iteritems():
720
'id': service_ref.get('id'),
721
'name': service_ref.get('name'),
722
'type': service_type,
723
'region': region_name,
724
'publicURL': service_ref.get('publicURL'),
725
'internalURL': service_ref.get('internalURL'),
726
'adminURL': service_ref.get('adminURL'),
729
return {'endpoints': endpoints, 'endpoints_links': []}