diff options
| author | Dolph Mathews <dolph.mathews@gmail.com> | 2012-12-11 14:40:27 -0600 |
|---|---|---|
| committer | Dolph Mathews <dolph.mathews@gmail.com> | 2012-12-12 07:47:46 -0600 |
| commit | 4e2be8a8880f03b1c6d1dc663d7259dbb45ddf67 (patch) | |
| tree | 58fc05007116a591f96c8e19288d0aade27a1581 | |
| parent | 6397580a52be5288b4cb5e0a86a8c340fe4fd0ae (diff) | |
| download | keystone-4e2be8a8880f03b1c6d1dc663d7259dbb45ddf67.tar.gz keystone-4e2be8a8880f03b1c6d1dc663d7259dbb45ddf67.tar.xz keystone-4e2be8a8880f03b1c6d1dc663d7259dbb45ddf67.zip | |
Move token controller into keystone.token
Change-Id: Ie8277529185f645854e0aebaafa173c06a7c5164
| -rw-r--r-- | keystone/common/controller.py | 12 | ||||
| -rw-r--r-- | keystone/contrib/ec2/core.py | 5 | ||||
| -rw-r--r-- | keystone/service.py | 556 | ||||
| -rw-r--r-- | keystone/token/__init__.py | 1 | ||||
| -rw-r--r-- | keystone/token/controllers.py | 545 | ||||
| -rw-r--r-- | tests/test_auth.py (renamed from tests/test_service.py) | 37 |
6 files changed, 589 insertions, 567 deletions
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_service.py b/tests/test_auth.py index c256e7e0..c0dbbd2c 100644 --- a/tests/test_service.py +++ b/tests/test_auth.py @@ -17,13 +17,15 @@ import uuid import default_fixtures +from keystone import catalog 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 +from keystone import policy +from keystone import test +from keystone import token CONF = config.CONF @@ -50,12 +52,19 @@ def _build_user_auth(token=None, username=None, return auth_json -class TokenControllerTest(test.TestCase): +class AuthTest(test.TestCase): def setUp(self): - super(TokenControllerTest, self).setUp() + 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 = service.TokenController() + + 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. @@ -78,7 +87,7 @@ class TokenControllerTest(test.TestCase): return self.assertDictEqual(normalize(a), normalize(b)) -class AuthBadRequests(TokenControllerTest): +class AuthBadRequests(AuthTest): def setUp(self): super(AuthBadRequests, self).setUp() @@ -86,7 +95,7 @@ class AuthBadRequests(TokenControllerTest): """Verify that _authenticate_external() raises exception if not applicable""" self.assertRaises( - service.ExternalAuthNotApplicable, + token.controllers.ExternalAuthNotApplicable, self.api._authenticate_external, {}, {}) @@ -121,7 +130,7 @@ class AuthBadRequests(TokenControllerTest): {}, {'auth': 'abcd'}) -class AuthWithToken(TokenControllerTest): +class AuthWithToken(AuthTest): def setUp(self): super(AuthWithToken, self).setUp() @@ -179,7 +188,7 @@ class AuthWithToken(TokenControllerTest): self.assertEquals(tenant["id"], self.tenant_bar['id']) -class AuthWithPasswordCredentials(TokenControllerTest): +class AuthWithPasswordCredentials(AuthTest): def setUp(self): super(AuthWithPasswordCredentials, self).setUp() @@ -236,7 +245,7 @@ class AuthWithPasswordCredentials(TokenControllerTest): {}, body_dict) -class AuthWithRemoteUser(TokenControllerTest): +class AuthWithRemoteUser(AuthTest): def setUp(self): super(AuthWithRemoteUser, self).setUp() @@ -303,13 +312,7 @@ class AuthWithRemoteUser(TokenControllerTest): 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() - +class TokenExpirationTest(AuthTest): def _maintain_token_expiration(self): """Token expiration should be maintained after re-auth & validation.""" r = self.api.authenticate( |
