From 601eeb50b60a2e99041690fe19238202bc203503 Mon Sep 17 00:00:00 2001 From: Adam Young Date: Tue, 26 Feb 2013 14:54:32 -0500 Subject: Trusts Blueprint trusts creates a trust. Using a trust, one user (the trustee), can then create tokens with a subset of another user's (the trustor) roles and projects. If the impersonate flag in the trust is set, the token user_id is set to the trustor's user ID If the impersonate flag is not set, the token's user_is is set to the trustee's user ID check that both trustor and trustee are enabled prior to creating the trust token. sql and kvs backends sql upgrade scripts unit tests for backends, auth and v3 api modifications to the trust controller for creating tokens Authenticates that only user can be trustor in create Deleting a trust invalidates all tokens created from that trust Adds the trust id and the id of the trustee to the header of the token policy rules for trust This version has a workaround for testing against the KVS version of the Service catalog Change-Id: I5745f4d9a4180b59671a143a55ed87019e98ec76 --- etc/policy.json | 20 +- keystone/auth/controllers.py | 85 ++++-- keystone/auth/methods/token.py | 2 + keystone/auth/token_factory.py | 91 +++++-- keystone/common/controller.py | 3 +- keystone/common/models.py | 15 ++ .../migrate_repo/versions/018_add_trust_tables.py | 68 +++++ keystone/config.py | 2 + keystone/exception.py | 4 + keystone/identity/backends/sql.py | 17 +- keystone/identity/controllers.py | 35 ++- keystone/service.py | 6 +- keystone/test.py | 2 + keystone/token/backends/kvs.py | 16 +- keystone/token/backends/memcache.py | 11 +- keystone/token/backends/sql.py | 20 +- keystone/token/controllers.py | 71 ++++- keystone/token/core.py | 6 +- keystone/trust/__init__.py | 19 ++ keystone/trust/backends/__init__.py | 0 keystone/trust/backends/kvs.py | 92 +++++++ keystone/trust/backends/sql.py | 123 +++++++++ keystone/trust/controllers.py | 244 ++++++++++++++++++ keystone/trust/core.py | 63 +++++ keystone/trust/routers.py | 58 +++++ tests/backend_sql.conf | 3 + tests/default_fixtures.py | 7 + tests/test_auth.py | 244 +++++++++++++++++- tests/test_backend.py | 88 ++++++- tests/test_backend_kvs.py | 10 + tests/test_backend_sql.py | 8 + tests/test_content_types.py | 2 +- tests/test_overrides.conf | 3 + tests/test_sql_upgrade.py | 12 + tests/test_v3.py | 8 + tests/test_v3_trust.py | 286 +++++++++++++++++++++ 36 files changed, 1645 insertions(+), 99 deletions(-) create mode 100644 keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py create mode 100644 keystone/trust/__init__.py create mode 100644 keystone/trust/backends/__init__.py create mode 100644 keystone/trust/backends/kvs.py create mode 100644 keystone/trust/backends/sql.py create mode 100644 keystone/trust/controllers.py create mode 100644 keystone/trust/core.py create mode 100644 keystone/trust/routers.py create mode 100644 tests/test_v3_trust.py diff --git a/etc/policy.json b/etc/policy.json index a0e77fc2..89365e5e 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -1,5 +1,9 @@ { "admin_required": [["role:admin"], ["is_admin:1"]], + "owner" : [["user_id:%(user_id)s"]], + "admin_or_owner": [["rule:admin_required"], ["rule:owner"]], + + "default": [["rule:admin_required"]], "identity:get_service": [["rule:admin_required"]], "identity:list_services": [["rule:admin_required"]], @@ -21,8 +25,9 @@ "identity:get_project": [["rule:admin_required"]], "identity:list_projects": [["rule:admin_required"]], - "identity:list_user_projects": [["rule:admin_required"], ["user_id:%(user_id)s"]], - "identity:create_project": [["rule:admin_required"]], + "identity:list_user_projects": [["rule:admin_required"], + ["user_id:%(user_id)s"]], + "identity:create_project": [["rule:admin_or_owner"]], "identity:update_project": [["rule:admin_required"]], "identity:delete_project": [["rule:admin_required"]], @@ -68,5 +73,14 @@ "identity:check_token": [["rule:admin_required"]], "identity:validate_token": [["rule:admin_required"]], "identity:revocation_list": [["rule:admin_required"]], - "identity:revoke_token": [["rule:admin_required"], ["user_id:%(user_id)s"]] + "identity:revoke_token": [["rule:admin_required"], + ["user_id:%(user_id)s"]], + + "identity:create_trust": [["user_id:%(trust.trustor_user_id)s"]], + "identity:get_trust": [["rule:admin_or_owner"]], + "identity:list_trusts": [["@"]], + "identity:list_roles_for_trust": [["@"]], + "identity:check_role_for_trust": [["@"]], + "identity:get_role_for_trust": [["@"]], + "identity:delete_trust": [["@"]] } diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py index 3760aa3a..96f3dc0c 100644 --- a/keystone/auth/controllers.py +++ b/keystone/auth/controllers.py @@ -24,6 +24,7 @@ from keystone import config from keystone import exception from keystone import identity from keystone import token +from keystone import trust from keystone.openstack.common import importutils @@ -63,13 +64,15 @@ class AuthInfo(object): def __init__(self, context, auth=None): self.identity_api = identity.Manager() + self.trust_api = trust.Manager() self.context = context self.auth = auth - self._scope_data = (None, None) - # self._scope_data is (domain_id, project_id) - # project scope: (None, project_id) - # domain scope: (domain_id, None) - # unscoped: (None, None) + self._scope_data = (None, None, None) + # self._scope_data is (domain_id, project_id, trust_ref) + # project scope: (None, project_id, None) + # domain scope: (domain_id, None, None) + # trust scope: (None, None, trust_id) + # unscoped: (None, None, None) self._validate_and_normalize_auth_data() def _assert_project_is_enabled(self, project_ref): @@ -136,6 +139,16 @@ class AuthInfo(object): self._assert_project_is_enabled(project_ref) return project_ref + def _lookup_trust(self, trust_info): + trust_id = trust_info.get('id') + if not trust_id: + raise exception.ValidationError(attribute='trust_id', + target='trust') + trust = self.trust_api.get_trust(self.context, trust_id) + if not trust: + raise exception.TrustNotFound(trust_id) + return trust + def lookup_user(self, user_info): user_id = user_info.get('id') user_name = user_info.get('name') @@ -165,25 +178,28 @@ class AuthInfo(object): """ Validate and normalize scope data """ if 'scope' not in self.auth: return - - # if scoped, only to a project or domain, but not both - if ('project' not in self.auth['scope'] and - 'domain' not in self.auth['scope']): - # neither domain or project provided - raise exception.ValidationError(attribute='project or domain', - target='scope') - if ('project' in self.auth['scope'] and - 'domain' in self.auth['scope']): - # both domain and project provided - raise exception.ValidationError(attribute='project or domain', - target='scope') + if sum(['project' in self.auth['scope'], + 'domain' in self.auth['scope'], + 'trust' in self.auth['scope']]) != 1: + raise exception.ValidationError( + attribute='project, domain, or trust', + target='scope') if 'project' in self.auth['scope']: project_ref = self._lookup_project(self.auth['scope']['project']) - self._scope_data = (None, project_ref['id']) - else: + self._scope_data = (None, project_ref['id'], None) + elif 'domain' in self.auth['scope']: domain_ref = self._lookup_domain(self.auth['scope']['domain']) - self._scope_data = (domain_ref['id'], None) + self._scope_data = (domain_ref['id'], None, None) + elif 'trust' in self.auth['scope']: + trust_ref = self._lookup_trust(self.auth['scope']['trust']) + #TODO ayoung when trusts support domain, Fill in domain data here + if 'project_id' in trust_ref: + project_ref = self._lookup_project( + {'id': trust_ref['project_id']}) + self._scope_data = (None, project_ref['id'], trust_ref) + else: + self._scope_data = (None, None, trust_ref) def _validate_auth_methods(self): # make sure auth methods are provided @@ -236,20 +252,31 @@ class AuthInfo(object): Verify and return the scoping information. - :returns: (domain_id, project_id). If scope to a project, - (None, project_id) will be returned. If scope to a domain, - (domain_id, None) will be returned. If unscope, - (None, None) will be returned. + :returns: (domain_id, project_id, trust_ref). + If scope to a project, (None, project_id, None) + will be returned. + If scoped to a domain, (domain_id, None,None) + will be returned. + If scoped to a trust, (None, project_id, trust_ref), + Will be returned, where the project_id comes from the + trust definition. + If unscoped, (None, None, None) will be returned. """ return self._scope_data - def set_scope(self, domain_id=None, project_id=None): + def set_scope(self, domain_id=None, project_id=None, trust=None): """ Set scope information. """ if domain_id and project_id: msg = _('Scoping to both domain and project is not allowed') raise ValueError(msg) - self._scope_data = (domain_id, project_id) + if domain_id and trust: + msg = _('Scoping to both domain and trust is not allowed') + raise ValueError(msg) + if project_id and trust: + msg = _('Scoping to both project and trust is not allowed') + raise ValueError(msg) + self._scope_data = (domain_id, project_id, trust) class Auth(controller.V3Controller): @@ -278,8 +305,10 @@ class Auth(controller.V3Controller): raise exception.Unauthorized(e) def _check_and_set_default_scoping(self, context, auth_info, auth_context): - (domain_id, project_id) = auth_info.get_scope() - if domain_id or project_id: + (domain_id, project_id, trust) = auth_info.get_scope() + if trust: + project_id = trust['project_id'] + if domain_id or project_id or trust: # scope is specified return diff --git a/keystone/auth/methods/token.py b/keystone/auth/methods/token.py index 10e99cfb..bb7b8d58 100644 --- a/keystone/auth/methods/token.py +++ b/keystone/auth/methods/token.py @@ -48,6 +48,8 @@ class Token(auth.AuthMethodHandler): token_ref['token_data']['token']['extras']) user_context['method_names'].extend( token_ref['token_data']['token']['methods']) + if 'trust' in token_ref['token_data']: + raise exception.Forbidden(e) except AssertionError as e: LOG.error(e) raise exception.Unauthorized(e) diff --git a/keystone/auth/token_factory.py b/keystone/auth/token_factory.py index d6dc68f9..8460aec6 100644 --- a/keystone/auth/token_factory.py +++ b/keystone/auth/token_factory.py @@ -28,6 +28,7 @@ from keystone import config from keystone import exception from keystone import identity from keystone import token as token_module +from keystone import trust from keystone.openstack.common import jsonutils from keystone.openstack.common import timeutils @@ -42,6 +43,7 @@ class TokenDataHelper(object): def __init__(self, context): self.identity_api = identity.Manager() self.catalog_api = catalog.Manager() + self.trust_api = trust.Manager() self.context = context def _get_filtered_domain(self, domain_id): @@ -100,33 +102,77 @@ class TokenDataHelper(object): roles = self._get_project_roles_for_user(user_id, project_id) return roles - def _populate_user(self, token_data, user_id, domain_id, project_id): + def _populate_user(self, token_data, user_id, domain_id, project_id, + trust): user_ref = self.identity_api.get_user(self.context, user_id) + if trust: + trustor_user_ref = (self.identity_api.get_user(self.context, + trust['trustor_user_id'])) + if not trustor_user_ref['enabled']: + raise exception.Forbidden() + if trust['impersonation']: + user_ref = trustor_user_ref + token_data['trust'] = ( + { + 'id': trust['id'], + 'trustor_user': {'id': trust['trustor_user_id']}, + 'trustee_user': {'id': trust['trustee_user_id']}, + 'impersonation': trust['impersonation'] + }) filtered_user = { 'id': user_ref['id'], 'name': user_ref['name'], 'domain': self._get_filtered_domain(user_ref['domain_id'])} token_data['user'] = filtered_user - def _populate_roles(self, token_data, user_id, domain_id, project_id): - if domain_id or project_id: - roles = self._get_roles_for_user(user_id, domain_id, project_id) - # we only care about id and name + def _populate_roles(self, token_data, user_id, domain_id, project_id, + trust): + if trust: + token_user_id = trust['trustor_user_id'] + token_project_id = trust['project_id'] + #trusts do not support domains yet + token_domain_id = None + else: + token_user_id = user_id + token_project_id = project_id + token_domain_id = domain_id + + if token_domain_id or token_project_id: + roles = self._get_roles_for_user(token_user_id, + token_domain_id, + token_project_id) filtered_roles = [] - for role in roles: - filtered_roles.append({'id': role['id'], 'name': role['name']}) + if trust: + for trust_role in trust['roles']: + match_roles = [x for x in roles + if x['id'] == trust_role['id']] + if match_roles: + filtered_roles.append(match_roles[0]) + else: + raise exception.Forbidden() + else: + for role in roles: + filtered_roles.append({'id': role['id'], + 'name': role['name']}) token_data['roles'] = filtered_roles def _populate_service_catalog(self, token_data, user_id, - domain_id, project_id): + domain_id, project_id, trust): + if trust: + user_id = trust['trustor_user_id'] if project_id or domain_id: - service_catalog = self.catalog_api.get_v3_catalog( - self.context, user_id, project_id) + try: + service_catalog = self.catalog_api.get_v3_catalog( + self.context, user_id, project_id) + #TODO KVS backend needs a sample implementation + except exception.NotImplemented: + service_catalog = {} # TODO(gyee): v3 service catalog is not quite completed yet + #TODO Enforce Endpoints for trust token_data['catalog'] = service_catalog - def _populate_token(self, token_data, expires=None): + def _populate_token(self, token_data, expires=None, trust=None): if not expires: expires = token_module.default_expire_time() if not isinstance(expires, basestring): @@ -135,15 +181,20 @@ class TokenDataHelper(object): token_data['issued_at'] = timeutils.isotime(subsecond=True) def get_token_data(self, user_id, method_names, extras, - domain_id=None, project_id=None, expires=None): + domain_id=None, project_id=None, expires=None, + trust=None): token_data = {'methods': method_names, 'extras': extras} + if trust: + if user_id != trust['trustee_user_id']: + raise exception.Forbidden() + self._populate_scope(token_data, domain_id, project_id) - self._populate_user(token_data, user_id, domain_id, project_id) - self._populate_roles(token_data, user_id, domain_id, project_id) + self._populate_user(token_data, user_id, domain_id, project_id, trust) + self._populate_roles(token_data, user_id, domain_id, project_id, trust) self._populate_service_catalog(token_data, user_id, domain_id, - project_id) - self._populate_token(token_data, expires) + project_id, trust) + self._populate_token(token_data, expires, trust) return {'token': token_data} @@ -189,7 +240,7 @@ def recreate_token_data(context, token_data=None, expires=None, def create_token(context, auth_context, auth_info): token_data_helper = TokenDataHelper(context) - (domain_id, project_id) = auth_info.get_scope() + (domain_id, project_id, trust) = auth_info.get_scope() method_names = list(set(auth_info.get_method_names() + auth_context.get('method_names', []))) token_data = token_data_helper.get_token_data( @@ -198,7 +249,9 @@ def create_token(context, auth_context, auth_info): auth_context['extras'], domain_id, project_id, - auth_context.get('expires_at', None)) + auth_context.get('expires_at', None), + trust) + if CONF.signing.token_format == 'UUID': token_id = uuid.uuid4().hex elif CONF.signing.token_format == 'PKI': @@ -214,7 +267,7 @@ def create_token(context, auth_context, auth_info): try: expiry = token_data['token']['expires_at'] if isinstance(expiry, basestring): - expiry = timeutils.parse_isotime(expiry) + expiry = timeutils.normalize_time(timeutils.parse_isotime(expiry)) role_ids = [] if 'project' in token_data['token']: # project-scoped token, fill in the v2 token data diff --git a/keystone/common/controller.py b/keystone/common/controller.py index c9300002..f4f3c79d 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -148,7 +148,8 @@ def filterprotected(*filters): return _filterprotected -@dependency.requires('identity_api', 'policy_api', 'token_api', 'catalog_api') +@dependency.requires('identity_api', 'policy_api', 'token_api', + 'trust_api', 'catalog_api') class V2Controller(wsgi.Application): """Base controller class for Identity API v2.""" diff --git a/keystone/common/models.py b/keystone/common/models.py index f572d382..86a327c2 100644 --- a/keystone/common/models.py +++ b/keystone/common/models.py @@ -42,6 +42,7 @@ class Token(Model): user tenant metadata + trust_id """ required_keys = ('id', 'expires') @@ -147,3 +148,17 @@ class Role(Model): required_keys = ('id', 'name') optional_keys = tuple() + + +class Trust(Model): + """Trust object. + + Required keys: + id + trustor_user_id + trustee_user_id + project_id + """ + + required_keys = ('id', 'trustor_user_id', 'trustee_user_id', 'project_id') + optional_keys = tuple('expires_at') diff --git a/keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py b/keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py new file mode 100644 index 00000000..77c42ba1 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/018_add_trust_tables.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 migrate +import sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + user_table = sql.Table('user', meta, autoload=True) + role_table = sql.Table('role', meta, autoload=True) + tenant_table = sql.Table('project', meta, autoload=True) + + trust_table = sql.Table( + 'trust', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('trustor_user_id', + sql.String(64), + unique=False, + nullable=False,), + sql.Column('trustee_user_id', + sql.String(64), + unique=False, + nullable=False), + sql.Column('project_id', sql.String(64), + unique=False, + nullable=True), + sql.Column("impersonation", sql.types.Boolean, nullable=False), + sql.Column("deleted_at", sql.types.DateTime, nullable=True), + sql.Column("expires_at", sql.types.DateTime, nullable=True), + sql.Column('extra', sql.Text())) + trust_table.create(migrate_engine, checkfirst=True) + + trust_role_table = sql.Table( + 'trust_role', + meta, + sql.Column('trust_id', sql.String(64), primary_key=True, + nullable=False), + sql.Column('role_id', sql.String(64), primary_key=True, + nullable=False)) + trust_role_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + # Operations to reverse the above upgrade go here. + for table_name in ['trust_role', 'trust']: + table = sql.Table(table_name, meta, autoload=True) + table.drop() diff --git a/keystone/config.py b/keystone/config.py index eca692f1..4c319778 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -235,6 +235,8 @@ register_str('driver', group='policy', default='keystone.policy.backends.sql.Policy') register_str('driver', group='token', default='keystone.token.backends.kvs.Token') +register_str('driver', group='trust', + default='keystone.trust.backends.sql.Trust') register_str('driver', group='ec2', default='keystone.contrib.ec2.backends.kvs.Ec2') register_str('driver', group='stats', diff --git a/keystone/exception.py b/keystone/exception.py index 017db27f..09c43585 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -190,6 +190,10 @@ class GroupNotFound(NotFound): """Could not find group: %(group_id)s""" +class TrustNotFound(NotFound): + """Could not find trust: %(trust_id)s""" + + class Conflict(Error): """Conflict occurred attempting to store %(type)s. diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index e6f07c2e..f36002ce 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -14,8 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import functools - from keystone import clean from keystone import config from keystone.common import sql @@ -195,18 +193,7 @@ class Identity(sql.Base, identity.Driver): # FIXME(gyee): this should really be # get_roles_for_user_and_project() after the dusts settle if tenant_id not in self.get_projects_for_user(user_id): - # get_roles_for_user_and_project() returns a set - roles = [] - try: - roles = self.get_roles_for_user_and_project(user_id, - tenant_id) - except: - # FIXME(gyee): we should never get into this situation - # after user project role migration is completed - pass - if not roles: - raise AssertionError('Invalid tenant') - + raise AssertionError('Invalid project') try: tenant_ref = self.get_project(tenant_id) metadata_ref = self.get_metadata(user_id, tenant_id) @@ -215,7 +202,6 @@ class Identity(sql.Base, identity.Driver): metadata_ref = {} except exception.MetadataNotFound: metadata_ref = {} - return (identity.filter_user(user_ref), tenant_ref, metadata_ref) def get_project(self, tenant_id): @@ -622,6 +608,7 @@ class Identity(sql.Base, identity.Driver): raise exception.DomainNotFound(domain_id=domain_id) return ref.to_dict() + @sql.handle_conflicts(type='domain') def get_domain_by_name(self, domain_name): session = self.get_session() try: diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index e5f30e56..e8472987 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -161,6 +161,24 @@ class Tenant(controller.V2Controller): return o +def delete_tokens_for_user(token_api, trust_api, context, user_id, user): + try: + #First delete tokens that could get other tokens. + for token_id in token_api.list_tokens(context, user_id): + token_api.delete_token(context, token_id) + #now delete trust tokens + for trust_id in (trust_api.list_trusts_for_trustee(context, user_id)): + token_list = token_api.list_tokens(context, userid, + trust_id=trust_id) + for token in token_list: + token_api.delete_token(context, token) + except exception.NotImplemented: + # The users status has been changed but tokens remain valid for + # backends that can't list tokens for users + LOG.warning('User %s status has changed, but existing tokens ' + 'remain valid' % user_id) + + class User(controller.V2Controller): def get_user(self, context, user_id): self.assert_admin(context) @@ -215,16 +233,12 @@ class User(controller.V2Controller): self.assert_admin(context) user_ref = self.identity_api.update_user(context, user_id, user) - # If the password was changed or the user was disabled we clear tokens if user.get('password') or not user.get('enabled', True): - try: - for token_id in self.token_api.list_tokens(context, user_id): - self.token_api.delete_token(context, token_id) - except exception.NotImplemented: - # The users status has been changed but tokens remain valid for - # backends that can't list tokens for users - LOG.warning('User %s status has changed, but existing tokens ' - 'remain valid' % user_id) + # If the password was changed or the user was disabled we clear tokens + delete_tokens_for_user(self.token_api, self.trust_api, + context, + user_id, + user) return {'user': self._filter_domain_id(user_ref)} def delete_user(self, context, user_id): @@ -329,7 +343,8 @@ class Role(controller.V2Controller): context, user_id, tenant_id, role_id) roles = self.identity_api.get_roles_for_user_and_project( context, user_id, tenant_id) - self.token_api.revoke_tokens(context, user_id, tenant_id) + delete_tokens_for_user(self.token_api, self.trust_api, context, + user_id, tenant_id) # COMPAT(diablo): CRUD extension def get_role_refs(self, context, user_id): diff --git a/keystone/service.py b/keystone/service.py index e4cca53e..423aee06 100644 --- a/keystone/service.py +++ b/keystone/service.py @@ -25,6 +25,7 @@ from keystone import identity from keystone import policy from keystone import routers from keystone import token +from keystone import trust LOG = logging.getLogger(__name__) @@ -34,7 +35,8 @@ DRIVERS = dict( ec2_api=ec2.Manager(), identity_api=identity.Manager(), policy_api=policy.Manager(), - token_api=token.Manager()) + token_api=token.Manager(), + trust_api=trust.Manager()) @logging.fail_gracefully @@ -81,7 +83,7 @@ def v3_app_factory(global_conf, **local_conf): conf.update(local_conf) mapper = routes.Mapper() v3routers = [] - for module in [auth, catalog, identity, policy]: + for module in [auth, catalog, identity, policy, trust]: module.routers.append_v3_routers(mapper, v3routers) # TODO(ayoung): put token routes here return wsgi.ComposingRouter(mapper, v3routers) diff --git a/keystone/test.py b/keystone/test.py index 2972314f..7386f552 100644 --- a/keystone/test.py +++ b/keystone/test.py @@ -39,6 +39,7 @@ from keystone import exception from keystone import identity from keystone import policy from keystone import token +from keystone import trust do_monkeypatch = not os.getenv('STANDARD_THREADS') @@ -74,6 +75,7 @@ def initialize_drivers(): DRIVERS['identity_api'] = identity.Manager() DRIVERS['policy_api'] = policy.Manager() DRIVERS['token_api'] = token.Manager() + DRIVERS['trust_api'] = trust.Manager() return DRIVERS diff --git a/keystone/token/backends/kvs.py b/keystone/token/backends/kvs.py index f12fe80d..0adf0579 100644 --- a/keystone/token/backends/kvs.py +++ b/keystone/token/backends/kvs.py @@ -31,7 +31,11 @@ class Token(kvs.Base, token.Driver): ref = self.db.get('token-%s' % token_id) except exception.NotFound: raise exception.TokenNotFound(token_id=token_id) - if ref['expires'] is None or ref['expires'] > timeutils.utcnow(): + now = timeutils.utcnow() + expiry = ref['expires'] + if expiry is None: + raise exception.TokenNotFound(token_id=token_id) + if expiry > now: return copy.deepcopy(ref) else: raise exception.TokenNotFound(token_id=token_id) @@ -39,8 +43,10 @@ class Token(kvs.Base, token.Driver): def create_token(self, token_id, data): token_id = token.unique_id(token_id) data_copy = copy.deepcopy(data) - if 'expires' not in data: + if not data_copy.get('expires'): data_copy['expires'] = token.default_expire_time() + if 'trust_id' in data and data['trust_id'] is None: + data_copy.pop('trust_id') self.db.set('token-%s' % token_id, data_copy) return copy.deepcopy(data_copy) @@ -53,7 +59,7 @@ class Token(kvs.Base, token.Driver): except exception.NotFound: raise exception.TokenNotFound(token_id=token_id) - def list_tokens(self, user_id, tenant_id=None): + def list_tokens(self, user_id, tenant_id=None, trust_id=None): tokens = [] now = timeutils.utcnow() for token, ref in self.db.items(): @@ -72,6 +78,10 @@ class Token(kvs.Base, token.Driver): continue if tenant.get('id') != tenant_id: continue + if trust_id is not None: + trust = ref.get('trust_id') + if not trust: + continue tokens.append(token.split('-', 1)[1]) return tokens diff --git a/keystone/token/backends/memcache.py b/keystone/token/backends/memcache.py index efac16fd..b097ab5e 100644 --- a/keystone/token/backends/memcache.py +++ b/keystone/token/backends/memcache.py @@ -64,7 +64,7 @@ class Token(token.Driver): def create_token(self, token_id, data): data_copy = copy.deepcopy(data) ptk = self._prefix_token_id(token.unique_id(token_id)) - if 'expires' not in data_copy: + if not data_copy.get('expires'): data_copy['expires'] = token.default_expire_time() kwargs = {} if data_copy['expires'] is not None: @@ -99,7 +99,7 @@ class Token(token.Driver): self._add_to_revocation_list(data) return result - def list_tokens(self, user_id, tenant_id=None): + def list_tokens(self, user_id, tenant_id=None, trust_id=None): tokens = [] user_key = self._prefix_user_id(user_id) user_record = self.client.get(user_key) or "" @@ -114,6 +114,13 @@ class Token(token.Driver): continue if tenant.get('id') != tenant_id: continue + if trust_id is not None: + trust = token_ref.get('trust_id') + if not trust: + continue + if trust != trust_id: + continue + tokens.append(token_id) return tokens diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py index 822b869e..62122ebe 100644 --- a/keystone/token/backends/sql.py +++ b/keystone/token/backends/sql.py @@ -43,16 +43,18 @@ class Token(sql.Base, token.Driver): query = query.filter_by(id=token.unique_id(token_id), valid=True) token_ref = query.first() now = datetime.datetime.utcnow() - if token_ref and (not token_ref.expires or now < token_ref.expires): - return token_ref.to_dict() - else: + if not token_ref: raise exception.TokenNotFound(token_id=token_id) + if not token_ref.expires: + raise exception.TokenNotFound(token_id=token_id) + if now >= token_ref.expires: + raise exception.TokenNotFound(token_id=token_id) + return token_ref.to_dict() def create_token(self, token_id, data): data_copy = copy.deepcopy(data) - if 'expires' not in data_copy: + if not data_copy.get('expires'): data_copy['expires'] = token.default_expire_time() - token_ref = TokenModel.from_dict(data_copy) token_ref.id = token.unique_id(token_id) token_ref.valid = True @@ -73,7 +75,7 @@ class Token(sql.Base, token.Driver): token_ref.valid = False session.flush() - def list_tokens(self, user_id, tenant_id=None): + def list_tokens(self, user_id, tenant_id=None, trust_id=None): session = self.get_session() tokens = [] now = timeutils.utcnow() @@ -93,6 +95,12 @@ class Token(sql.Base, token.Driver): continue if tenant.get('id') != tenant_id: continue + if trust_id is not None: + token_trust_id = token_ref_dict.get('trust_id') + if not token_trust_id: + continue + if token_trust_id != trust_id: + continue tokens.append(token_ref['id']) return tokens diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py index d0538098..ade2af4f 100644 --- a/keystone/token/controllers.py +++ b/keystone/token/controllers.py @@ -21,7 +21,7 @@ class ExternalAuthNotApplicable(Exception): pass -@dependency.requires('catalog_api') +@dependency.requires('catalog_api', 'trust_api', 'token_api') class Auth(controller.V2Controller): def ca_cert(self, context, auth=None): ca_file = open(CONF.signing.ca_certs, 'r') @@ -78,6 +78,7 @@ class Auth(controller.V2Controller): context, auth) user_ref, tenant_ref, metadata_ref, expiry = auth_info + trust_id = metadata_ref.get('trust_id') user_ref = self._filter_domain_id(user_ref) if tenant_ref: tenant_ref = self._filter_domain_id(tenant_ref) @@ -128,7 +129,8 @@ class Auth(controller.V2Controller): expires=auth_token_data['expires'], user=user_ref, tenant=tenant_ref, - metadata=metadata_ref)) + metadata=metadata_ref, + trust_id=trust_id)) except Exception as e: # an identical token may have been created already. # if so, return the token_data as it is also identical @@ -166,11 +168,43 @@ class Auth(controller.V2Controller): except exception.NotFound as e: raise exception.Unauthorized(e) + #A trust token cannot be used to get another token + if 'trust' in old_token_ref: + raise exception.Unauthorized() + if 'trust_id' in old_token_ref["metadata"]: + raise exception.Forbidden() + user_ref = old_token_ref['user'] user_id = user_ref['id'] + if 'trust_id' in auth: + trust_ref = self.trust_api.get_trust(context, auth['trust_id']) + if trust_ref is None: + raise exception.Forbidden() + if user_id != trust_ref['trustee_user_id']: + raise exception.Forbidden() + if ('expires' in trust_ref) and (trust_ref['expires']): + expiry = trust_ref['expires'] + if expiry < timeutils.parse_isotime(timeutils.isotime()): + raise exception.Forbidden()() + user_id = trust_ref['trustor_user_id'] + trustor_user_ref = (self.identity_api.get_user( + context=context, + user_id=trust_ref['trustor_user_id'])) + if not trustor_user_ref['enabled']: + raise exception.Forbidden()() + trustee_user_ref = self.identity_api.get_user( + context, trust_ref['trustee_user_id']) + if not trustee_user_ref['enabled']: + raise exception.Forbidden()() + if trust_ref['impersonation'] == 'True': + current_user_ref = trustor_user_ref + else: + current_user_ref = trustee_user_ref - current_user_ref = self.identity_api.get_user(context=context, - user_id=user_id) + else: + tenant_id = self._get_project_id_from_auth(context, auth) + current_user_ref = self.identity_api.get_user(context=context, + user_id=user_id) tenant_id = self._get_project_id_from_auth(context, auth) @@ -185,6 +219,28 @@ class Auth(controller.V2Controller): context, user_id, tenant_id)) expiry = old_token_ref['expires'] + if 'trust_id' in auth: + trust_id = auth['trust_id'] + trust_roles = [] + for role in trust_ref['roles']: + if not 'roles' in metadata_ref: + raise exception.Forbidden()() + if role['id'] in metadata_ref['roles']: + trust_roles.append(role['id']) + else: + raise exception.Forbidden() + if 'expiry' in trust_ref and trust_ref['expiry']: + trust_expiry = timeutils.parse_isotime(trust_ref['expiry']) + if trust_expiry < expiry: + expiry = trust_expiry + metadata_ref['roles'] = trust_roles + metadata_ref['trustee_user_id'] = trust_ref['trustee_user_id'] + metadata_ref['trust_id'] = trust_id + + auth_token_data = self._get_auth_token_data(current_user_ref, + tenant_ref, + metadata_ref, + expiry) return (current_user_ref, tenant_ref, metadata_ref, expiry) def _authenticate_local(self, context, auth): @@ -526,7 +582,12 @@ class Auth(controller.V2Controller): else: o['access']['metadata'] = {'is_admin': 0} if 'roles' in metadata_ref: - o['access']['metadata']['roles'] = metadata_ref['roles'] + o['access']['metadata']['roles'] = metadata_ref['roles'] + if 'trust_id' in metadata_ref: + o['access']['trust'] = {'trustee_user_id': + metadata_ref['trustee_user_id'], + 'id': metadata_ref['trust_id'] + } return o @classmethod diff --git a/keystone/token/core.py b/keystone/token/core.py index 4737f539..37ecffbc 100644 --- a/keystone/token/core.py +++ b/keystone/token/core.py @@ -179,11 +179,15 @@ class Driver(object): """ raise exception.NotImplemented() - def list_tokens(self, user_id): + def list_tokens(self, user_id, tenant_id=None, trust_id=None): """Returns a list of current token_id's for a user :param user_id: identity of the user :type user_id: string + :param tenant_id: identity of the tenant + :type tenant_id: string + :param trust_id: identified of the trust + :type trust_id: string :returns: list of token_id's """ diff --git a/keystone/trust/__init__.py b/keystone/trust/__init__.py new file mode 100644 index 00000000..9c6a22f0 --- /dev/null +++ b/keystone/trust/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +from keystone.trust.core import Manager, Driver +from keystone.trust import controllers +from keystone.trust import routers diff --git a/keystone/trust/backends/__init__.py b/keystone/trust/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone/trust/backends/kvs.py b/keystone/trust/backends/kvs.py new file mode 100644 index 00000000..ef528626 --- /dev/null +++ b/keystone/trust/backends/kvs.py @@ -0,0 +1,92 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. +""" +An in memory implementation of the trusts API. +only to be used for testing purposes +""" +import copy +import datetime + + +from keystone.common import kvs +from keystone.openstack.common import timeutils +from keystone import exception +from keystone import trust + + +class Trust(kvs.Base, trust.Driver): + def create_trust(self, trust_id, trust, roles): + trust_ref = trust + trust_ref['id'] = trust_id + trust_ref['deleted'] = False + trust_ref['roles'] = roles + if (trust_ref.get('expires_at') and + trust_ref['expires_at'].tzinfo is not None): + trust_ref['expires_at'] = (timeutils.normalize_time + (trust_ref['expires_at'])) + + self.db.set('trust-%s' % trust_id, trust_ref) + trustee_user_id = trust_ref['trustee_user_id'] + trustee_list = self.db.get('trustee-%s' % trustee_user_id, []) + trustee_list.append(trust_id) + self.db.set('trustee-%s' % trustee_user_id, trustee_list) + trustor_user_id = trust_ref['trustor_user_id'] + trustor_list = self.db.get('trustor-%s' % trustor_user_id, []) + trustor_list.append(trust_id) + self.db.set('trustor-%s' % trustor_user_id, trustor_list) + return copy.deepcopy(trust_ref) + + def _filter_trust(selfself, ref): + if ref['deleted']: + return None + if ref.get('expires_at') and timeutils.utcnow() > ref['expires_at']: + return None + ref = copy.deepcopy(ref) + return ref + + def get_trust(self, trust_id): + try: + ref = self.db.get('trust-%s' % trust_id) + return self._filter_trust(ref) + except exception.NotFound: + return None + + def delete_trust(self, trust_id): + try: + ref = self.db.get('trust-%s' % trust_id) + except exception.NotFound: + raise exception.TrustNotFound(token_id=token_id) + ref['deleted'] = True + self.db.set('trust-%s' % trust_id, ref) + + def list_trusts(self): + trusts = [] + for key, value in self.db.items(): + if key.startswith("trust-") and not value['deleted']: + trusts.append(value) + return trusts + + def list_trusts_for_trustee(self, trustee_user_id): + trusts = [] + for trust in self.db.get('trustee-%s' % trustee_user_id, []): + trusts.append(self.get_trust(trust)) + return trusts + + def list_trusts_for_trustor(self, trustor_user_id): + trusts = [] + for trust in self.db.get('trustor-%s' % trustor_user_id, []): + trusts.append(self.get_trust(trust)) + return trusts diff --git a/keystone/trust/backends/sql.py b/keystone/trust/backends/sql.py new file mode 100644 index 00000000..dc3644e3 --- /dev/null +++ b/keystone/trust/backends/sql.py @@ -0,0 +1,123 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +from keystone.common import sql +from keystone import exception +from keystone.openstack.common import timeutils +from keystone import trust + + +class TrustModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'trust' + attributes = ['id', 'trustor_user_id', 'trustee_user_id', + 'project_id', 'impersonation', 'expires_at'] + id = sql.Column(sql.String(64), primary_key=True) + #user id Of owner + trustor_user_id = sql.Column(sql.String(64), unique=False, nullable=False,) + #user_id of user allowed to consume this preauth + trustee_user_id = sql.Column(sql.String(64), unique=False, nullable=False) + project_id = sql.Column(sql.String(64), unique=False, nullable=True) + impersonation = sql.Column(sql.Boolean) + deleted_at = sql.Column(sql.DateTime) + expires_at = sql.Column(sql.DateTime) + extra = sql.Column(sql.JsonBlob()) + + +class TrustRole(sql.ModelBase): + __tablename__ = 'trust_role' + attributes = ['trust_id', 'role_id'] + trust_id = sql.Column(sql.String(64), primary_key=True, nullable=False) + role_id = sql.Column(sql.String(64), primary_key=True, nullable=False) + + +class Trust(sql.Base, trust.Driver): + @sql.handle_conflicts(type='trust') + def create_trust(self, trust_id, trust, roles): + session = self.get_session() + with session.begin(): + ref = TrustModel.from_dict(trust) + ref['id'] = trust_id + if ref.get('expires_at') and ref['expires_at'].tzinfo is not None: + ref['expires_at'] = timeutils.normalize_time(ref['expires_at']) + session.add(ref) + added_roles = [] + for role in roles: + trust_role = TrustRole() + trust_role.trust_id = trust_id + trust_role.role_id = role['id'] + added_roles.append({'id': role['id']}) + session.add(trust_role) + session.flush() + trust_dict = ref.to_dict() + trust_dict['roles'] = added_roles + return trust_dict + + def _add_roles(self, trust_id, session, trust_dict): + roles = [] + for role in session.query(TrustRole).filter_by(trust_id=trust_id): + roles.append({'id': role.role_id}) + trust_dict['roles'] = roles + + @sql.handle_conflicts(type='trust') + def get_trust(self, trust_id): + session = self.get_session() + ref = (session.query(TrustModel). + filter_by(deleted_at=None). + filter_by(id=trust_id).first()) + if ref is None: + return None + if ref.expires_at is not None: + now = timeutils.utcnow() + if now > ref.expires_at: + return None + trust_dict = ref.to_dict() + + self._add_roles(trust_id, session, trust_dict) + return trust_dict + + @sql.handle_conflicts(type='trust') + def list_trusts(self): + session = self.get_session() + trusts = session.query(TrustModel).filter_by(deleted_at=None) + return [trust_ref.to_dict() for trust_ref in trusts] + + @sql.handle_conflicts(type='trust') + def list_trusts_for_trustee(self, trustee_user_id): + session = self.get_session() + trusts = (session.query(TrustModel). + filter_by(deleted_at=None). + filter_by(trustee_user_id=trustee_user_id)) + return [trust_ref.to_dict() for trust_ref in trusts] + + @sql.handle_conflicts(type='trust') + def list_trusts_for_trustor(self, trustor_user_id): + session = self.get_session() + trusts = (session.query(TrustModel). + filter_by(deleted_at=None). + filter_by(trustor_user_id=trustor_user_id)) + return [trust_ref.to_dict() for trust_ref in trusts] + + @sql.handle_conflicts(type='trust') + def delete_trust(self, trust_id): + session = self.get_session() + with session.begin(): + try: + trust_ref = (session.query(TrustModel). + filter_by(id=trust_id).one()) + except sql.NotFound: + raise exception.TrustNotFound(trust_id=trust_id) + trust_ref.deleted_at = timeutils.utcnow() + session.flush() diff --git a/keystone/trust/controllers.py b/keystone/trust/controllers.py new file mode 100644 index 00000000..00183bc5 --- /dev/null +++ b/keystone/trust/controllers.py @@ -0,0 +1,244 @@ +import uuid +import json + +from keystone import config +from keystone import exception +from keystone import identity +from keystone.common import controller +from keystone.common import dependency +from keystone.common import logging +from keystone import exception +from keystone.openstack.common import timeutils + + +LOG = logging.getLogger(__name__) +CONF = config.CONF + + +def _trustor_only(context, trust, user_id): + if user_id != trust.get('trustor_user_id'): + raise exception.Forbidden() + + +def _admin_trustor_trustee_only(context, trust, user_id): + if (user_id != trust.get('trustor_user_id') and + user_id != trust.get('trustor_user_id') and + context['is_admin']): + raise exception.Forbidden() + + +def _admin_trustor_only(context, trust, user_id): + if user_id != trust.get('trustor_user_id') and not context['is_admin']: + raise exception.Forbidden() + + +@dependency.requires('identity_api', 'trust_api', 'token_api') +class TrustV3(controller.V3Controller): + collection_name = "trusts" + member_name = "trust" + + def _get_user_id(self, context): + if 'token_id' in context: + token_id = context['token_id'] + token = self.token_api.get_token(context, token_id) + user_id = token['user']['id'] + return user_id + return None + + def get_trust(self, context, trust_id): + user_id = self._get_user_id(context) + trust = self.trust_api.get_trust(context, trust_id) + if not trust: + raise exception.TrustNotFound(trust_id) + _admin_trustor_trustee_only(context, trust, user_id) + if not trust: + raise exception.TrustNotFound(trust_id=trust_id) + if (user_id != trust['trustor_user_id'] and + user_id != trust['trustee_user_id']): + raise exception.Forbidden() + self._fill_in_roles(context, trust, + self.identity_api.list_roles(context)) + return TrustV3.wrap_member(context, trust) + + def _fill_in_roles(self, context, trust, global_roles): + if trust.get('expires_at') is not None: + trust['expires_at'] = (timeutils.isotime + (trust['expires_at'], + subsecond=True)) + + if not 'roles' in trust: + trust['roles'] = [] + trust_full_roles = [] + for trust_role in trust['roles']: + if isinstance(trust_role, basestring): + trust_role = {'id': trust_role} + matching_roles = [x for x in global_roles + if x['id'] == trust_role['id']] + if matching_roles: + full_role = identity.controllers.RoleV3.wrap_member( + context, matching_roles[0])['role'] + trust_full_roles.append(full_role) + trust['roles'] = trust_full_roles + trust['roles_links'] = { + 'self': (CONF.public_endpoint % CONF + + "trusts/%s/roles" % trust['id']), + 'next': None, + 'previous': None} + + def _clean_role_list(self, context, trust, global_roles): + trust_roles = [] + global_role_names = dict((r['name'], r) + for r in + global_roles) + for role in trust.get('roles', []): + if 'id' in role: + trust_roles.append({'id': role['id']}) + elif 'name' in role: + rolename = role['name'] + if rolename in global_role_names: + trust_roles.append({'id': + global_role_names[rolename]['id']}) + else: + raise exception.RoleNotFound("role %s is not defined" % + rolename) + else: + raise exception.ValidationError(attribute='id or name', + target='roles') + return trust_roles + + @controller.protected + def create_trust(self, context, trust=None): + """ + The user creating the trust must be trustor + """ + + #TODO instead of raising ValidationError on the first problem, + #return a collection of all the problems. + if not trust: + raise exception.ValidationError(attribute='trust', + target='request') + try: + user_id = self._get_user_id(context) + _trustor_only(context, trust, user_id) + #confirm that the trustee exists + trustee_ref = self.identity_api.get_user(context, + trust['trustee_user_id']) + if not trustee_ref: + raise exception.UserNotFound(user_id=trust['trustee_user_id']) + global_roles = self.identity_api.list_roles(context) + clean_roles = self._clean_role_list(context, trust, global_roles) + if trust.get('project_id'): + user_roles = self.identity_api.get_roles_for_user_and_project( + context, user_id, trust['project_id']) + else: + user_roles = [] + for trust_role in clean_roles: + matching_roles = [x for x in user_roles + if x == trust_role['id']] + if not matching_roles: + raise exception.RoleNotFound(role_id=trust_role['id']) + if trust.get('expires_at') is not None: + if not trust['expires_at'].endswith('Z'): + trust['expires_at'] += 'Z' + trust['expires_at'] = (timeutils.parse_isotime + (trust['expires_at'])) + new_trust = self.trust_api.create_trust( + context=context, + trust_id=uuid.uuid4().hex, + trust=trust, + roles=clean_roles) + self._fill_in_roles(context, + new_trust, + global_roles) + return TrustV3.wrap_member(context, new_trust) + except KeyError as e: + raise exception.ValidationError(attribute=e.args[0], + target='trust') + + @controller.protected + def list_trusts(self, context): + query = context['query_string'] + trusts = [] + if not query: + self.assert_admin(context) + trusts += self.trust_api.list_trusts(context) + if 'trustor_user_id' in query: + user_id = query['trustor_user_id'] + calling_user_id = self._get_user_id(context) + if user_id != calling_user_id: + raise exception.Forbidden() + trusts += (self.trust_api. + list_trusts_for_trustor(context, user_id)) + if 'trustee_user_id' in query: + user_id = query['trustee_user_id'] + calling_user_id = self._get_user_id(context) + if user_id != calling_user_id: + raise exception.Forbidden() + trusts += (self.trust_api. + list_trusts_for_trustee(context, user_id)) + global_roles = self.identity_api.list_roles(context) + for trust in trusts: + self._fill_in_roles(context, trust, global_roles) + return TrustV3.wrap_collection(context, trusts) + + @controller.protected + def delete_trust(self, context, trust_id): + trust = self.trust_api.get_trust(context, trust_id) + if not trust: + raise exception.TrustNotFound(trust_id) + + user_id = self._get_user_id(context) + _admin_trustor_only(context, trust, user_id) + self.trust_api.delete_trust(context, trust_id) + userid = trust['trustor_user_id'] + token_list = self.token_api.list_tokens(context, + userid, + trust_id=trust_id) + for token in token_list: + self.token_api.delete_token(context, token) + + @controller.protected + def list_roles_for_trust(self, context, trust_id): + trust = self.get_trust(context, trust_id)['trust'] + if not trust: + raise exception.TrustNotFound(trust_id) + user_id = self._get_user_id(context) + _admin_trustor_trustee_only(context, trust, user_id) + return {'roles': trust['roles'], + 'links': trust['roles_links']} + + @controller.protected + def check_role_for_trust(self, context, trust_id, role_id): + """Checks if a role has been assigned to a trust.""" + trust = self.trust_api.get_trust(context, trust_id) + if not trust: + raise exception.TrustNotFound(trust_id) + user_id = self._get_user_id(context) + _admin_trustor_trustee_only(context, trust, user_id) + matching_roles = [x for x in trust['roles'] + if x['id'] == role_id] + if not matching_roles: + raise exception.RoleNotFound(role_id=role_id) + + @controller.protected + def get_role_for_trust(self, context, trust_id, role_id): + """Checks if a role has been assigned to a trust.""" + trust = self.trust_api.get_trust(context, trust_id) + if not trust: + raise exception.TrustNotFound(trust_id) + + user_id = self._get_user_id(context) + _admin_trustor_trustee_only(context, trust, user_id) + matching_roles = [x for x in trust['roles'] + if x['id'] == role_id] + if not matching_roles: + raise exception.RoleNotFound(role_id=role_id) + global_roles = self.identity_api.list_roles(context) + matching_roles = [x for x in global_roles + if x['id'] == role_id] + if matching_roles: + full_role = (identity.controllers. + RoleV3.wrap_member(context, matching_roles[0])) + return full_role + else: + raise exception.RoleNotFound(role_id=role_id) diff --git a/keystone/trust/core.py b/keystone/trust/core.py new file mode 100644 index 00000000..a9a15ff7 --- /dev/null +++ b/keystone/trust/core.py @@ -0,0 +1,63 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +"""Main entry point into the Identity service.""" + +from keystone.common import dependency +from keystone.common import logging +from keystone.common import manager +from keystone.common import wsgi +from keystone import config +from keystone import exception + + +CONF = config.CONF + +LOG = logging.getLogger(__name__) + + +@dependency.provider('trust_api') +class Manager(manager.Manager): + """Default pivot point for the Trust backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + def __init__(self): + super(Manager, self).__init__(CONF.trust.driver) + + +class Driver(object): + def create_trust(self, trust_id, trust, roles): + """Create a new trust. + + :returns: a new trust + """ + raise exception.NotImplemented() + + def get_trust(self, trust_id): + raise exception.NotImplemented() + + def list_trusts(self): + raise exception.NotImplemented() + + def list_trusts_for_trustee(self, trustee): + raise exception.NotImplemented() + + def list_trusts_for_trustor(self, trustor): + raise exception.NotImplemented() diff --git a/keystone/trust/routers.py b/keystone/trust/routers.py new file mode 100644 index 00000000..2ed35ed0 --- /dev/null +++ b/keystone/trust/routers.py @@ -0,0 +1,58 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. +"""WSGI Routers for the Identity service.""" +from keystone.common import wsgi +from keystone.trust import controllers +from keystone.common import router + + +def append_v3_routers(mapper, routers): + trust_controller = controllers.TrustV3() + + mapper.connect('/trusts', + controller=trust_controller, + action='create_trust', + conditions=dict(method=['POST'])) + + mapper.connect('/trusts', + controller=trust_controller, + action='list_trusts', + conditions=dict(method=['GET'])) + + mapper.connect('/trusts/{trust_id}', + controller=trust_controller, + action='delete_trust', + conditions=dict(method=['DELETE'])) + + mapper.connect('/trusts/{trust_id}', + controller=trust_controller, + action='get_trust', + conditions=dict(method=['GET'])) + + mapper.connect('/trusts/{trust_id}/roles', + controller=trust_controller, + action='list_roles_for_trust', + conditions=dict(method=['GET'])) + + mapper.connect('/trusts/{trust_id}/roles/{role_id}', + controller=trust_controller, + action='check_role_for_trust', + conditions=dict(method=['HEAD'])) + + mapper.connect('/trusts/{trust_id}/roles/{role_id}', + controller=trust_controller, + action='get_role_for_trust', + conditions=dict(method=['GET'])) diff --git a/tests/backend_sql.conf b/tests/backend_sql.conf index e1027cc8..0baf610c 100644 --- a/tests/backend_sql.conf +++ b/tests/backend_sql.conf @@ -22,3 +22,6 @@ driver = keystone.catalog.backends.sql.Catalog [policy] driver = keystone.policy.backends.sql.Policy + +[trust] +driver = keystone.trust.backends.sql.Trust diff --git a/tests/default_fixtures.py b/tests/default_fixtures.py index 4499be17..1141ddfd 100644 --- a/tests/default_fixtures.py +++ b/tests/default_fixtures.py @@ -109,5 +109,12 @@ ROLES = [ }, { 'id': 'other', 'name': 'Other', + }, { + 'id': 'browser', + 'name': 'Browser', + }, { + 'id': 'writer', + 'name': 'Writer', } + ] diff --git a/tests/test_auth.py b/tests/test_auth.py index ae9bdac3..8c8feba0 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -12,23 +12,29 @@ # License for the specific language governing permissions and limitations # under the License. +import copy +import datetime import time import uuid +from keystone import auth from keystone import config from keystone import exception from keystone.openstack.common import timeutils from keystone import test from keystone import token +from keystone import trust import default_fixtures CONF = config.CONF +TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' def _build_user_auth(token=None, user_id=None, username=None, - password=None, tenant_id=None, tenant_name=None): + password=None, tenant_id=None, tenant_name=None, + trust_id=None): """Build auth dictionary. It will create an auth dictionary based on all the arguments @@ -49,6 +55,8 @@ def _build_user_auth(token=None, user_id=None, username=None, auth_json['tenantName'] = tenant_name if tenant_id is not None: auth_json['tenantId'] = tenant_id + if trust_id is not None: + auth_json['trust_id'] = trust_id return auth_json @@ -452,6 +460,240 @@ class AuthWithRemoteUser(AuthTest): body_dict) +class AuthWithTrust(AuthTest): + def setUp(self): + super(AuthWithTrust, self).setUp() + + trust.Manager() + self.trust_controller = trust.controllers.TrustV3() + self.auth_v3_controller = auth.controllers.Auth() + self.trustor = self.user_foo + self.trustee = self.user_two + self.assigned_roles = [self.role_member['id'], + self.role_browser['id']] + for assigned_role in self.assigned_roles: + self.identity_api.add_role_to_user_and_project( + self.trustor['id'], self.tenant_bar['id'], assigned_role) + + self.sample_data = {'trustor_user_id': self.trustor['id'], + 'trustee_user_id': self.trustee['id'], + 'project_id': self.tenant_bar['id'], + 'impersonation': 'True', + 'roles': [{'id': self.role_browser['id']}, + {'name': self.role_member['name']}]} + expires_at = timeutils.strtime(timeutils.utcnow() + + datetime.timedelta(minutes=10), + fmt=TIME_FORMAT) + self.create_trust(expires_at=expires_at) + + def create_trust(self, expires_at=None, impersonation='True'): + username = self.trustor['name'], + password = 'foo2' + body_dict = _build_user_auth(username=username, password=password) + self.unscoped_token = self.controller.authenticate({}, body_dict) + context = {'token_id': self.unscoped_token['access']['token']['id']} + trust_data = copy.deepcopy(self.sample_data) + trust_data['expires_at'] = expires_at + trust_data['impersonation'] = impersonation + + self.new_trust = (self.trust_controller.create_trust + (context, trust=trust_data)['trust']) + + def build_v2_token_request(self, username, password): + body_dict = _build_user_auth(username=username, password=password) + self.unscoped_token = self.controller.authenticate({}, body_dict) + unscoped_token_id = self.unscoped_token['access']['token']['id'] + request_body = _build_user_auth(token={'id': unscoped_token_id}, + trust_id=self.new_trust['id'], + tenant_id=self.tenant_bar['id']) + return request_body + + def test_create_trust_bad_data_fails(self): + context = {'token_id': self.unscoped_token['access']['token']['id']} + bad_sample_data = {'trustor_user_id': self.trustor['id']} + + self.assertRaises(exception.ValidationError, + self.trust_controller.create_trust, + context, trust=bad_sample_data) + + def test_create_trust_no_roles(self): + self.new_trust = None + self.sample_data['roles'] = [] + self.create_trust() + self.assertEquals(self.new_trust['roles'], []) + + def test_create_trust(self): + self.assertEquals(self.new_trust['trustor_user_id'], + self.trustor['id']) + self.assertEquals(self.new_trust['trustee_user_id'], + self.trustee['id']) + role_ids = [self.role_browser['id'], self.role_member['id']] + self.assertTrue(timeutils.parse_strtime(self.new_trust['expires_at'], + fmt=TIME_FORMAT)) + + for role in self.new_trust['roles']: + self.assertIn(role['id'], role_ids) + + def test_get_trust(self): + context = {'token_id': self.unscoped_token['access']['token']['id']} + trust = self.trust_controller.get_trust(context, + self.new_trust['id'])['trust'] + self.assertEquals(trust['trustor_user_id'], + self.trustor['id']) + self.assertEquals(trust['trustee_user_id'], + self.trustee['id']) + role_ids = [self.role_browser['id'], self.role_member['id']] + for role in self.new_trust['roles']: + self.assertIn(role['id'], role_ids) + + def test_create_trust_no_impersonation(self): + self.create_trust(expires_at=None, impersonation='False') + self.assertEquals(self.new_trust['trustor_user_id'], + self.trustor['id']) + self.assertEquals(self.new_trust['trustee_user_id'], + self.trustee['id']) + self.assertEquals(self.new_trust['impersonation'], + 'False') + auth_response = self.fetch_v2_token_from_trust() + token_user = auth_response['access']['user'] + self.assertEquals(token_user['id'], + self.new_trust['trustee_user_id']) + + #TODO Endpoints + + def test_token_from_trust_wrong_user_fails(self): + new_trust = self.create_trust() + request_body = self.build_v2_token_request('FOO', 'foo2') + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def fetch_v2_token_from_trust(self): + request_body = self.build_v2_token_request('TWO', 'two2') + auth_response = self.controller.authenticate({}, request_body) + return auth_response + + def fetch_v3_token_from_trust(self): + self.identity_api.create_domain("default", + {"name": "default", + "id": "default"}) + v3_password_data = { + 'identity': { + "methods": ["password"], + "password": { + "user": { + "id": self.trustee["id"], + "password": self.trustee["password"]}} + }, + 'scope': { + 'project': { + 'id': self.tenant_baz['id']}}} + auth_response = (self.auth_v3_controller.authenticate_for_token + ({}, v3_password_data)) + token = auth_response.headers['X-Subject-Token'] + + v3_req_with_trust = { + "identity": { + "methods": ["token"], + "token": {"id": token}}, + "scope": { + "trust": {"id": self.new_trust['id']}}} + token_auth_response = (self.auth_v3_controller.authenticate_for_token + ({}, v3_req_with_trust)) + return token_auth_response + + def test_create_v3_token_from_trust(self): + auth_response = self.fetch_v3_token_from_trust() + + trust_token_user = auth_response.json['token']['user'] + self.assertEquals(trust_token_user['id'], self.trustor['id']) + + trust_token_trust = auth_response.json['token']['trust'] + self.assertEquals(trust_token_trust['id'], self.new_trust['id']) + self.assertEquals(trust_token_trust['trustor_user']['id'], + self.trustor['id']) + self.assertEquals(trust_token_trust['trustee_user']['id'], + self.trustee['id']) + + trust_token_roles = auth_response.json['token']['roles'] + self.assertEquals(len(trust_token_roles), 2) + + def test_v3_trust_token_get_token_fails(self): + auth_response = self.fetch_v3_token_from_trust() + trust_token = auth_response.headers['X-Subject-Token'] + v3_token_data = { + "methods": ["token"], + "token": {"id": trust_token} + } + self.assertRaises( + exception.Unauthorized, + self.auth_v3_controller.authenticate_for_token, + {}, v3_token_data) + + def test_token_from_trust(self): + auth_response = self.fetch_v2_token_from_trust() + + self.assertIsNotNone(auth_response) + self.assertEquals(len(auth_response['access']['metadata']['roles']), + 2, + "user_foo has three roles, but the token should" + " only get the two roles specified in the trust.") + + def test_token_from_trust_cant_get_another_token(self): + auth_response = self.fetch_v2_token_from_trust() + trust_token_id = auth_response['access']['token']['id'] + request_body = _build_user_auth(token={'id': trust_token_id}, + tenant_id=self.tenant_bar['id']) + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_delete_trust_revokes_token(self): + context = {'token_id': self.unscoped_token['access']['token']['id']} + auth_response = self.fetch_v2_token_from_trust() + trust_id = self.new_trust['id'] + trust_token_id = auth_response['access']['token']['id'] + tokens = self.token_api.list_tokens(self.trustor['id'], + trust_id=trust_id) + self.assertEquals(len(tokens), 1) + self.trust_controller.delete_trust(context, trust_id=trust_id) + tokens = self.token_api.list_tokens(self.trustor['id'], + trust_id=trust_id) + self.assertEquals(len(tokens), 0) + + def test_token_from_trust_with_no_role_fails(self): + for assigned_role in self.assigned_roles: + self.identity_api.remove_role_from_user_and_project( + self.trustor['id'], self.tenant_bar['id'], assigned_role) + request_body = self.build_v2_token_request('TWO', 'two2') + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_expired_trust_get_token_fails(self): + expiry = "1999-02-18T10:10:00Z" + self.create_trust(expiry) + request_body = self.build_v2_token_request('TWO', 'two2') + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_token_from_trust_with_wrong_role_fails(self): + self.identity_api.add_role_to_user_and_project( + self.trustor['id'], + self.tenant_bar['id'], + self.role_other['id']) + for assigned_role in self.assigned_roles: + self.identity_api.remove_role_from_user_and_project( + self.trustor['id'], self.tenant_bar['id'], assigned_role) + + request_body = self.build_v2_token_request('TWO', 'two2') + + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + class TokenExpirationTest(AuthTest): def _maintain_token_expiration(self): """Token expiration should be maintained after re-auth & validation.""" diff --git a/tests/test_backend.py b/tests/test_backend.py index 029901eb..1af0822c 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -29,6 +29,7 @@ from keystone import test CONF = config.CONF DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id +TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' class IdentityTests(object): @@ -1888,12 +1889,14 @@ class TokenTests(object): self.assertRaises(exception.TokenNotFound, self.token_api.delete_token, token_id) - def create_token_sample_data(self, tenant_id=None): + def create_token_sample_data(self, tenant_id=None, trust_id=None): token_id = uuid.uuid4().hex data = {'id': token_id, 'a': 'b', 'user': {'id': 'testuserid'}} if tenant_id is not None: data['tenant'] = {'id': tenant_id, 'name': tenant_id} + if trust_id is not None: + data['trust_id'] = trust_id self.token_api.create_token(token_id, data) return token_id @@ -1936,6 +1939,13 @@ class TokenTests(object): self.assertNotIn(token_id3, tokens) self.assertIn(token_id4, tokens) + def test_token_list_trust(self): + trust_id = uuid.uuid4().hex + token_id5 = self.create_token_sample_data(trust_id=trust_id) + tokens = self.token_api.list_tokens('testuserid', trust_id=trust_id) + self.assertEquals(len(tokens), 1) + self.assertIn(token_id5, tokens) + def test_get_token_404(self): self.assertRaises(exception.TokenNotFound, self.token_api.get_token, @@ -1965,7 +1975,7 @@ class TokenTests(object): data = {'id': token_id, 'id_hash': token_id, 'a': 'b', 'expires': None, 'user': {'id': 'testuserid'}} data_ref = self.token_api.create_token(token_id, data) - self.assertDictEqual(data_ref, data) + self.assertIsNotNone(data_ref['expires']) new_data_ref = self.token_api.get_token(token_id) self.assertEqual(data_ref, new_data_ref) @@ -2002,6 +2012,80 @@ class TokenTests(object): for x in xrange(2)]) +class TrustTests(object): + def create_sample_trust(self, new_id): + self.trustor = self.user_foo + self.trustee = self.user_two + trust_data = (self.trust_api.create_trust + (new_id, + {'trustor_user_id': self.trustor['id'], + 'trustee_user_id': self.user_two['id'], + 'project_id': self.tenant_bar['id'], + 'expires_at': timeutils. + parse_isotime('2031-02-18T18:10:00Z'), + 'impersonation': True}, + roles=[{"id": "member"}, + {"id": "other"}, + {"id": "browser"}])) + return trust_data + + def test_delete_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + trust_id = trust_data['id'] + self.assertIsNotNone(trust_data) + trust_data = self.trust_api.get_trust(trust_id) + self.assertEquals(new_id, trust_data['id']) + self.trust_api.delete_trust(trust_id) + self.assertIsNone(self.trust_api.get_trust(trust_id)) + + def test_get_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + trust_id = trust_data['id'] + self.assertIsNotNone(trust_data) + trust_data = self.trust_api.get_trust(trust_id) + self.assertEquals(new_id, trust_data['id']) + + def test_create_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + + self.assertEquals(new_id, trust_data['id']) + self.assertEquals(self.trustee['id'], trust_data['trustee_user_id']) + self.assertEquals(self.trustor['id'], trust_data['trustor_user_id']) + self.assertTrue(timeutils.normalize_time(trust_data['expires_at']) > + timeutils.utcnow()) + + self.assertEquals([{'id':'member'}, + {'id': 'other'}, + {'id': 'browser'}], trust_data['roles']) + + def test_list_trust_by_trustee(self): + for i in range(0, 3): + trust_data = self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts_for_trustee(self.trustee) + self.assertEqual(len(trusts), 3) + self.assertEqual(trusts[0]["trustee_user_id"], self.trustee['id']) + trusts = self.trust_api.list_trusts_for_trustee(self.trustor) + self.assertEqual(len(trusts), 0) + + def test_list_trust_by_trustee(self): + for i in range(0, 3): + trust_data = self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts_for_trustor(self.trustor['id']) + self.assertEqual(len(trusts), 3) + self.assertEqual(trusts[0]["trustor_user_id"], self.trustor['id']) + trusts = self.trust_api.list_trusts_for_trustor(self.trustee['id']) + self.assertEqual(len(trusts), 0) + + def test_list_trusts(self): + for i in range(0, 3): + trust_data = self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts() + self.assertEqual(len(trusts), 3) + + class CommonHelperTests(test.TestCase): def test_format_helper_raises_malformed_on_missing_key(self): with self.assertRaises(exception.MalformedEndpoint): diff --git a/tests/test_backend_kvs.py b/tests/test_backend_kvs.py index e1d99d47..74b5e4eb 100644 --- a/tests/test_backend_kvs.py +++ b/tests/test_backend_kvs.py @@ -22,6 +22,7 @@ from keystone import exception from keystone.identity.backends import kvs as identity_kvs from keystone import test from keystone.token.backends import kvs as token_kvs +from keystone.trust.backends import kvs as trust_kvs import default_fixtures import test_backend @@ -71,6 +72,15 @@ class KvsToken(test.TestCase, test_backend.TokenTests): self.token_api = token_kvs.Token(db={}) +class KvsTrust(test.TestCase, test_backend.TrustTests): + def setUp(self): + super(KvsTrust, self).setUp() + self.trust_api = trust_kvs.Trust(db={}) + self.identity_api = identity_kvs.Identity(db={}) + self.catalog_api = catalog_kvs.Catalog(db={}) + self.load_fixtures(default_fixtures) + + class KvsCatalog(test.TestCase, test_backend.CatalogTests): def setUp(self): super(KvsCatalog, self).setUp() diff --git a/tests/test_backend_sql.py b/tests/test_backend_sql.py index a4be85e3..04310307 100644 --- a/tests/test_backend_sql.py +++ b/tests/test_backend_sql.py @@ -24,6 +24,8 @@ from keystone import identity from keystone import policy from keystone import test from keystone import token +from keystone import trust + import default_fixtures import test_backend @@ -43,6 +45,7 @@ class SqlTests(test.TestCase): self.catalog_man = catalog.Manager() self.identity_man = identity.Manager() self.token_man = token.Manager() + self.trust_man = trust.Manager() self.policy_man = policy.Manager() # create shortcut references to each driver @@ -50,6 +53,7 @@ class SqlTests(test.TestCase): self.identity_api = self.identity_man.driver self.token_api = self.token_man.driver self.policy_api = self.policy_man.driver + self.trust_api = self.trust_man.driver # populate the engine with tables & fixtures self.load_fixtures(default_fixtures) @@ -221,6 +225,10 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key]) +class SqlTrust(SqlTests, test_backend.TrustTests): + pass + + class SqlToken(SqlTests, test_backend.TokenTests): pass diff --git a/tests/test_content_types.py b/tests/test_content_types.py index 91b61661..e0b42db9 100644 --- a/tests/test_content_types.py +++ b/tests/test_content_types.py @@ -90,7 +90,7 @@ class RestfulTestCase(test.TestCase): # Initialize headers dictionary headers = {} if not headers else headers - connection = httplib.HTTPConnection(host, port, timeout=10) + connection = httplib.HTTPConnection(host, port, timeout=100000) # Perform the request connection.request(method, path, body, headers) diff --git a/tests/test_overrides.conf b/tests/test_overrides.conf index 48f5dd7f..0e41fd32 100644 --- a/tests/test_overrides.conf +++ b/tests/test_overrides.conf @@ -8,6 +8,9 @@ driver = keystone.identity.backends.kvs.Identity driver = keystone.catalog.backends.templated.TemplatedCatalog template_file = default_catalog.templates +[trust] +driver = keystone.trust.backends.kvs.Trust + [signing] certfile = ../examples/pki/certs/signing_cert.pem keyfile = ../examples/pki/private/signing_key.pem diff --git a/tests/test_sql_upgrade.py b/tests/test_sql_upgrade.py index 85ea7580..003fb4ec 100644 --- a/tests/test_sql_upgrade.py +++ b/tests/test_sql_upgrade.py @@ -526,6 +526,18 @@ class SqlUpgradeTests(test.TestCase): cmd = this_table.delete(id=project['id']) self.engine.execute(cmd) + def test_upgrade_trusts(self): + self.assertEqual(self.schema.version, 0, "DB is at version 0") + self.upgrade(18) + self.assertTableColumns("trust", + ["id", "trustor_user_id", + "trustee_user_id", + "project_id", "impersonation", + "deleted_at", + "expires_at", "extra"]) + self.assertTableColumns("trust_role", + ["trust_id", "role_id"]) + def populate_user_table(self, with_pass_enab=False, with_pass_enab_domain=False): # Populate the appropriate fields in the user diff --git a/tests/test_v3.py b/tests/test_v3.py index a0252af0..2aa2c2b5 100644 --- a/tests/test_v3.py +++ b/tests/test_v3.py @@ -7,6 +7,8 @@ from keystone.common.sql import util as sql_util from keystone import auth from keystone import test from keystone import config +from keystone.policy.backends import rules + import test_content_types @@ -16,11 +18,14 @@ CONF = config.CONF class RestfulTestCase(test_content_types.RestfulTestCase): def setUp(self): + rules.reset() + self.config([ test.etcdir('keystone.conf.sample'), test.testsdir('test_overrides.conf'), test.testsdir('backend_sql.conf'), test.testsdir('backend_sql_disk.conf')]) + sql_util.setup_test_database() self.load_backends() @@ -62,6 +67,9 @@ class RestfulTestCase(test_content_types.RestfulTestCase): sql_util.teardown_test_database() # need to reset the plug-ins auth.controllers.AUTH_METHODS = {} + #drop the policy rules + CONF.reset() + rules.reset() def new_ref(self): """Populates a ref with attributes common to all API entities.""" diff --git a/tests/test_v3_trust.py b/tests/test_v3_trust.py new file mode 100644 index 00000000..19d645d8 --- /dev/null +++ b/tests/test_v3_trust.py @@ -0,0 +1,286 @@ +import copy +import uuid +import test_v3 +import json + +from keystone import config +from keystone.common.sql import util as sql_util +from keystone import test + +import test_content_types + + +CONF = config.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + + +class TrustTestCase(test_v3.RestfulTestCase): + def setUp(self): + super(TrustTestCase, self).setUp() + self.domain = None + self.password = 'freeipa4all' + self.auth_url = '/v2.0' + self.admin_url = '/v2.0' + self.admin_url_v3 = '/v3' + self.url_template = "%(auth_url)s/%(resource)s" + self.headers = {'Content-type': 'application/json'} + self.trustor = self.create_user() + self.trustee = self.create_user() + self.role_1 = self.create_role() + self.role_2 = self.create_role() + self.grant_role_to_user(self.trustor['id'], + self.role_1['id'], + self.get_project()['id']) + self.grant_role_to_user(self.trustor['id'], + self.role_2['id'], + self.get_project()['id']) + + def v3_request(self, path, data): + r = self.request(method='POST', + path=path, + body=data, + headers=self.headers) + return r + + def get_unscoped_token_response(self, username, password): + url = self.url_template % {'auth_url': self.admin_url, + 'resource': "tokens"} + data = self.get_unscoped_auth(username=username, password=password) + r = self.restful_request(method='POST', + port=self._public_port(), + path=url, + body=data, + headers=self.headers) + if 'access' in r.body: + return r.body['access'] + raise Exception(r) + + def get_scoped_token_response(self, username, password, project_name): + url = self.url_template % {'auth_url': self.admin_url, + 'resource': "tokens"} + data = self.get_scoped_auth(username, password, project_name) + r = self.restful_request(method='POST', + port=self._public_port(), + path=url, + body=data, + headers=self.headers) + if 'access' in r.body: + return r.body['access'] + raise Exception(r) + + def get_admin_token_data(self): + if not hasattr(self, 'admin_token_response'): + self.admin_token_response = self.get_scoped_token_response( + 'admin', 'freeipa4all', 'demo') + return self.admin_token_response + + def get_admin_token_id(self): + return 'ADMIN' + + def make_admin_post_request(self, resource, data): + return self.make_post_request(resource, + data, + self.get_admin_token_id()) + + def make_post_request(self, resource, data, token_id): + headers = copy.copy(self.headers) + headers["x-auth-token"] = token_id + url = self.url_template % {'auth_url': self.admin_url_v3, + 'resource': resource} + r = self.restful_request(method='POST', + path=url, + port=self._admin_port(), + body=data, + headers=headers) + return r + + def make_v2_post_request(self, resource, data, token_id): + headers = copy.copy(self.headers) + headers["x-auth-token"] = token_id + url = self.url_template % {'auth_url': self.admin_url, + 'resource': resource} + r = self.restful_request(method='POST', + path=url, + port=self._admin_port(), + body=data, + headers=headers) + return r + + def make_put_request(self, resource, data, token_id): + headers = copy.copy(self.headers) + headers["x-auth-token"] = self.get_admin_token_id() + url = self.url_template % {'auth_url': self.admin_url_v3, + 'resource': resource} + r = self.request(method='PUT', + path=url, + port=self._admin_port(), + body=json.dumps(data), + headers=headers) + return r + + def create_domain(self): + domain = self.new_domain_ref() + resource = 'domains' + data = {'domain': domain} + r = self.make_admin_post_request(resource, data) + dom = r.body['domain'] + self.domain = dom + + def create_project(self): + project = self.new_project_ref( + domain_id=self.get_domain()['id']) + data = {'project': project} + r = self.make_admin_post_request('projects', data) + self.project = r.body['project'] + + def get_domain(self): + if not self.domain: + #once authenticate supports domains, use the following function +# self.create_domain() + self.domain = {'id': DEFAULT_DOMAIN_ID} + return self.domain + + def get_project(self): + if not hasattr(self, 'project'): + self.create_project() + return self.project + + def create_user(self): + user_id = uuid.uuid4().hex + user = {'user': {'name': uuid.uuid4().hex, + 'password': self.password, + 'enabled': True, + 'domain_id': self.get_domain()['id'], + 'project_id': self.get_project()['id']}} + r = self.make_admin_post_request('users', user) + return r.body['user'] + + def create_role(self): + ref = self.new_role_ref() + body = {'role': ref} + r = self.make_admin_post_request('roles', body) + return r.body['role'] + + def grant_role_to_user(self, user_id, role_id, project_id): + """PUT /projects/{project_id}/users/{user_id}/roles/{role_id}""" + url_template = 'projects/%(project_id)s/users'\ + '/%(user_id)s/roles/%(role_id)s' + url = url_template % {'project_id': project_id, + 'user_id': user_id, + 'role_id': role_id} + r = self.make_put_request(url, '', self.get_admin_token_id()) + return r + + def get_scoped_auth(self, username, password, project_name): + return {"auth": + {"passwordCredentials": {"username": username, + "password": password}, + "projectName": project_name}} + + def get_unscoped_auth(self, username, password): + return {"auth": + {"passwordCredentials": {"username": username, + "password": password}}} + + def create_trust(self, impersonation=True): + trustor_token = self.get_scoped_token_response( + self.trustor['name'], + self.password, + self.get_project()['name']) + trustee_token = self.get_unscoped_token_response(self.trustee['name'], + self.password) + trust_request = {'trust': + {'trustor_user_id': self.trustor['id'], + 'trustee_user_id': self.trustee['id'], + 'project_id': self.get_project()['id'], + 'impersonation': impersonation, + 'description': 'described', + 'roles': []}} + trust_response = self.make_post_request('trusts', trust_request, + trustor_token['token']['id']) + return trust_response, trustee_token + + def test_create_trust(self): + trust_response, trustee_token = self.create_trust() + trust_id = trust_response.body['trust']['id'] + self.assertEquals(trust_response.body['trust']['description'], + 'described') + auth_data = {"auth": {"token": {'id': trustee_token['token']['id']}, + "trust_id": trust_id}} + r = self.make_v2_post_request("tokens", + auth_data, + trustee_token['token']['id']) + trust_token = r.body + self.assertIsNotNone(trust_token['access']['token']['id']) + self.assertEquals(trust_token['access']['trust']['trustee_user_id'], + self.trustee['id']) + self.assertEquals(trust_token['access']['trust']['id'], trust_id) + + def test_delete_trust(self): + trust_response, trustee_token = self.create_trust() + url = self.url_template % {'auth_url': self.admin_url_v3, + 'resource': "trusts/"} + url += trust_response.body['trust']['id'] + trustor_token = self.get_scoped_token_response( + self.trustor['name'], + self.password, + self.get_project()['name']) + + headers = copy.copy(self.headers) + headers["x-auth-token"] = trustor_token['token']['id'] + response = self.request(method='DELETE', + path=url, + port=self._public_port(), + body="", + headers=headers) + self.assertIsNotNone(response) + + def test_list_trusts(self): + trustor_token = self.get_scoped_token_response( + self.trustor['name'], + self.password, + self.get_project()['name']) + + for i in range(0, 3): + trust_response, trustee_token = self.create_trust() + url = self.url_template % {'auth_url': self.admin_url_v3, + 'resource': "trusts"} + headers = copy.copy(self.headers) + headers["x-auth-token"] = self.get_admin_token_id() + trust_lists_response = self.restful_request(method='GET', + path=url, + port=self._public_port(), + body="", + headers=headers) + trusts = trust_lists_response.body['trusts'] + self.assertEqual(len(trusts), 3) + + trustee_url = url + "?trustee_user_id=" + self.trustee['id'] + headers["x-auth-token"] = trustee_token['token']['id'] + trust_lists_response = self.restful_request( + method='GET', path=trustee_url, port=self._public_port(), + body="", headers=headers) + trusts = trust_lists_response.body['trusts'] + self.assertEqual(len(trusts), 3) + + headers["x-auth-token"] = trustor_token['token']['id'] + + trust_lists_response = self.restful_request( + method='GET', path=trustee_url, port=self._public_port(), + body="", headers=headers, expected_status=403) + + trustor_url = url + "?trustor_user_id=" + self.trustor['id'] + headers["x-auth-token"] = trustor_token['token']['id'] + trust_lists_response = self.restful_request( + method='GET', + path=trustor_url, + port=self._public_port(), + body="", + headers=headers) + trusts = trust_lists_response.body['trusts'] + self.assertEqual(len(trusts), 3) + + headers["x-auth-token"] = trustee_token['token']['id'] + trust_lists_response = self.restful_request( + method='GET', path=trustor_url, port=self._public_port(), + body="", headers=headers, expected_status=403) -- cgit