From 4e2be8a8880f03b1c6d1dc663d7259dbb45ddf67 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 11 Dec 2012 14:40:27 -0600 Subject: Move token controller into keystone.token Change-Id: Ie8277529185f645854e0aebaafa173c06a7c5164 --- keystone/common/controller.py | 12 +- keystone/contrib/ec2/core.py | 5 +- keystone/service.py | 556 ++---------------------------------------- keystone/token/__init__.py | 1 + keystone/token/controllers.py | 545 +++++++++++++++++++++++++++++++++++++++++ tests/test_auth.py | 358 +++++++++++++++++++++++++++ tests/test_service.py | 355 --------------------------- 7 files changed, 927 insertions(+), 905 deletions(-) create mode 100644 keystone/token/controllers.py create mode 100644 tests/test_auth.py delete mode 100644 tests/test_service.py diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 5db57b61..5f351411 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -55,15 +55,19 @@ def protected(f): return wrapper -class V3Controller(wsgi.Application): - """Base controller class for Identity API v3.""" +class V2Controller(wsgi.Application): + """Base controller class for Identity API v2.""" - def __init__(self, catalog_api, identity_api, token_api, policy_api): + def __init__(self, catalog_api, identity_api, policy_api, token_api): self.catalog_api = catalog_api self.identity_api = identity_api self.policy_api = policy_api self.token_api = token_api - super(V3Controller, self).__init__() + super(V2Controller, self).__init__() + + +class V3Controller(V2Controller): + """Base controller class for Identity API v3.""" def _paginate(self, context, refs): """Paginates a list of references by page & per_page query strings.""" diff --git a/keystone/contrib/ec2/core.py b/keystone/contrib/ec2/core.py index d9e9eaae..078a845c 100644 --- a/keystone/contrib/ec2/core.py +++ b/keystone/contrib/ec2/core.py @@ -44,7 +44,6 @@ from keystone import config from keystone import exception from keystone import identity from keystone import policy -from keystone import service from keystone import token @@ -190,12 +189,10 @@ class Ec2Controller(wsgi.Application): tenant=tenant_ref, metadata=metadata_ref)) - # TODO(termie): make this a util function or something # TODO(termie): i don't think the ec2 middleware currently expects a # full return, but it contains a note saying that it # would be better to expect a full return - token_controller = service.TokenController() - return token_controller._format_authenticate( + return token.controllers.Auth.format_authenticate( token_ref, roles_ref, catalog_ref) def create_credential(self, context, user_id, tenant_id): diff --git a/keystone/service.py b/keystone/service.py index 67167604..f103aac3 100644 --- a/keystone/service.py +++ b/keystone/service.py @@ -14,18 +14,13 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid import routes -import json -from keystone import config from keystone import catalog -from keystone.common import cms from keystone.common import logging from keystone.common import wsgi from keystone import exception from keystone import identity -from keystone.openstack.common import timeutils from keystone import policy from keystone import token @@ -207,13 +202,19 @@ class AdminRouter(wsgi.ComposingRouter): def __init__(self): mapper = routes.Mapper() + apis = dict( + catalog_api=catalog.Manager(), + identity_api=identity.Manager(), + policy_api=policy.Manager(), + token_api=token.Manager()) + version_controller = VersionController('admin') mapper.connect('/', controller=version_controller, action='get_version') # Token Operations - auth_controller = TokenController() + auth_controller = token.controllers.Auth(**apis) mapper.connect('/tokens', controller=auth_controller, action='authenticate', @@ -269,13 +270,19 @@ class PublicRouter(wsgi.ComposingRouter): def __init__(self): mapper = routes.Mapper() + apis = dict( + catalog_api=catalog.Manager(), + identity_api=identity.Manager(), + policy_api=policy.Manager(), + token_api=token.Manager()) + version_controller = VersionController('public') mapper.connect('/', controller=version_controller, action='get_version') # Token Operations - auth_controller = TokenController() + auth_controller = token.controllers.Auth(**apis) mapper.connect('/tokens', controller=auth_controller, action='authenticate', @@ -414,541 +421,6 @@ class NoopController(wsgi.Application): return {} -class ExternalAuthNotApplicable(Exception): - """External authentication is not applicable""" - - -class TokenController(wsgi.Application): - def __init__(self): - self.catalog_api = catalog.Manager() - self.identity_api = identity.Manager() - self.token_api = token.Manager() - self.policy_api = policy.Manager() - super(TokenController, self).__init__() - - def ca_cert(self, context, auth=None): - ca_file = open(config.CONF.signing.ca_certs, 'r') - data = ca_file.read() - ca_file.close() - return data - - def signing_cert(self, context, auth=None): - cert_file = open(config.CONF.signing.certfile, 'r') - data = cert_file.read() - cert_file.close() - return data - - def authenticate(self, context, auth=None): - """Authenticate credentials and return a token. - - Accept auth as a dict that looks like:: - - { - "auth":{ - "passwordCredentials":{ - "username":"test_user", - "password":"mypass" - }, - "tenantName":"customer-x" - } - } - - In this case, tenant is optional, if not provided the token will be - considered "unscoped" and can later be used to get a scoped token. - - Alternatively, this call accepts auth with only a token and tenant - that will return a token that is scoped to that tenant. - """ - - if auth is None: - raise exception.ValidationError(attribute='auth', - target='request body') - - auth_token_data = None - - if "token" in auth: - # Try to authenticate using a token - auth_token_data, auth_info = self._authenticate_token( - context, auth) - else: - # Try external authentication - try: - auth_token_data, auth_info = self._authenticate_external( - context, auth) - except ExternalAuthNotApplicable: - # Try local authentication - auth_token_data, auth_info = self._authenticate_local( - context, auth) - - user_ref, tenant_ref, metadata_ref = auth_info - - # If the user is disabled don't allow them to authenticate - if not user_ref.get('enabled', True): - msg = 'User is disabled: %s' % user_ref['id'] - LOG.warning(msg) - raise exception.Unauthorized(msg) - - # If the tenant is disabled don't allow them to authenticate - if tenant_ref and not tenant_ref.get('enabled', True): - msg = 'Tenant is disabled: %s' % tenant_ref['id'] - LOG.warning(msg) - raise exception.Unauthorized(msg) - - if tenant_ref: - catalog_ref = self.catalog_api.get_catalog( - context=context, - user_id=user_ref['id'], - tenant_id=tenant_ref['id'], - metadata=metadata_ref) - else: - catalog_ref = {} - - auth_token_data['id'] = 'placeholder' - - roles_ref = [] - for role_id in metadata_ref.get('roles', []): - role_ref = self.identity_api.get_role(context, role_id) - roles_ref.append(dict(name=role_ref['name'])) - - token_data = self._format_token(auth_token_data, roles_ref) - - service_catalog = self._format_catalog(catalog_ref) - token_data['access']['serviceCatalog'] = service_catalog - - if config.CONF.signing.token_format == 'UUID': - token_id = uuid.uuid4().hex - elif config.CONF.signing.token_format == 'PKI': - token_id = cms.cms_sign_token(json.dumps(token_data), - config.CONF.signing.certfile, - config.CONF.signing.keyfile) - else: - raise exception.UnexpectedError( - 'Invalid value for token_format: %s.' - ' Allowed values are PKI or UUID.' % - config.CONF.signing.token_format) - try: - self.token_api.create_token( - context, token_id, dict(key=token_id, - id=token_id, - expires=auth_token_data['expires'], - user=user_ref, - tenant=tenant_ref, - metadata=metadata_ref)) - except Exception as e: - # an identical token may have been created already. - # if so, return the token_data as it is also identical - try: - self.token_api.get_token(context=context, - token_id=token_id) - except exception.TokenNotFound: - raise e - - token_data['access']['token']['id'] = token_id - - return token_data - - def _authenticate_token(self, context, auth): - """Try to authenticate using an already existing token. - - Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) - """ - if 'token' not in auth: - raise exception.ValidationError( - attribute='token', target='auth') - - if "id" not in auth['token']: - raise exception.ValidationError( - attribute="id", target="token") - - old_token = auth['token']['id'] - - try: - old_token_ref = self.token_api.get_token(context=context, - token_id=old_token) - except exception.NotFound as e: - raise exception.Unauthorized(e) - - user_ref = old_token_ref['user'] - user_id = user_ref['id'] - - current_user_ref = self.identity_api.get_user(context=context, - user_id=user_id) - - tenant_id = self._get_tenant_id_from_auth(context, auth) - - tenant_ref = self._get_tenant_ref(context, user_id, tenant_id) - metadata_ref = self._get_metadata_ref(context, user_id, tenant_id) - - expiry = old_token_ref['expires'] - auth_token_data = self._get_auth_token_data(current_user_ref, - tenant_ref, - metadata_ref, - expiry) - - return auth_token_data, (current_user_ref, tenant_ref, metadata_ref) - - def _authenticate_local(self, context, auth): - """Try to authenticate against the identity backend. - - Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) - """ - if 'passwordCredentials' not in auth: - raise exception.ValidationError( - attribute='passwordCredentials', target='auth') - - if "password" not in auth['passwordCredentials']: - raise exception.ValidationError( - attribute='password', target='passwordCredentials') - - password = auth['passwordCredentials']['password'] - - if ("userId" not in auth['passwordCredentials'] and - "username" not in auth['passwordCredentials']): - raise exception.ValidationError( - attribute='username or userId', - target='passwordCredentials') - - user_id = auth['passwordCredentials'].get('userId', None) - username = auth['passwordCredentials'].get('username', '') - - if username: - try: - user_ref = self.identity_api.get_user_by_name( - context=context, user_name=username) - user_id = user_ref['id'] - except exception.UserNotFound as e: - raise exception.Unauthorized(e) - - tenant_id = self._get_tenant_id_from_auth(context, auth) - - try: - auth_info = self.identity_api.authenticate( - context=context, - user_id=user_id, - password=password, - tenant_id=tenant_id) - except AssertionError as e: - raise exception.Unauthorized(e) - (user_ref, tenant_ref, metadata_ref) = auth_info - - expiry = self.token_api._get_default_expire_time(context=context) - auth_token_data = self._get_auth_token_data(user_ref, - tenant_ref, - metadata_ref, - expiry) - - return auth_token_data, (user_ref, tenant_ref, metadata_ref) - - def _authenticate_external(self, context, auth): - """Try to authenticate an external user via REMOTE_USER variable. - - Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) - """ - if 'REMOTE_USER' not in context: - raise ExternalAuthNotApplicable() - - username = context['REMOTE_USER'] - try: - user_ref = self.identity_api.get_user_by_name( - context=context, user_name=username) - user_id = user_ref['id'] - except exception.UserNotFound as e: - raise exception.Unauthorized(e) - - tenant_id = self._get_tenant_id_from_auth(context, auth) - - tenant_ref = self._get_tenant_ref(context, user_id, tenant_id) - metadata_ref = self._get_metadata_ref(context, user_id, tenant_id) - - expiry = self.token_api._get_default_expire_time(context=context) - auth_token_data = self._get_auth_token_data(user_ref, - tenant_ref, - metadata_ref, - expiry) - - return auth_token_data, (user_ref, tenant_ref, metadata_ref) - - def _get_auth_token_data(self, user, tenant, metadata, expiry): - return dict(dict(user=user, - tenant=tenant, - metadata=metadata, - expires=expiry)) - - def _get_tenant_id_from_auth(self, context, auth): - """Extract tenant information from auth dict. - - Returns a valid tenant_id if it exists, or None if not specified. - """ - tenant_id = auth.get('tenantId', None) - tenant_name = auth.get('tenantName', None) - if tenant_name: - try: - tenant_ref = self.identity_api.get_tenant_by_name( - context=context, tenant_name=tenant_name) - tenant_id = tenant_ref['id'] - except exception.TenantNotFound as e: - raise exception.Unauthorized(e) - return tenant_id - - def _get_tenant_ref(self, context, user_id, tenant_id): - """Returns the tenant_ref for the user's tenant""" - tenant_ref = None - if tenant_id: - tenants = self.identity_api.get_tenants_for_user(context, user_id) - if tenant_id not in tenants: - msg = 'User %s is unauthorized for tenant %s' % ( - user_id, tenant_id) - LOG.warning(msg) - raise exception.Unauthorized(msg) - - try: - tenant_ref = self.identity_api.get_tenant(context=context, - tenant_id=tenant_id) - except exception.TenantNotFound as e: - exception.Unauthorized(e) - return tenant_ref - - def _get_metadata_ref(self, context, user_id, tenant_id): - """Returns the metadata_ref for a user in a tenant""" - metadata_ref = {} - if tenant_id: - try: - metadata_ref = self.identity_api.get_metadata( - context=context, - user_id=user_id, - tenant_id=tenant_id) - except exception.MetadataNotFound: - metadata_ref = {} - - return metadata_ref - - def _get_token_ref(self, context, token_id, belongs_to=None): - """Returns a token if a valid one exists. - - Optionally, limited to a token owned by a specific tenant. - - """ - # TODO(termie): this stuff should probably be moved to middleware - self.assert_admin(context) - - if cms.is_ans1_token(token_id): - data = json.loads(cms.cms_verify(cms.token_to_cms(token_id), - config.CONF.signing.certfile, - config.CONF.signing.ca_certs)) - data['access']['token']['user'] = data['access']['user'] - data['access']['token']['metadata'] = data['access']['metadata'] - if belongs_to: - assert data['access']['token']['tenant']['id'] == belongs_to - token_ref = data['access']['token'] - else: - token_ref = self.token_api.get_token(context=context, - token_id=token_id) - return token_ref - - # admin only - def validate_token_head(self, context, token_id): - """Check that a token is valid. - - Optionally, also ensure that it is owned by a specific tenant. - - Identical to ``validate_token``, except does not return a response. - - """ - belongs_to = context['query_string'].get('belongsTo') - assert self._get_token_ref(context, token_id, belongs_to) - - # admin only - def validate_token(self, context, token_id): - """Check that a token is valid. - - Optionally, also ensure that it is owned by a specific tenant. - - Returns metadata about the token along any associated roles. - - """ - belongs_to = context['query_string'].get('belongsTo') - token_ref = self._get_token_ref(context, token_id, belongs_to) - - # TODO(termie): optimize this call at some point and put it into the - # the return for metadata - # fill out the roles in the metadata - metadata_ref = token_ref['metadata'] - roles_ref = [] - for role_id in metadata_ref.get('roles', []): - roles_ref.append(self.identity_api.get_role(context, role_id)) - - # Get a service catalog if possible - # This is needed for on-behalf-of requests - catalog_ref = None - if token_ref.get('tenant'): - catalog_ref = self.catalog_api.get_catalog( - context=context, - user_id=token_ref['user']['id'], - tenant_id=token_ref['tenant']['id'], - metadata=metadata_ref) - return self._format_token(token_ref, roles_ref, catalog_ref) - - def delete_token(self, context, token_id): - """Delete a token, effectively invalidating it for authz.""" - # TODO(termie): this stuff should probably be moved to middleware - self.assert_admin(context) - self.token_api.delete_token(context=context, token_id=token_id) - - def revocation_list(self, context, auth=None): - self.assert_admin(context) - tokens = self.token_api.list_revoked_tokens(context) - - for t in tokens: - expires = t['expires'] - if not (expires and isinstance(expires, unicode)): - t['expires'] = timeutils.isotime(expires) - data = {'revoked': tokens} - json_data = json.dumps(data) - signed_text = cms.cms_sign_text(json_data, - config.CONF.signing.certfile, - config.CONF.signing.keyfile) - - return {'signed': signed_text} - - def endpoints(self, context, token_id): - """Return a list of endpoints available to the token.""" - self.assert_admin(context) - - token_ref = self._get_token_ref(context, token_id) - - catalog_ref = None - if token_ref.get('tenant'): - catalog_ref = self.catalog_api.get_catalog( - context=context, - user_id=token_ref['user']['id'], - tenant_id=token_ref['tenant']['id'], - metadata=token_ref['metadata']) - - return self._format_endpoint_list(catalog_ref) - - def _format_authenticate(self, token_ref, roles_ref, catalog_ref): - o = self._format_token(token_ref, roles_ref) - o['access']['serviceCatalog'] = self._format_catalog(catalog_ref) - return o - - def _format_token(self, token_ref, roles_ref, catalog_ref=None): - user_ref = token_ref['user'] - metadata_ref = token_ref['metadata'] - expires = token_ref['expires'] - if expires is not None: - if not isinstance(expires, unicode): - expires = timeutils.isotime(expires) - o = {'access': {'token': {'id': token_ref['id'], - 'expires': expires, - 'issued_at': timeutils.strtime() - }, - 'user': {'id': user_ref['id'], - 'name': user_ref['name'], - 'username': user_ref['name'], - 'roles': roles_ref, - 'roles_links': metadata_ref.get('roles_links', - []) - } - } - } - if 'tenant' in token_ref and token_ref['tenant']: - token_ref['tenant']['enabled'] = True - o['access']['token']['tenant'] = token_ref['tenant'] - if catalog_ref is not None: - o['access']['serviceCatalog'] = self._format_catalog(catalog_ref) - if metadata_ref: - if 'is_admin' in metadata_ref: - o['access']['metadata'] = {'is_admin': - metadata_ref['is_admin']} - else: - o['access']['metadata'] = {'is_admin': 0} - if 'roles' in metadata_ref: - o['access']['metadata']['roles'] = metadata_ref['roles'] - return o - - def _format_catalog(self, catalog_ref): - """Munge catalogs from internal to output format - Internal catalogs look like: - - {$REGION: { - {$SERVICE: { - $key1: $value1, - ... - } - } - } - - The legacy api wants them to look like - - [{'name': $SERVICE[name], - 'type': $SERVICE, - 'endpoints': [{ - 'tenantId': $tenant_id, - ... - 'region': $REGION, - }], - 'endpoints_links': [], - }] - - """ - if not catalog_ref: - return [] - - services = {} - for region, region_ref in catalog_ref.iteritems(): - for service, service_ref in region_ref.iteritems(): - new_service_ref = services.get(service, {}) - new_service_ref['name'] = service_ref.pop('name') - new_service_ref['type'] = service - new_service_ref['endpoints_links'] = [] - service_ref['region'] = region - - endpoints_ref = new_service_ref.get('endpoints', []) - endpoints_ref.append(service_ref) - - new_service_ref['endpoints'] = endpoints_ref - services[service] = new_service_ref - - return services.values() - - def _format_endpoint_list(self, catalog_ref): - """Formats a list of endpoints according to Identity API v2. - - The v2.0 API wants an endpoint list to look like:: - - { - 'endpoints': [ - { - 'id': $endpoint_id, - 'name': $SERVICE[name], - 'type': $SERVICE, - 'tenantId': $tenant_id, - 'region': $REGION, - } - ], - 'endpoints_links': [], - } - - """ - if not catalog_ref: - return {} - - endpoints = [] - for region_name, region_ref in catalog_ref.iteritems(): - for service_type, service_ref in region_ref.iteritems(): - endpoints.append({ - 'id': service_ref.get('id'), - 'name': service_ref.get('name'), - 'type': service_type, - 'region': region_name, - 'publicURL': service_ref.get('publicURL'), - 'internalURL': service_ref.get('internalURL'), - 'adminURL': service_ref.get('adminURL'), - }) - - return {'endpoints': endpoints, 'endpoints_links': []} - - class ExtensionsController(wsgi.Application): """Base extensions controller to be extended by public and admin API's.""" diff --git a/keystone/token/__init__.py b/keystone/token/__init__.py index 884f2658..b0765ece 100644 --- a/keystone/token/__init__.py +++ b/keystone/token/__init__.py @@ -15,3 +15,4 @@ # under the License. from keystone.token.core import * +from keystone.token import controllers diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py new file mode 100644 index 00000000..329235ad --- /dev/null +++ b/keystone/token/controllers.py @@ -0,0 +1,545 @@ +import uuid +import json + +from keystone import config +from keystone.common import cms +from keystone.common import controller +from keystone.common import logging +from keystone import exception +from keystone.openstack.common import timeutils + + +LOG = logging.getLogger(__name__) + + +class ExternalAuthNotApplicable(Exception): + """External authentication is not applicable""" + pass + + +class Auth(controller.V2Controller): + def ca_cert(self, context, auth=None): + ca_file = open(config.CONF.signing.ca_certs, 'r') + data = ca_file.read() + ca_file.close() + return data + + def signing_cert(self, context, auth=None): + cert_file = open(config.CONF.signing.certfile, 'r') + data = cert_file.read() + cert_file.close() + return data + + def authenticate(self, context, auth=None): + """Authenticate credentials and return a token. + + Accept auth as a dict that looks like:: + + { + "auth":{ + "passwordCredentials":{ + "username":"test_user", + "password":"mypass" + }, + "tenantName":"customer-x" + } + } + + In this case, tenant is optional, if not provided the token will be + considered "unscoped" and can later be used to get a scoped token. + + Alternatively, this call accepts auth with only a token and tenant + that will return a token that is scoped to that tenant. + """ + + if auth is None: + raise exception.ValidationError(attribute='auth', + target='request body') + + auth_token_data = None + + if "token" in auth: + # Try to authenticate using a token + auth_token_data, auth_info = self._authenticate_token( + context, auth) + else: + # Try external authentication + try: + auth_token_data, auth_info = self._authenticate_external( + context, auth) + except ExternalAuthNotApplicable: + # Try local authentication + auth_token_data, auth_info = self._authenticate_local( + context, auth) + + user_ref, tenant_ref, metadata_ref = auth_info + + # If the user is disabled don't allow them to authenticate + if not user_ref.get('enabled', True): + msg = 'User is disabled: %s' % user_ref['id'] + LOG.warning(msg) + raise exception.Unauthorized(msg) + + # If the tenant is disabled don't allow them to authenticate + if tenant_ref and not tenant_ref.get('enabled', True): + msg = 'Tenant is disabled: %s' % tenant_ref['id'] + LOG.warning(msg) + raise exception.Unauthorized(msg) + + if tenant_ref: + catalog_ref = self.catalog_api.get_catalog( + context=context, + user_id=user_ref['id'], + tenant_id=tenant_ref['id'], + metadata=metadata_ref) + else: + catalog_ref = {} + + auth_token_data['id'] = 'placeholder' + + roles_ref = [] + for role_id in metadata_ref.get('roles', []): + role_ref = self.identity_api.get_role(context, role_id) + roles_ref.append(dict(name=role_ref['name'])) + + token_data = Auth.format_token(auth_token_data, roles_ref) + + service_catalog = Auth.format_catalog(catalog_ref) + token_data['access']['serviceCatalog'] = service_catalog + + if config.CONF.signing.token_format == 'UUID': + token_id = uuid.uuid4().hex + elif config.CONF.signing.token_format == 'PKI': + token_id = cms.cms_sign_token(json.dumps(token_data), + config.CONF.signing.certfile, + config.CONF.signing.keyfile) + else: + raise exception.UnexpectedError( + 'Invalid value for token_format: %s.' + ' Allowed values are PKI or UUID.' % + config.CONF.signing.token_format) + try: + self.token_api.create_token( + context, token_id, dict(key=token_id, + id=token_id, + expires=auth_token_data['expires'], + user=user_ref, + tenant=tenant_ref, + metadata=metadata_ref)) + except Exception as e: + # an identical token may have been created already. + # if so, return the token_data as it is also identical + try: + self.token_api.get_token(context=context, + token_id=token_id) + except exception.TokenNotFound: + raise e + + token_data['access']['token']['id'] = token_id + + return token_data + + def _authenticate_token(self, context, auth): + """Try to authenticate using an already existing token. + + Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) + """ + if 'token' not in auth: + raise exception.ValidationError( + attribute='token', target='auth') + + if "id" not in auth['token']: + raise exception.ValidationError( + attribute="id", target="token") + + old_token = auth['token']['id'] + + try: + old_token_ref = self.token_api.get_token(context=context, + token_id=old_token) + except exception.NotFound as e: + raise exception.Unauthorized(e) + + user_ref = old_token_ref['user'] + user_id = user_ref['id'] + + current_user_ref = self.identity_api.get_user(context=context, + user_id=user_id) + + tenant_id = self._get_tenant_id_from_auth(context, auth) + + tenant_ref = self._get_tenant_ref(context, user_id, tenant_id) + metadata_ref = self._get_metadata_ref(context, user_id, tenant_id) + + expiry = old_token_ref['expires'] + auth_token_data = self._get_auth_token_data(current_user_ref, + tenant_ref, + metadata_ref, + expiry) + + return auth_token_data, (current_user_ref, tenant_ref, metadata_ref) + + def _authenticate_local(self, context, auth): + """Try to authenticate against the identity backend. + + Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) + """ + if 'passwordCredentials' not in auth: + raise exception.ValidationError( + attribute='passwordCredentials', target='auth') + + if "password" not in auth['passwordCredentials']: + raise exception.ValidationError( + attribute='password', target='passwordCredentials') + + password = auth['passwordCredentials']['password'] + + if ("userId" not in auth['passwordCredentials'] and + "username" not in auth['passwordCredentials']): + raise exception.ValidationError( + attribute='username or userId', + target='passwordCredentials') + + user_id = auth['passwordCredentials'].get('userId', None) + username = auth['passwordCredentials'].get('username', '') + + if username: + try: + user_ref = self.identity_api.get_user_by_name( + context=context, user_name=username) + user_id = user_ref['id'] + except exception.UserNotFound as e: + raise exception.Unauthorized(e) + + tenant_id = self._get_tenant_id_from_auth(context, auth) + + try: + auth_info = self.identity_api.authenticate( + context=context, + user_id=user_id, + password=password, + tenant_id=tenant_id) + except AssertionError as e: + raise exception.Unauthorized(e) + (user_ref, tenant_ref, metadata_ref) = auth_info + + expiry = self.token_api._get_default_expire_time(context=context) + auth_token_data = self._get_auth_token_data(user_ref, + tenant_ref, + metadata_ref, + expiry) + + return auth_token_data, (user_ref, tenant_ref, metadata_ref) + + def _authenticate_external(self, context, auth): + """Try to authenticate an external user via REMOTE_USER variable. + + Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) + """ + if 'REMOTE_USER' not in context: + raise ExternalAuthNotApplicable() + + username = context['REMOTE_USER'] + try: + user_ref = self.identity_api.get_user_by_name( + context=context, user_name=username) + user_id = user_ref['id'] + except exception.UserNotFound as e: + raise exception.Unauthorized(e) + + tenant_id = self._get_tenant_id_from_auth(context, auth) + + tenant_ref = self._get_tenant_ref(context, user_id, tenant_id) + metadata_ref = self._get_metadata_ref(context, user_id, tenant_id) + + expiry = self.token_api._get_default_expire_time(context=context) + auth_token_data = self._get_auth_token_data(user_ref, + tenant_ref, + metadata_ref, + expiry) + + return auth_token_data, (user_ref, tenant_ref, metadata_ref) + + def _get_auth_token_data(self, user, tenant, metadata, expiry): + return dict(dict(user=user, + tenant=tenant, + metadata=metadata, + expires=expiry)) + + def _get_tenant_id_from_auth(self, context, auth): + """Extract tenant information from auth dict. + + Returns a valid tenant_id if it exists, or None if not specified. + """ + tenant_id = auth.get('tenantId', None) + tenant_name = auth.get('tenantName', None) + if tenant_name: + try: + tenant_ref = self.identity_api.get_tenant_by_name( + context=context, tenant_name=tenant_name) + tenant_id = tenant_ref['id'] + except exception.TenantNotFound as e: + raise exception.Unauthorized(e) + return tenant_id + + def _get_tenant_ref(self, context, user_id, tenant_id): + """Returns the tenant_ref for the user's tenant""" + tenant_ref = None + if tenant_id: + tenants = self.identity_api.get_tenants_for_user(context, user_id) + if tenant_id not in tenants: + msg = 'User %s is unauthorized for tenant %s' % ( + user_id, tenant_id) + LOG.warning(msg) + raise exception.Unauthorized(msg) + + try: + tenant_ref = self.identity_api.get_tenant(context=context, + tenant_id=tenant_id) + except exception.TenantNotFound as e: + exception.Unauthorized(e) + return tenant_ref + + def _get_metadata_ref(self, context, user_id, tenant_id): + """Returns the metadata_ref for a user in a tenant""" + metadata_ref = {} + if tenant_id: + try: + metadata_ref = self.identity_api.get_metadata( + context=context, + user_id=user_id, + tenant_id=tenant_id) + except exception.MetadataNotFound: + metadata_ref = {} + + return metadata_ref + + def _get_token_ref(self, context, token_id, belongs_to=None): + """Returns a token if a valid one exists. + + Optionally, limited to a token owned by a specific tenant. + + """ + # TODO(termie): this stuff should probably be moved to middleware + self.assert_admin(context) + + if cms.is_ans1_token(token_id): + data = json.loads(cms.cms_verify(cms.token_to_cms(token_id), + config.CONF.signing.certfile, + config.CONF.signing.ca_certs)) + data['access']['token']['user'] = data['access']['user'] + data['access']['token']['metadata'] = data['access']['metadata'] + if belongs_to: + assert data['access']['token']['tenant']['id'] == belongs_to + token_ref = data['access']['token'] + else: + token_ref = self.token_api.get_token(context=context, + token_id=token_id) + return token_ref + + # admin only + def validate_token_head(self, context, token_id): + """Check that a token is valid. + + Optionally, also ensure that it is owned by a specific tenant. + + Identical to ``validate_token``, except does not return a response. + + """ + belongs_to = context['query_string'].get('belongsTo') + assert self._get_token_ref(context, token_id, belongs_to) + + # admin only + def validate_token(self, context, token_id): + """Check that a token is valid. + + Optionally, also ensure that it is owned by a specific tenant. + + Returns metadata about the token along any associated roles. + + """ + belongs_to = context['query_string'].get('belongsTo') + token_ref = self._get_token_ref(context, token_id, belongs_to) + + # TODO(termie): optimize this call at some point and put it into the + # the return for metadata + # fill out the roles in the metadata + metadata_ref = token_ref['metadata'] + roles_ref = [] + for role_id in metadata_ref.get('roles', []): + roles_ref.append(self.identity_api.get_role(context, role_id)) + + # Get a service catalog if possible + # This is needed for on-behalf-of requests + catalog_ref = None + if token_ref.get('tenant'): + catalog_ref = self.catalog_api.get_catalog( + context=context, + user_id=token_ref['user']['id'], + tenant_id=token_ref['tenant']['id'], + metadata=metadata_ref) + return Auth.format_token(token_ref, roles_ref, catalog_ref) + + def delete_token(self, context, token_id): + """Delete a token, effectively invalidating it for authz.""" + # TODO(termie): this stuff should probably be moved to middleware + self.assert_admin(context) + self.token_api.delete_token(context=context, token_id=token_id) + + def revocation_list(self, context, auth=None): + self.assert_admin(context) + tokens = self.token_api.list_revoked_tokens(context) + + for t in tokens: + expires = t['expires'] + if not (expires and isinstance(expires, unicode)): + t['expires'] = timeutils.isotime(expires) + data = {'revoked': tokens} + json_data = json.dumps(data) + signed_text = cms.cms_sign_text(json_data, + config.CONF.signing.certfile, + config.CONF.signing.keyfile) + + return {'signed': signed_text} + + def endpoints(self, context, token_id): + """Return a list of endpoints available to the token.""" + self.assert_admin(context) + + token_ref = self._get_token_ref(context, token_id) + + catalog_ref = None + if token_ref.get('tenant'): + catalog_ref = self.catalog_api.get_catalog( + context=context, + user_id=token_ref['user']['id'], + tenant_id=token_ref['tenant']['id'], + metadata=token_ref['metadata']) + + return Auth.format_endpoint_list(catalog_ref) + + @classmethod + def format_authenticate(cls, token_ref, roles_ref, catalog_ref): + o = Auth.format_token(token_ref, roles_ref) + o['access']['serviceCatalog'] = Auth.format_catalog(catalog_ref) + return o + + @classmethod + def format_token(cls, token_ref, roles_ref, catalog_ref=None): + user_ref = token_ref['user'] + metadata_ref = token_ref['metadata'] + expires = token_ref['expires'] + if expires is not None: + if not isinstance(expires, unicode): + expires = timeutils.isotime(expires) + o = {'access': {'token': {'id': token_ref['id'], + 'expires': expires, + 'issued_at': timeutils.strtime() + }, + 'user': {'id': user_ref['id'], + 'name': user_ref['name'], + 'username': user_ref['name'], + 'roles': roles_ref, + 'roles_links': metadata_ref.get('roles_links', + []) + } + } + } + if 'tenant' in token_ref and token_ref['tenant']: + token_ref['tenant']['enabled'] = True + o['access']['token']['tenant'] = token_ref['tenant'] + if catalog_ref is not None: + o['access']['serviceCatalog'] = Auth.format_catalog(catalog_ref) + if metadata_ref: + if 'is_admin' in metadata_ref: + o['access']['metadata'] = {'is_admin': + metadata_ref['is_admin']} + else: + o['access']['metadata'] = {'is_admin': 0} + if 'roles' in metadata_ref: + o['access']['metadata']['roles'] = metadata_ref['roles'] + return o + + @classmethod + def format_catalog(cls, catalog_ref): + """Munge catalogs from internal to output format + Internal catalogs look like: + + {$REGION: { + {$SERVICE: { + $key1: $value1, + ... + } + } + } + + The legacy api wants them to look like + + [{'name': $SERVICE[name], + 'type': $SERVICE, + 'endpoints': [{ + 'tenantId': $tenant_id, + ... + 'region': $REGION, + }], + 'endpoints_links': [], + }] + + """ + if not catalog_ref: + return [] + + services = {} + for region, region_ref in catalog_ref.iteritems(): + for service, service_ref in region_ref.iteritems(): + new_service_ref = services.get(service, {}) + new_service_ref['name'] = service_ref.pop('name') + new_service_ref['type'] = service + new_service_ref['endpoints_links'] = [] + service_ref['region'] = region + + endpoints_ref = new_service_ref.get('endpoints', []) + endpoints_ref.append(service_ref) + + new_service_ref['endpoints'] = endpoints_ref + services[service] = new_service_ref + + return services.values() + + @classmethod + def format_endpoint_list(cls, catalog_ref): + """Formats a list of endpoints according to Identity API v2. + + The v2.0 API wants an endpoint list to look like:: + + { + 'endpoints': [ + { + 'id': $endpoint_id, + 'name': $SERVICE[name], + 'type': $SERVICE, + 'tenantId': $tenant_id, + 'region': $REGION, + } + ], + 'endpoints_links': [], + } + + """ + if not catalog_ref: + return {} + + endpoints = [] + for region_name, region_ref in catalog_ref.iteritems(): + for service_type, service_ref in region_ref.iteritems(): + endpoints.append({ + 'id': service_ref.get('id'), + 'name': service_ref.get('name'), + 'type': service_type, + 'region': region_name, + 'publicURL': service_ref.get('publicURL'), + 'internalURL': service_ref.get('internalURL'), + 'adminURL': service_ref.get('adminURL'), + }) + + return {'endpoints': endpoints, 'endpoints_links': []} diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 00000000..c0dbbd2c --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,358 @@ +# Copyright 2012 OpenStack LLC +# +# 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 time +import uuid + +import default_fixtures + +from keystone import catalog +from keystone import config +from keystone import exception +from keystone import identity +from keystone.identity.backends import kvs as kvs_identity +from keystone.openstack.common import timeutils +from keystone import policy +from keystone import test +from keystone import token + + +CONF = config.CONF + + +def _build_user_auth(token=None, username=None, + password=None, tenant_name=None): + """Build auth dictionary. + + It will create an auth dictionary based on all the arguments + that it receives. + """ + auth_json = {} + if token is not None: + auth_json['token'] = token + if username or password: + auth_json['passwordCredentials'] = {} + if username is not None: + auth_json['passwordCredentials']['username'] = username + if password is not None: + auth_json['passwordCredentials']['password'] = password + if tenant_name is not None: + auth_json['tenantName'] = tenant_name + return auth_json + + +class AuthTest(test.TestCase): + def setUp(self): + super(AuthTest, self).setUp() + + # load_fixtures checks for 'identity_api' to be defined + self.identity_api = kvs_identity.Identity() + self.load_fixtures(default_fixtures) + + self.api = token.controllers.Auth( + catalog_api=catalog.Manager(), + identity_api=identity.Manager(), + policy_api=policy.Manager(), + token_api=token.Manager()) + + def assertEqualTokens(self, a, b): + """Assert that two tokens are equal. + + Compare two tokens except for their ids. This also truncates + the time in the comparison. + """ + def normalize(token): + token['access']['token']['id'] = 'dummy' + del token['access']['token']['expires'] + del token['access']['token']['issued_at'] + return token + + self.assertCloseEnoughForGovernmentWork( + timeutils.parse_isotime(a['access']['token']['expires']), + timeutils.parse_isotime(b['access']['token']['expires'])) + self.assertCloseEnoughForGovernmentWork( + timeutils.parse_isotime(a['access']['token']['issued_at']), + timeutils.parse_isotime(b['access']['token']['issued_at'])) + return self.assertDictEqual(normalize(a), normalize(b)) + + +class AuthBadRequests(AuthTest): + def setUp(self): + super(AuthBadRequests, self).setUp() + + def test_no_external_auth(self): + """Verify that _authenticate_external() raises exception if + not applicable""" + self.assertRaises( + token.controllers.ExternalAuthNotApplicable, + self.api._authenticate_external, + {}, {}) + + def test_no_token_in_auth(self): + """Verity that _authenticate_token() raises exception if no token""" + self.assertRaises( + exception.ValidationError, + self.api._authenticate_token, + None, {}) + + def test_no_credentials_in_auth(self): + """Verity that _authenticate_local() raises exception if no creds""" + self.assertRaises( + exception.ValidationError, + self.api._authenticate_local, + None, {}) + + def test_authenticate_blank_request_body(self): + """Verify sending empty json dict raises the right exception.""" + self.assertRaises(exception.ValidationError, self.api.authenticate, + {}, {}) + + def test_authenticate_blank_auth(self): + """Verify sending blank 'auth' raises the right exception.""" + body_dict = _build_user_auth() + self.assertRaises(exception.ValidationError, self.api.authenticate, + {}, body_dict) + + def test_authenticate_invalid_auth_content(self): + """Verify sending invalid 'auth' raises the right exception.""" + self.assertRaises(exception.ValidationError, self.api.authenticate, + {}, {'auth': 'abcd'}) + + +class AuthWithToken(AuthTest): + def setUp(self): + super(AuthWithToken, self).setUp() + + def test_unscoped_token(self): + """Verify getting an unscoped token with password creds""" + body_dict = _build_user_auth(username='FOO', + password='foo2') + unscoped_token = self.api.authenticate({}, body_dict) + tenant = unscoped_token["access"]["token"].get("tenant", None) + self.assertEqual(tenant, None) + + def test_auth_invalid_token(self): + """Verify exception is raised if invalid token""" + body_dict = _build_user_auth(token={"id": uuid.uuid4().hex}) + self.assertRaises( + exception.Unauthorized, + self.api.authenticate, + {}, body_dict) + + def test_auth_bad_formatted_token(self): + """Verify exception is raised if invalid token""" + body_dict = _build_user_auth(token={}) + self.assertRaises( + exception.ValidationError, + self.api.authenticate, + {}, body_dict) + + def test_auth_unscoped_token_no_tenant(self): + """Verify getting an unscoped token with an unscoped token""" + body_dict = _build_user_auth( + username='FOO', + password='foo2') + unscoped_token = self.api.authenticate({}, body_dict) + + body_dict = _build_user_auth( + token=unscoped_token["access"]["token"]) + unscoped_token_2 = self.api.authenticate({}, body_dict) + + self.assertEqualTokens(unscoped_token, unscoped_token_2) + + def test_auth_unscoped_token_tenant(self): + """Verify getting a token in a tenant with an unscoped token""" + # Get an unscoped tenant + body_dict = _build_user_auth( + username='FOO', + password='foo2') + unscoped_token = self.api.authenticate({}, body_dict) + # Get a token on BAR tenant using the unscoped tenant + body_dict = _build_user_auth( + token=unscoped_token["access"]["token"], + tenant_name="BAR") + scoped_token = self.api.authenticate({}, body_dict) + + tenant = scoped_token["access"]["token"]["tenant"] + self.assertEquals(tenant["id"], self.tenant_bar['id']) + + +class AuthWithPasswordCredentials(AuthTest): + def setUp(self): + super(AuthWithPasswordCredentials, self).setUp() + + def test_auth_invalid_user(self): + """Verify exception is raised if invalid user""" + body_dict = _build_user_auth( + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + self.assertRaises( + exception.Unauthorized, + self.api.authenticate, + {}, body_dict) + + def test_auth_valid_user_invalid_password(self): + """Verify exception is raised if invalid password""" + body_dict = _build_user_auth( + username="FOO", + password=uuid.uuid4().hex) + self.assertRaises( + exception.Unauthorized, + self.api.authenticate, + {}, body_dict) + + def test_auth_empty_password(self): + """Verify exception is raised if empty password""" + body_dict = _build_user_auth( + username="FOO", + password="") + self.assertRaises( + exception.Unauthorized, + self.api.authenticate, + {}, body_dict) + + def test_auth_no_password(self): + """Verify exception is raised if empty password""" + body_dict = _build_user_auth(username="FOO") + self.assertRaises( + exception.ValidationError, + self.api.authenticate, + {}, body_dict) + + def test_authenticate_blank_password_credentials(self): + """Verify sending empty json dict as passwordCredentials raises the + right exception.""" + body_dict = {'passwordCredentials': {}, 'tenantName': 'demo'} + self.assertRaises(exception.ValidationError, self.api.authenticate, + {}, body_dict) + + def test_authenticate_no_username(self): + """Verify skipping username raises the right exception.""" + body_dict = _build_user_auth(password="pass", + tenant_name="demo") + self.assertRaises(exception.ValidationError, self.api.authenticate, + {}, body_dict) + + +class AuthWithRemoteUser(AuthTest): + def setUp(self): + super(AuthWithRemoteUser, self).setUp() + + def test_unscoped_remote_authn(self): + """Verify getting an unscoped token with external authn""" + body_dict = _build_user_auth( + username='FOO', + password='foo2') + local_token = self.api.authenticate( + {}, body_dict) + + body_dict = _build_user_auth() + remote_token = self.api.authenticate( + {'REMOTE_USER': 'FOO'}, body_dict) + + self.assertEqualTokens(local_token, remote_token) + + def test_unscoped_remote_authn_jsonless(self): + """Verify that external auth with invalid request fails""" + self.assertRaises( + exception.ValidationError, + self.api.authenticate, + {'REMOTE_USER': 'FOO'}, + None) + + def test_scoped_remote_authn(self): + """Verify getting a token with external authn""" + body_dict = _build_user_auth( + username='FOO', + password='foo2', + tenant_name='BAR') + local_token = self.api.authenticate( + {}, body_dict) + + body_dict = _build_user_auth( + tenant_name='BAR') + remote_token = self.api.authenticate( + {'REMOTE_USER': 'FOO'}, body_dict) + + self.assertEqualTokens(local_token, remote_token) + + def test_scoped_nometa_remote_authn(self): + """Verify getting a token with external authn and no metadata""" + body_dict = _build_user_auth( + username='TWO', + password='two2', + tenant_name='BAZ') + local_token = self.api.authenticate( + {}, body_dict) + + body_dict = _build_user_auth(tenant_name='BAZ') + remote_token = self.api.authenticate( + {'REMOTE_USER': 'TWO'}, body_dict) + + self.assertEqualTokens(local_token, remote_token) + + def test_scoped_remote_authn_invalid_user(self): + """Verify that external auth with invalid user fails""" + body_dict = _build_user_auth(tenant_name="BAR") + self.assertRaises( + exception.Unauthorized, + self.api.authenticate, + {'REMOTE_USER': uuid.uuid4().hex}, + body_dict) + + +class TokenExpirationTest(AuthTest): + def _maintain_token_expiration(self): + """Token expiration should be maintained after re-auth & validation.""" + r = self.api.authenticate( + {}, + auth={ + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'] + } + }) + unscoped_token_id = r['access']['token']['id'] + original_expiration = r['access']['token']['expires'] + + time.sleep(0.5) + + r = self.api.validate_token( + dict(is_admin=True, query_string={}), + token_id=unscoped_token_id) + self.assertEqual(original_expiration, r['access']['token']['expires']) + + time.sleep(0.5) + + r = self.api.authenticate( + {}, + auth={ + 'token': { + 'id': unscoped_token_id, + }, + 'tenantId': self.tenant_bar['id'], + }) + scoped_token_id = r['access']['token']['id'] + self.assertEqual(original_expiration, r['access']['token']['expires']) + + time.sleep(0.5) + + r = self.api.validate_token( + dict(is_admin=True, query_string={}), + token_id=scoped_token_id) + self.assertEqual(original_expiration, r['access']['token']['expires']) + + def test_maintain_uuid_token_expiration(self): + self.opt_in_group('signing', token_format='UUID') + self._maintain_token_expiration() diff --git a/tests/test_service.py b/tests/test_service.py deleted file mode 100644 index c256e7e0..00000000 --- a/tests/test_service.py +++ /dev/null @@ -1,355 +0,0 @@ -# Copyright 2012 OpenStack LLC -# -# 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 time -import uuid - -import default_fixtures - -from keystone import config -from keystone import exception -from keystone import identity -from keystone import service -from keystone import test -from keystone.identity.backends import kvs as kvs_identity -from keystone.openstack.common import timeutils - - -CONF = config.CONF - - -def _build_user_auth(token=None, username=None, - password=None, tenant_name=None): - """Build auth dictionary. - - It will create an auth dictionary based on all the arguments - that it receives. - """ - auth_json = {} - if token is not None: - auth_json['token'] = token - if username or password: - auth_json['passwordCredentials'] = {} - if username is not None: - auth_json['passwordCredentials']['username'] = username - if password is not None: - auth_json['passwordCredentials']['password'] = password - if tenant_name is not None: - auth_json['tenantName'] = tenant_name - return auth_json - - -class TokenControllerTest(test.TestCase): - def setUp(self): - super(TokenControllerTest, self).setUp() - self.identity_api = kvs_identity.Identity() - self.load_fixtures(default_fixtures) - self.api = service.TokenController() - - def assertEqualTokens(self, a, b): - """Assert that two tokens are equal. - - Compare two tokens except for their ids. This also truncates - the time in the comparison. - """ - def normalize(token): - token['access']['token']['id'] = 'dummy' - del token['access']['token']['expires'] - del token['access']['token']['issued_at'] - return token - - self.assertCloseEnoughForGovernmentWork( - timeutils.parse_isotime(a['access']['token']['expires']), - timeutils.parse_isotime(b['access']['token']['expires'])) - self.assertCloseEnoughForGovernmentWork( - timeutils.parse_isotime(a['access']['token']['issued_at']), - timeutils.parse_isotime(b['access']['token']['issued_at'])) - return self.assertDictEqual(normalize(a), normalize(b)) - - -class AuthBadRequests(TokenControllerTest): - def setUp(self): - super(AuthBadRequests, self).setUp() - - def test_no_external_auth(self): - """Verify that _authenticate_external() raises exception if - not applicable""" - self.assertRaises( - service.ExternalAuthNotApplicable, - self.api._authenticate_external, - {}, {}) - - def test_no_token_in_auth(self): - """Verity that _authenticate_token() raises exception if no token""" - self.assertRaises( - exception.ValidationError, - self.api._authenticate_token, - None, {}) - - def test_no_credentials_in_auth(self): - """Verity that _authenticate_local() raises exception if no creds""" - self.assertRaises( - exception.ValidationError, - self.api._authenticate_local, - None, {}) - - def test_authenticate_blank_request_body(self): - """Verify sending empty json dict raises the right exception.""" - self.assertRaises(exception.ValidationError, self.api.authenticate, - {}, {}) - - def test_authenticate_blank_auth(self): - """Verify sending blank 'auth' raises the right exception.""" - body_dict = _build_user_auth() - self.assertRaises(exception.ValidationError, self.api.authenticate, - {}, body_dict) - - def test_authenticate_invalid_auth_content(self): - """Verify sending invalid 'auth' raises the right exception.""" - self.assertRaises(exception.ValidationError, self.api.authenticate, - {}, {'auth': 'abcd'}) - - -class AuthWithToken(TokenControllerTest): - def setUp(self): - super(AuthWithToken, self).setUp() - - def test_unscoped_token(self): - """Verify getting an unscoped token with password creds""" - body_dict = _build_user_auth(username='FOO', - password='foo2') - unscoped_token = self.api.authenticate({}, body_dict) - tenant = unscoped_token["access"]["token"].get("tenant", None) - self.assertEqual(tenant, None) - - def test_auth_invalid_token(self): - """Verify exception is raised if invalid token""" - body_dict = _build_user_auth(token={"id": uuid.uuid4().hex}) - self.assertRaises( - exception.Unauthorized, - self.api.authenticate, - {}, body_dict) - - def test_auth_bad_formatted_token(self): - """Verify exception is raised if invalid token""" - body_dict = _build_user_auth(token={}) - self.assertRaises( - exception.ValidationError, - self.api.authenticate, - {}, body_dict) - - def test_auth_unscoped_token_no_tenant(self): - """Verify getting an unscoped token with an unscoped token""" - body_dict = _build_user_auth( - username='FOO', - password='foo2') - unscoped_token = self.api.authenticate({}, body_dict) - - body_dict = _build_user_auth( - token=unscoped_token["access"]["token"]) - unscoped_token_2 = self.api.authenticate({}, body_dict) - - self.assertEqualTokens(unscoped_token, unscoped_token_2) - - def test_auth_unscoped_token_tenant(self): - """Verify getting a token in a tenant with an unscoped token""" - # Get an unscoped tenant - body_dict = _build_user_auth( - username='FOO', - password='foo2') - unscoped_token = self.api.authenticate({}, body_dict) - # Get a token on BAR tenant using the unscoped tenant - body_dict = _build_user_auth( - token=unscoped_token["access"]["token"], - tenant_name="BAR") - scoped_token = self.api.authenticate({}, body_dict) - - tenant = scoped_token["access"]["token"]["tenant"] - self.assertEquals(tenant["id"], self.tenant_bar['id']) - - -class AuthWithPasswordCredentials(TokenControllerTest): - def setUp(self): - super(AuthWithPasswordCredentials, self).setUp() - - def test_auth_invalid_user(self): - """Verify exception is raised if invalid user""" - body_dict = _build_user_auth( - username=uuid.uuid4().hex, - password=uuid.uuid4().hex) - self.assertRaises( - exception.Unauthorized, - self.api.authenticate, - {}, body_dict) - - def test_auth_valid_user_invalid_password(self): - """Verify exception is raised if invalid password""" - body_dict = _build_user_auth( - username="FOO", - password=uuid.uuid4().hex) - self.assertRaises( - exception.Unauthorized, - self.api.authenticate, - {}, body_dict) - - def test_auth_empty_password(self): - """Verify exception is raised if empty password""" - body_dict = _build_user_auth( - username="FOO", - password="") - self.assertRaises( - exception.Unauthorized, - self.api.authenticate, - {}, body_dict) - - def test_auth_no_password(self): - """Verify exception is raised if empty password""" - body_dict = _build_user_auth(username="FOO") - self.assertRaises( - exception.ValidationError, - self.api.authenticate, - {}, body_dict) - - def test_authenticate_blank_password_credentials(self): - """Verify sending empty json dict as passwordCredentials raises the - right exception.""" - body_dict = {'passwordCredentials': {}, 'tenantName': 'demo'} - self.assertRaises(exception.ValidationError, self.api.authenticate, - {}, body_dict) - - def test_authenticate_no_username(self): - """Verify skipping username raises the right exception.""" - body_dict = _build_user_auth(password="pass", - tenant_name="demo") - self.assertRaises(exception.ValidationError, self.api.authenticate, - {}, body_dict) - - -class AuthWithRemoteUser(TokenControllerTest): - def setUp(self): - super(AuthWithRemoteUser, self).setUp() - - def test_unscoped_remote_authn(self): - """Verify getting an unscoped token with external authn""" - body_dict = _build_user_auth( - username='FOO', - password='foo2') - local_token = self.api.authenticate( - {}, body_dict) - - body_dict = _build_user_auth() - remote_token = self.api.authenticate( - {'REMOTE_USER': 'FOO'}, body_dict) - - self.assertEqualTokens(local_token, remote_token) - - def test_unscoped_remote_authn_jsonless(self): - """Verify that external auth with invalid request fails""" - self.assertRaises( - exception.ValidationError, - self.api.authenticate, - {'REMOTE_USER': 'FOO'}, - None) - - def test_scoped_remote_authn(self): - """Verify getting a token with external authn""" - body_dict = _build_user_auth( - username='FOO', - password='foo2', - tenant_name='BAR') - local_token = self.api.authenticate( - {}, body_dict) - - body_dict = _build_user_auth( - tenant_name='BAR') - remote_token = self.api.authenticate( - {'REMOTE_USER': 'FOO'}, body_dict) - - self.assertEqualTokens(local_token, remote_token) - - def test_scoped_nometa_remote_authn(self): - """Verify getting a token with external authn and no metadata""" - body_dict = _build_user_auth( - username='TWO', - password='two2', - tenant_name='BAZ') - local_token = self.api.authenticate( - {}, body_dict) - - body_dict = _build_user_auth(tenant_name='BAZ') - remote_token = self.api.authenticate( - {'REMOTE_USER': 'TWO'}, body_dict) - - self.assertEqualTokens(local_token, remote_token) - - def test_scoped_remote_authn_invalid_user(self): - """Verify that external auth with invalid user fails""" - body_dict = _build_user_auth(tenant_name="BAR") - self.assertRaises( - exception.Unauthorized, - self.api.authenticate, - {'REMOTE_USER': uuid.uuid4().hex}, - body_dict) - - -class TokenExpirationTest(test.TestCase): - def setUp(self): - super(TokenExpirationTest, self).setUp() - self.identity_api = kvs_identity.Identity() - self.load_fixtures(default_fixtures) - self.api = service.TokenController() - - def _maintain_token_expiration(self): - """Token expiration should be maintained after re-auth & validation.""" - r = self.api.authenticate( - {}, - auth={ - 'passwordCredentials': { - 'username': self.user_foo['name'], - 'password': self.user_foo['password'] - } - }) - unscoped_token_id = r['access']['token']['id'] - original_expiration = r['access']['token']['expires'] - - time.sleep(0.5) - - r = self.api.validate_token( - dict(is_admin=True, query_string={}), - token_id=unscoped_token_id) - self.assertEqual(original_expiration, r['access']['token']['expires']) - - time.sleep(0.5) - - r = self.api.authenticate( - {}, - auth={ - 'token': { - 'id': unscoped_token_id, - }, - 'tenantId': self.tenant_bar['id'], - }) - scoped_token_id = r['access']['token']['id'] - self.assertEqual(original_expiration, r['access']['token']['expires']) - - time.sleep(0.5) - - r = self.api.validate_token( - dict(is_admin=True, query_string={}), - token_id=scoped_token_id) - self.assertEqual(original_expiration, r['access']['token']['expires']) - - def test_maintain_uuid_token_expiration(self): - self.opt_in_group('signing', token_format='UUID') - self._maintain_token_expiration() -- cgit