diff options
| author | Jenkins <jenkins@review.openstack.org> | 2013-03-06 03:43:44 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2013-03-06 03:43:44 +0000 |
| commit | cdeda9416b8ea4af5250fb4bd48865f4d87967b3 (patch) | |
| tree | b5177fddbe575c4a74c2e537f4c97ae8df73bdf4 | |
| parent | 6db8b6d480453927ee26a720f2e96c20e13907be (diff) | |
| parent | 601eeb50b60a2e99041690fe19238202bc203503 (diff) | |
Merge "Trusts"
36 files changed, 1645 insertions, 99 deletions
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 --- /dev/null +++ b/keystone/trust/backends/__init__.py 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) |
