diff options
56 files changed, 3010 insertions, 765 deletions
diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 8c49f68e..808cff75 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -137,6 +137,7 @@ # use_dumb_member = False # allow_subtree_delete = False # dumb_member = cn=dumb,dc=example,dc=com +# page_size = 0 # The LDAP scope for queries, this can be either 'one' # (onelevel/singleLevel) or 'sub' (subtree/wholeSubtree) @@ -145,6 +146,7 @@ # user_tree_dn = ou=Users,dc=example,dc=com # user_filter = # user_objectclass = inetOrgPerson +# user_domain_id_attribute = businessCategory # user_id_attribute = cn # user_name_attribute = sn # user_mail_attribute = email @@ -162,6 +164,7 @@ # tenant_tree_dn = ou=Groups,dc=example,dc=com # tenant_filter = # tenant_objectclass = groupOfNames +# tenant_domain_id_attribute = businessCategory # tenant_id_attribute = cn # tenant_member_attribute = member # tenant_name_attribute = ou 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 d2eaa234..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,37 +178,40 @@ 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 - if 'methods' not in self.auth['authentication']: + if 'methods' not in self.auth['identity']: raise exception.ValidationError(attribute='methods', - target='authentication') + target='identity') # make sure all the method data/payload are provided for method_name in self.get_method_names(): - if method_name not in self.auth['authentication']: + if method_name not in self.auth['identity']: raise exception.ValidationError(attribute=method_name, - target='authentication') + target='identity') # make sure auth method is supported for method_name in self.get_method_names(): @@ -213,12 +229,12 @@ class AuthInfo(object): self._validate_and_normalize_scope_data() def get_method_names(self): - """ Returns the authentication method names. + """ Returns the identity method names. :returns: list of auth method names """ - return self.auth['authentication']['methods'] + return self.auth['identity']['methods'] def get_method_data(self, method): """ Get the auth method payload. @@ -226,30 +242,41 @@ class AuthInfo(object): :returns: auth method payload """ - if method not in self.auth['authentication']['methods']: + if method not in self.auth['identity']['methods']: raise exception.ValidationError(attribute=method_name, - target='authentication') - return self.auth['authentication'][method] + target='identity') + return self.auth['identity'][method] def get_scope(self): """ Get scope information. 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): @@ -257,13 +284,9 @@ class Auth(controller.V3Controller): super(Auth, self).__init__(*args, **kw) self.token_controllers_ref = token.controllers.Auth() - def authenticate_for_token(self, context, authentication, scope=None): + def authenticate_for_token(self, context, auth=None): """ Authenticate user and issue a token. """ try: - auth = None - auth = {'authentication': authentication} - if scope: - auth['scope'] = scope auth_info = AuthInfo(context, auth=auth) auth_context = {'extras': {}, 'method_names': []} self.authenticate(context, auth_info, auth_context) @@ -282,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 @@ -306,7 +331,7 @@ class Auth(controller.V3Controller): # requiring domain_id to do user lookup now. Try to get # the user_id from auth_info for now, assuming external auth # has check to make sure user is the same as the one specify - # in "authentication". + # in "identity". if 'password' in auth_info.get_method_names(): user_info = auth_info.get_method_data('password') user_ref = auth_info.lookup_user(user_info['user']) diff --git a/keystone/auth/core.py b/keystone/auth/core.py index 40f7d040..da70c43c 100644 --- a/keystone/auth/core.py +++ b/keystone/auth/core.py @@ -49,21 +49,23 @@ class AuthMethodHandler(object): "extras": {}} Plugins are invoked in the order in which they are specified in the - "methods" attribute of the "authentication" request body. + "methods" attribute of the "identity" object. For example, with the following authentication request, - {"authentication": { - "methods": ["custom-plugin", "password", "token"], - "token": { - "id": "sdfafasdfsfasfasdfds" - }, - "custom-plugin": { - "custom-data": "sdfdfsfsfsdfsf" - }, - "password": { - "user": { - "id": "s23sfad1", - "password": "secrete" + {"auth": { + "identity": { + "methods": ["custom-plugin", "password", "token"], + "token": { + "id": "sdfafasdfsfasfasdfds" + }, + "custom-plugin": { + "custom-data": "sdfdfsfsfsdfsf" + }, + "password": { + "user": { + "id": "s23sfad1", + "password": "secrete" + } } } }} diff --git a/keystone/auth/methods/token.py b/keystone/auth/methods/token.py index 72006130..bb7b8d58 100644 --- a/keystone/auth/methods/token.py +++ b/keystone/auth/methods/token.py @@ -38,12 +38,18 @@ class Token(auth.AuthMethodHandler): target=METHOD_NAME) token_id = auth_payload['id'] token_ref = self.token_api.get_token(context, token_id) - user_context.setdefault('user_id', - token_ref['token_data']['user']['id']) - user_context.setdefault('expires', - token_ref['expires']) - user_context['extras'].update(token_ref['token_data']['extras']) - user_context['method_names'] += token_ref['token_data']['methods'] + user_context.setdefault( + 'user_id', token_ref['token_data']['token']['user']['id']) + # to support Grizzly-3 to Grizzly-RC1 transition + expires_at = token_ref['token_data']['token'].get( + 'expires_at', token_ref['token_data']['token'].get('expires')) + user_context.setdefault('expires_at', expires_at) + user_context['extras'].update( + 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 03d4ed74..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,51 +102,100 @@ 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, unicode): - expires = timeutils.isotime(expires) - token_data['expires'] = expires - token_data['issued_at'] = timeutils.strtime() + if not isinstance(expires, basestring): + expires = timeutils.isotime(expires, subsecond=True) + token_data['expires_at'] = expires + 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) - return token_data + project_id, trust) + self._populate_token(token_data, expires, trust) + return {'token': token_data} def recreate_token_data(context, token_data=None, expires=None, @@ -161,12 +212,17 @@ def recreate_token_data(context, token_data=None, expires=None, methods = ['password', 'token'] extras = {} if token_data: + # peel the outer layer so its easier to operate + token_data = token_data['token'] domain_id = (token_data['domain']['id'] if 'domain' in token_data else None) project_id = (token_data['project']['id'] if 'project' in token_data else None) if not new_expires: - new_expires = token_data['expires'] + # support Grizzly-3 to Grizzly-RC1 transition + # tokens issued in G3 has 'expires' instead of 'expires_at' + new_expires = token_data.get('expires_at', + token_data.get('expires')) user_id = token_data['user']['id'] methods = token_data['methods'] extras = token_data['extras'] @@ -184,16 +240,18 @@ 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(auth_context['user_id'], - method_names, - auth_context['extras'], - domain_id, - project_id, - auth_context.get('expires', - None)) + token_data = token_data_helper.get_token_data( + auth_context['user_id'], + method_names, + auth_context['extras'], + domain_id, + project_id, + auth_context.get('expires_at', None), + trust) + if CONF.signing.token_format == 'UUID': token_id = uuid.uuid4().hex elif CONF.signing.token_format == 'PKI': @@ -207,20 +265,20 @@ def create_token(context, auth_context, auth_info): CONF.signing.token_format) token_api = token_module.Manager() try: - expiry = token_data['expires'] + 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: + if 'project' in token_data['token']: # project-scoped token, fill in the v2 token data # all we care are the role IDs - role_ids = [role['id'] for role in token_data['roles']] + role_ids = [role['id'] for role in token_data['token']['roles']] metadata_ref = {'roles': role_ids} data = dict(key=token_id, id=token_id, expires=expiry, - user=token_data['user'], - tenant=token_data.get('project'), + user=token_data['token']['user'], + tenant=token_data['token'].get('project'), metadata=metadata_ref, token_data=token_data) token_api.create_token(context, token_id, data) diff --git a/keystone/cli.py b/keystone/cli.py index e33e8188..6410d7b8 100644 --- a/keystone/cli.py +++ b/keystone/cli.py @@ -169,4 +169,5 @@ def main(argv=None, config_files=None): project='keystone', usage='%(prog)s [' + '|'.join([cmd.name for cmd in CMDS]) + ']', default_config_files=config_files) + config.setup_logging(CONF) CONF.command.cmd_class.main() diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 6377692e..f4f3c79d 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -1,3 +1,4 @@ +import collections import functools import uuid @@ -27,46 +28,78 @@ def _build_policy_check_credentials(self, action, context, kwargs): raise exception.Unauthorized() creds = {} - token_data = token_ref['token_data'] + if 'token_data' in token_ref: + #V3 Tokens + token_data = token_ref['token_data']['token'] + try: + creds['user_id'] = token_data['user']['id'] + except AttributeError: + LOG.warning(_('RBAC: Invalid user')) + raise exception.Unauthorized() + + if 'project' in token_data: + creds['project_id'] = token_data['project']['id'] + else: + LOG.debug(_('RBAC: Proceeding without project')) - try: - creds['user_id'] = token_data['user']['id'] - except AttributeError: - LOG.warning(_('RBAC: Invalid user')) - raise exception.Unauthorized() + if 'domain' in token_data: + creds['domain_id'] = token_data['domain']['id'] - if 'project' in token_data: - creds['project_id'] = token_data['project']['id'] + if 'roles' in token_data: + creds['roles'] = [] + for role in token_data['roles']: + creds['roles'].append(role['name']) else: - LOG.debug(_('RBAC: Proceeding without project')) + #v2 Tokens + creds = token_ref.get('metadata', {}).copy() + try: + creds['user_id'] = token_ref['user'].get('id') + except AttributeError: + LOG.warning(_('RBAC: Invalid user')) + raise exception.Unauthorized() + try: + creds['project_id'] = token_ref['tenant'].get('id') + except AttributeError: + LOG.debug(_('RBAC: Proceeding without tenant')) + # NOTE(vish): this is pretty inefficient + creds['roles'] = [self.identity_api.get_role(context, role)['name'] + for role in creds.get('roles', [])] + + return creds - if 'domain' in token_data: - creds['domain_id'] = token_data['domain']['id'] - if 'roles' in token_data: - creds['roles'] = [] - for role in token_data['roles']: - creds['roles'].append(role['name']) +def flatten(d, parent_key=''): + """Flatten a nested dictionary - return creds + Converts a dictionary with nested values to a single level flat + dictionary, with dotted notation for each key. + + """ + items = [] + for k, v in d.items(): + new_key = parent_key + '.' + k if parent_key else k + if isinstance(v, collections.MutableMapping): + items.extend(flatten(v, new_key).items()) + else: + items.append((new_key, v)) + return dict(items) def protected(f): """Wraps API calls with role based access controls (RBAC).""" - @functools.wraps(f) def wrapper(self, context, **kwargs): - if not context['is_admin']: + if 'is_admin' in context and context['is_admin']: + LOG.warning(_('RBAC: Bypassing authorization')) + else: action = 'identity:%s' % f.__name__ creds = _build_policy_check_credentials(self, action, context, kwargs) # Simply use the passed kwargs as the target dict, which # would typically include the prime key of a get/update/delete # call. - self.policy_api.enforce(context, creds, action, kwargs) + self.policy_api.enforce(context, creds, action, flatten(kwargs)) LOG.debug(_('RBAC: Authorization granted')) - else: - LOG.warning(_('RBAC: Bypassing authorization')) return f(self, context, **kwargs) return wrapper @@ -89,11 +122,6 @@ def filterprotected(*filters): # parameter) and would typically include the prime key # of a get/update/delete call # - # TODO(henry-nash) do we need to put the whole object - # in, which is part of kwargs? I kept this in as it was part - # of the previous implementation, but without a specific key - # reference in the target I don't see how it can be used. - # First any query filter parameters target = dict() if len(filters) > 0: @@ -109,7 +137,8 @@ def filterprotected(*filters): for key in kwargs: target[key] = kwargs[key] - self.policy_api.enforce(context, creds, action, target) + self.policy_api.enforce(context, creds, action, + flatten(target)) LOG.debug(_('RBAC: Authorization granted')) else: @@ -119,7 +148,8 @@ def filterprotected(*filters): return _filterprotected -@dependency.requires('identity_api', 'policy_api', 'token_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/ldap/core.py b/keystone/common/ldap/core.py index f0a5cac6..25f6a8fe 100644 --- a/keystone/common/ldap/core.py +++ b/keystone/common/ldap/core.py @@ -88,6 +88,7 @@ class BaseLdap(object): self.LDAP_USER = conf.ldap.user self.LDAP_PASSWORD = conf.ldap.password self.LDAP_SCOPE = ldap_scope(conf.ldap.query_scope) + self.page_size = conf.ldap.page_size if self.options_name is not None: self.suffix = conf.ldap.suffix @@ -128,7 +129,8 @@ class BaseLdap(object): if self.LDAP_URL.startswith('fake://'): conn = fakeldap.FakeLdap(self.LDAP_URL) else: - conn = LdapWrapper(self.LDAP_URL) + conn = LdapWrapper(self.LDAP_URL, + self.page_size) if user is None: user = self.LDAP_USER @@ -270,51 +272,6 @@ class BaseLdap(object): return [self._ldap_res_to_model(x) for x in self._ldap_get_all(filter)] - def get_page(self, marker, limit): - return self._get_page(marker, limit, self.get_all()) - - def get_page_markers(self, marker, limit): - return self._get_page_markers(marker, limit, self.get_all()) - - @staticmethod - def _get_page(marker, limit, lst, key=lambda x: x.id): - lst.sort(key=key) - if not marker: - return lst[:limit] - else: - return [x for x in lst if key(x) > marker][:limit] - - @staticmethod - def _get_page_markers(marker, limit, lst, key=lambda x: x.id): - if len(lst) < limit: - return (None, None) - - lst.sort(key=key) - if marker is None: - if len(lst) <= limit + 1: - nxt = None - else: - nxt = key(lst[limit]) - return (None, nxt) - - i = 0 - for i, item in enumerate(lst): - k = key(item) - if k >= marker: - break - - if i <= limit: - prv = None - else: - prv = key(lst[i - limit]) - - if i + limit >= len(lst) - 1: - nxt = None - else: - nxt = key(lst[i + limit]) - - return (prv, nxt) - def update(self, id, values, old_obj=None): if not self.allow_update: action = _('LDAP %s update') % self.options_name @@ -361,9 +318,10 @@ class BaseLdap(object): class LdapWrapper(object): - def __init__(self, url): + def __init__(self, url, page_size): LOG.debug(_("LDAP init: url=%s"), url) self.conn = ldap.initialize(url) + self.page_size = page_size def simple_bind_s(self, user, password): LOG.debug(_("LDAP bind: dn=%s"), user) @@ -387,15 +345,59 @@ class LdapWrapper(object): scope, query, attrlist) - res = self.conn.search_s(dn, scope, query, attrlist) + if self.page_size: + res = self.paged_search_s(dn, scope, query, attrlist) + else: + res = self.conn.search_s(dn, scope, query, attrlist) o = [] for dn, attrs in res: o.append((dn, dict((kind, [ldap2py(x) for x in values]) for kind, values in attrs.iteritems()))) - return o + def paged_search_s(self, dn, scope, query, attrlist=None): + res = [] + lc = ldap.controls.SimplePagedResultsControl( + controlType=ldap.LDAP_CONTROL_PAGE_OID, + criticality=True, + controlValue=(self.page_size, '')) + msgid = self.conn.search_ext(dn, + scope, + query, + attrlist, + serverctrls=[lc]) + # Endless loop request pages on ldap server until it has no data + while True: + # Request to the ldap server a page with 'page_size' entries + rtype, rdata, rmsgid, serverctrls = self.conn.result3(msgid) + # Receive the data + res.extend(rdata) + pctrls = [c for c in serverctrls + if c.controlType == ldap.LDAP_CONTROL_PAGE_OID] + if pctrls: + # LDAP server supports pagination + est, cookie = pctrls[0].controlValue + if cookie: + # There is more data still on the server + # so we request another page + lc.controlValue = (self.page_size, cookie) + msgid = self.conn.search_ext(dn, + scope, + query, + attrlist, + serverctrls=[lc]) + else: + # Exit condition no more data on server + break + else: + LOG.warning(_('LDAP Server does not support paging.' + 'Disable paging in keystone.conf to' + 'avoid this message')) + self._disable_paging() + break + return res + def modify_s(self, dn, modlist): ldap_modlist = [ (op, kind, (None if values is None @@ -418,6 +420,10 @@ class LdapWrapper(object): LOG.debug(_("LDAP delete_ext: dn=%s, serverctrls=%s"), dn, serverctrls) return self.conn.delete_ext_s(dn, serverctrls) + def _disable_paging(self): + # Disable the pagination from now on + self.page_size = 0 + class EnabledEmuMixIn(BaseLdap): """Emulates boolean 'enabled' attribute if turned on. 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/serializer.py b/keystone/common/serializer.py index 2a33ee70..91a16be4 100644 --- a/keystone/common/serializer.py +++ b/keystone/common/serializer.py @@ -120,10 +120,28 @@ class XmlDeserializer(object): # current spec does not have attributes on an element with text values = values or text or {} + decoded_tag = XmlDeserializer._tag_name(element.tag, namespace) + list_item_tag = None + if decoded_tag[-1] == 's' and len(values) == 0: + # FIXME(gyee): special-case lists for now unti we + # figure out how to properly handle them. + # If any key ends with an 's', we are assuming it is a list. + # List element have no attributes. + values = list(values) + if decoded_tag == 'policies': + list_item_tag = 'policy' + else: + list_item_tag = decoded_tag[:-1] for child in [self.walk_element(x) for x in element if not isinstance(x, ENTITY_TYPE)]: - values = dict(values.items() + child.items()) + if list_item_tag: + # FIXME(gyee): special-case lists for now unti we + # figure out how to properly handle them. + # If any key ends with an 's', we are assuming it is a list. + values.append(child[list_item_tag]) + else: + values = dict(values.items() + child.items()) return {XmlDeserializer._tag_name(element.tag, namespace): values} @@ -173,7 +191,7 @@ class XmlSerializer(object): container = etree.Element(k) element.append(container) name = k[:-1] - elif k == 'serviceCatalog': + elif k == 'serviceCatalog' or k == 'catalog': # xsd compliance: <serviceCatalog> contains <service>s container = etree.Element(k) element.append(container) @@ -184,7 +202,13 @@ class XmlSerializer(object): # unnecessary in XML name = element.tag[:-1] elif k[-1] == 's': - name = k[:-1] + container = etree.Element(k) + element.append(container) + if k == 'policies': + # need to special-case policies since policie is not a word + name = 'policy' + else: + name = k[:-1] else: name = k @@ -226,6 +250,8 @@ class XmlSerializer(object): self._populate_sequence(element, value) elif isinstance(value, dict): self._populate_tree(element, value) + elif isinstance(value, basestring): + element.text = unicode(value) def _populate_sequence(self, element, l): """Populates an etree with a sequence of elements, given a list.""" @@ -233,6 +259,8 @@ class XmlSerializer(object): name = element.tag if element.tag[-1] == 's': name = element.tag[:-1] + if name == 'policie': + name = 'policy' for item in l: child = etree.Element(name) 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/common/wsgi.py b/keystone/common/wsgi.py index a515eefe..108cc0ca 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -553,5 +553,5 @@ def render_exception(error): 'message': str(error) }} if isinstance(error, exception.AuthPluginException): - body['authentication'] = error.authentication + body['error']['identity'] = error.authentication return render_response(status=(error.code, error.title), body=body) diff --git a/keystone/config.py b/keystone/config.py index a96073c6..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', @@ -250,6 +252,7 @@ register_bool('use_dumb_member', group='ldap', default=False) register_str('dumb_member', group='ldap', default='cn=dumb,dc=nonexistent') register_bool('allow_subtree_delete', group='ldap', default=False) register_str('query_scope', group='ldap', default='one') +register_int('page_size', group='ldap', default=0) register_str('user_tree_dn', group='ldap', default=None) register_str('user_filter', group='ldap', default=None) @@ -259,7 +262,8 @@ register_str('user_name_attribute', group='ldap', default='sn') register_str('user_mail_attribute', group='ldap', default='email') register_str('user_pass_attribute', group='ldap', default='userPassword') register_str('user_enabled_attribute', group='ldap', default='enabled') -register_str('user_domain_id_attribute', group='ldap', default='domain_id') +register_str('user_domain_id_attribute', group='ldap', + default='businessCategory') register_int('user_enabled_mask', group='ldap', default=0) register_str('user_enabled_default', group='ldap', default='True') register_list('user_attribute_ignore', group='ldap', @@ -278,7 +282,8 @@ register_str('tenant_member_attribute', group='ldap', default='member') register_str('tenant_name_attribute', group='ldap', default='ou') register_str('tenant_desc_attribute', group='ldap', default='description') register_str('tenant_enabled_attribute', group='ldap', default='enabled') -register_str('tenant_domain_id_attribute', group='ldap', default='domain_id') +register_str('tenant_domain_id_attribute', group='ldap', + default='businessCategory') register_list('tenant_attribute_ignore', group='ldap', default='') register_bool('tenant_allow_create', group='ldap', default=True) register_bool('tenant_allow_update', group='ldap', default=True) 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/ldap/core.py b/keystone/identity/backends/ldap/core.py index 53a7d977..078a1deb 100644 --- a/keystone/identity/backends/ldap/core.py +++ b/keystone/identity/backends/ldap/core.py @@ -464,22 +464,6 @@ class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap, ApiShimMixin): return self.role_api.add_user(values.role_id, values.user_id, values.tenant_id) - def users_get_page(self, marker, limit): - return self.get_page(marker, limit) - - def users_get_page_markers(self, marker, limit): - return self.get_page_markers(marker, limit) - - def users_get_by_project_get_page(self, tenant_id, role_id, marker, limit): - return self._get_page(marker, - limit, - self.project_api.get_users(tenant_id, role_id)) - - def users_get_by_project_get_page_markers(self, tenant_id, role_id, - marker, limit): - return self._get_page_markers( - marker, limit, self.project_api.get_users(tenant_id, role_id)) - def check_password(self, user_id, password): user = self.get(user_id) return utils.check_password(password, user.password) @@ -553,15 +537,6 @@ class ProjectApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap, projects.append(self.get(project_id)) return projects - def list_for_user_get_page(self, user, marker, limit): - return self._get_page(marker, - limit, - self.get_user_projects(user['id'])) - - def list_for_user_get_page_markers(self, user, marker, limit): - return self._get_page_markers( - marker, limit, self.get_user_projects(user['id'])) - def is_empty(self, id): tenant = self._ldap_get(id) members = tenant[1].get(self.member_attribute, []) @@ -851,14 +826,6 @@ class RoleApi(common_ldap.BaseLdap, ApiShimMixin): tenant_id=tenant_id)) return res - def get_by_service_get_page(self, service_id, marker, limit): - all_roles = self.get_by_service(service_id) - return self._get_page(marker, limit, all_roles) - - def get_by_service_get_page_markers(self, service_id, marker, limit): - all_roles = self.get_by_service(service_id) - return self._get_page_markers(marker, limit, all_roles) - def roles_delete_subtree_by_project(self, tenant_id): conn = self.get_connection() query = '(objectClass=%s)' % self.object_class 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/locale/keystone.pot b/keystone/locale/keystone.pot index e8d0e04e..92d56792 100644 --- a/keystone/locale/keystone.pot +++ b/keystone/locale/keystone.pot @@ -6,9 +6,10 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: keystone 2013.1\n" +"Project-Id-Version: keystone " +"jenkins.keystone.propose.translation.update.130\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2013-02-05 00:01+0000\n" +"POT-Creation-Date: 2013-03-04 00:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -37,20 +38,56 @@ msgstr "" msgid "%(property_name)s is not a%(display_expected_type)s" msgstr "" -#: keystone/config.py:44 +#: keystone/config.py:93 #, python-format msgid "Unable to locate specified logging config file: %s" msgstr "" -#: keystone/config.py:62 +#: keystone/config.py:111 msgid "Invalid syslog facility" msgstr "" -#: keystone/test.py:106 +#: keystone/test.py:107 #, python-format msgid "Failed to checkout %s" msgstr "" +#: keystone/auth/controllers.py:78 +#, python-format +msgid "Project is disabled: %s" +msgstr "" + +#: keystone/auth/controllers.py:84 keystone/auth/methods/password.py:40 +#, python-format +msgid "Domain is disabled: %s" +msgstr "" + +#: keystone/auth/controllers.py:90 keystone/auth/methods/password.py:46 +#, python-format +msgid "User is disabled: %s" +msgstr "" + +#: keystone/auth/controllers.py:250 +msgid "Scoping to both domain and project is not allowed" +msgstr "" + +#: keystone/auth/controllers.py:311 +#, python-format +msgid "Unable to lookup user %s" +msgstr "" + +#: keystone/auth/controllers.py:341 +msgid "User not found" +msgstr "" + +#: keystone/auth/token_factory.py:75 +msgid "User have no access to project" +msgstr "" + +#: keystone/auth/token_factory.py:90 +msgid "User have no access to domain" +msgstr "" + #: keystone/catalog/core.py:38 #, python-format msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" @@ -90,29 +127,42 @@ msgstr "" msgid "Signing error: %s" msgstr "" -#: keystone/common/controller.py:21 +#: keystone/common/controller.py:19 #, python-format msgid "RBAC: Authorizing %s(%s)" msgstr "" -#: keystone/common/controller.py:29 +#: keystone/common/controller.py:27 msgid "RBAC: Invalid token" msgstr "" -#: keystone/common/controller.py:37 +#: keystone/common/controller.py:37 keystone/common/controller.py:58 msgid "RBAC: Invalid user" msgstr "" #: keystone/common/controller.py:43 +msgid "RBAC: Proceeding without project" +msgstr "" + +#: keystone/common/controller.py:63 msgid "RBAC: Proceeding without tenant" msgstr "" -#: keystone/common/controller.py:51 +#: keystone/common/controller.py:93 keystone/common/controller.py:145 +msgid "RBAC: Bypassing authorization" +msgstr "" + +#: keystone/common/controller.py:102 keystone/common/controller.py:143 msgid "RBAC: Authorization granted" msgstr "" -#: keystone/common/controller.py:53 -msgid "RBAC: Bypassing authorization" +#: keystone/common/controller.py:132 +#, python-format +msgid "RBAC: Adding query filter params (%s)" +msgstr "" + +#: keystone/common/controller.py:280 +msgid "Invalid token in normalize_domain_id" msgstr "" #: keystone/common/utils.py:93 @@ -144,182 +194,188 @@ msgstr "" msgid "base64 encoded digest: %s" msgstr "" -#: keystone/common/wsgi.py:74 +#: keystone/common/wsgi.py:76 #, python-format msgid "Starting %(arg0)s on %(host)s:%(port)s" msgstr "" -#: keystone/common/wsgi.py:209 +#: keystone/common/wsgi.py:211 #, python-format msgid "arg_dict: %s" msgstr "" -#: keystone/common/wsgi.py:230 +#: keystone/common/wsgi.py:233 #, python-format msgid "Authorization failed. %s from %s" msgstr "" -#: keystone/common/wsgi.py:443 +#: keystone/common/wsgi.py:454 msgid "The resource could not be found." msgstr "" -#: keystone/common/ldap/core.py:171 +#: keystone/common/ldap/core.py:68 +#, python-format +msgid "Invalid LDAP scope: %s. Choose one of: " +msgstr "" + +#: keystone/common/ldap/core.py:197 keystone/identity/backends/kvs.py:588 +#: keystone/identity/backends/kvs.py:616 #, python-format msgid "Duplicate name, %s." msgstr "" -#: keystone/common/ldap/core.py:181 +#: keystone/common/ldap/core.py:207 keystone/identity/backends/kvs.py:581 #, python-format msgid "Duplicate ID, %s." msgstr "" -#: keystone/common/ldap/core.py:186 +#: keystone/common/ldap/core.py:212 #, python-format msgid "LDAP %s create" msgstr "" -#: keystone/common/ldap/core.py:292 +#: keystone/common/ldap/core.py:320 #, python-format msgid "LDAP %s update" msgstr "" -#: keystone/common/ldap/core.py:319 +#: keystone/common/ldap/core.py:348 #, python-format msgid "LDAP %s delete" msgstr "" -#: keystone/common/ldap/core.py:336 +#: keystone/common/ldap/core.py:365 #, python-format msgid "LDAP init: url=%s" msgstr "" -#: keystone/common/ldap/core.py:340 +#: keystone/common/ldap/core.py:369 #, python-format msgid "LDAP bind: dn=%s" msgstr "" -#: keystone/common/ldap/core.py:351 +#: keystone/common/ldap/core.py:380 #, python-format msgid "LDAP add: dn=%s, attrs=%s" msgstr "" -#: keystone/common/ldap/core.py:356 +#: keystone/common/ldap/core.py:385 #, python-format -msgid "LDAP search: dn=%s, scope=%s, query=%s" +msgid "LDAP search: dn=%s, scope=%s, query=%s, attrs=%s" msgstr "" -#: keystone/common/ldap/core.py:379 +#: keystone/common/ldap/core.py:409 #, python-format msgid "LDAP modify: dn=%s, modlist=%s" msgstr "" -#: keystone/common/ldap/core.py:384 +#: keystone/common/ldap/core.py:414 #, python-format msgid "LDAP delete: dn=%s" msgstr "" -#: keystone/common/ldap/core.py:388 +#: keystone/common/ldap/core.py:418 #, python-format msgid "LDAP delete_ext: dn=%s, serverctrls=%s" msgstr "" -#: keystone/common/ldap/fakeldap.py:148 +#: keystone/common/ldap/fakeldap.py:146 #, python-format msgid "FakeLdap initialize url=%s" msgstr "" -#: keystone/common/ldap/fakeldap.py:158 +#: keystone/common/ldap/fakeldap.py:156 #, python-format msgid "FakeLdap bind dn=%s" msgstr "" -#: keystone/common/ldap/fakeldap.py:165 +#: keystone/common/ldap/fakeldap.py:163 #, python-format msgid "FakeLdap bind fail: dn=%s not found" msgstr "" -#: keystone/common/ldap/fakeldap.py:172 +#: keystone/common/ldap/fakeldap.py:170 #, python-format msgid "FakeLdap bind fail: password for dn=%s not found" msgstr "" -#: keystone/common/ldap/fakeldap.py:177 +#: keystone/common/ldap/fakeldap.py:175 #, python-format msgid "FakeLdap bind fail: password for dn=%s does not match" msgstr "" -#: keystone/common/ldap/fakeldap.py:192 +#: keystone/common/ldap/fakeldap.py:190 #, python-format msgid "FakeLdap add item: dn=%s, attrs=%s" msgstr "" -#: keystone/common/ldap/fakeldap.py:194 +#: keystone/common/ldap/fakeldap.py:192 #, python-format msgid "FakeLdap add item failed: dn=%s is already in store." msgstr "" -#: keystone/common/ldap/fakeldap.py:208 keystone/common/ldap/fakeldap.py:222 +#: keystone/common/ldap/fakeldap.py:206 keystone/common/ldap/fakeldap.py:220 #, python-format msgid "FakeLdap delete item: dn=%s" msgstr "" -#: keystone/common/ldap/fakeldap.py:212 keystone/common/ldap/fakeldap.py:226 +#: keystone/common/ldap/fakeldap.py:210 keystone/common/ldap/fakeldap.py:224 #, python-format msgid "FakeLdap delete item failed: dn=%s not found." msgstr "" -#: keystone/common/ldap/fakeldap.py:241 +#: keystone/common/ldap/fakeldap.py:239 #, python-format msgid "FakeLdap modify item: dn=%s attrs=%s" msgstr "" -#: keystone/common/ldap/fakeldap.py:245 +#: keystone/common/ldap/fakeldap.py:243 #, python-format msgid "FakeLdap modify item failed: dn=%s not found." msgstr "" -#: keystone/common/ldap/fakeldap.py:262 +#: keystone/common/ldap/fakeldap.py:260 #, python-format msgid "FakeLdap modify item failed: item has no attribute \"%s\" to delete" msgstr "" -#: keystone/common/ldap/fakeldap.py:273 +#: keystone/common/ldap/fakeldap.py:271 #, python-format msgid "" "FakeLdap modify item failed: item has no attribute \"%s\" with value " "\"%s\" to delete" msgstr "" -#: keystone/common/ldap/fakeldap.py:278 +#: keystone/common/ldap/fakeldap.py:276 #, python-format msgid "FakeLdap modify item failed: unknown command %s" msgstr "" -#: keystone/common/ldap/fakeldap.py:280 +#: keystone/common/ldap/fakeldap.py:278 #, python-format msgid "modify_s action %s not implemented" msgstr "" -#: keystone/common/ldap/fakeldap.py:298 +#: keystone/common/ldap/fakeldap.py:296 #, python-format msgid "FakeLdap search at dn=%s scope=%s query=%s" msgstr "" -#: keystone/common/ldap/fakeldap.py:304 +#: keystone/common/ldap/fakeldap.py:302 msgid "FakeLdap search fail: dn not found for SCOPE_BASE" msgstr "" -#: keystone/common/ldap/fakeldap.py:318 +#: keystone/common/ldap/fakeldap.py:316 #, python-format msgid "Search scope %s not implemented." msgstr "" -#: keystone/common/sql/core.py:203 +#: keystone/common/sql/core.py:206 #, python-format msgid "Got mysql server has gone away: %s" msgstr "" -#: keystone/common/sql/legacy.py:175 +#: keystone/common/sql/legacy.py:180 #, python-format msgid "Cannot migrate EC2 credential: %s" msgstr "" @@ -328,55 +384,75 @@ msgstr "" msgid "version should be an integer" msgstr "" -#: keystone/common/sql/nova.py:58 +#: keystone/common/sql/nova.py:62 #, python-format msgid "Create tenant %s" msgstr "" -#: keystone/common/sql/nova.py:74 +#: keystone/common/sql/nova.py:79 #, python-format msgid "Create user %s" msgstr "" -#: keystone/common/sql/nova.py:83 +#: keystone/common/sql/nova.py:88 #, python-format msgid "Add user %s to tenant %s" msgstr "" -#: keystone/common/sql/nova.py:91 +#: keystone/common/sql/nova.py:96 #, python-format msgid "Ignoring existing role %s" msgstr "" -#: keystone/common/sql/nova.py:98 +#: keystone/common/sql/nova.py:103 #, python-format msgid "Create role %s" msgstr "" -#: keystone/common/sql/nova.py:108 +#: keystone/common/sql/nova.py:113 #, python-format msgid "Assign role %s to user %s on tenant %s" msgstr "" -#: keystone/common/sql/nova.py:123 +#: keystone/common/sql/nova.py:128 #, python-format msgid "Creating ec2 cred for user %s and tenant %s" msgstr "" -#: keystone/identity/backends/kvs.py:250 keystone/identity/backends/kvs.py:259 +#: keystone/identity/backends/kvs.py:254 keystone/identity/backends/kvs.py:263 msgid "User not found in group" msgstr "" -#: keystone/identity/backends/ldap/core.py:751 +#: keystone/identity/backends/sql.py:467 +#, python-format +msgid "Cannot remove role that has not been granted, %s" +msgstr "" + +#: keystone/identity/backends/ldap/core.py:730 #, python-format msgid "Role %s not found" msgstr "" -#: keystone/identity/backends/ldap/core.py:1115 +#: keystone/identity/backends/ldap/core.py:968 msgid "Changing Name not supported by LDAP" msgstr "" -#: keystone/policy/backends/rules.py:95 +#: keystone/openstack/common/policy.py:394 +#, python-format +msgid "Failed to understand rule %(rule)s" +msgstr "" + +#: keystone/openstack/common/policy.py:404 +#, python-format +msgid "No handler for matches of kind %s" +msgstr "" + +#: keystone/openstack/common/policy.py:679 +#, python-format +msgid "Failed to understand rule %(rule)r" +msgstr "" + +#: keystone/policy/backends/rules.py:93 #, python-format msgid "enforce %s: %s" msgstr "" diff --git a/keystone/middleware/core.py b/keystone/middleware/core.py index d904e3c0..29a6832b 100644 --- a/keystone/middleware/core.py +++ b/keystone/middleware/core.py @@ -16,6 +16,7 @@ import webob.dec +from keystone.common import logging from keystone.common import serializer from keystone.common import utils from keystone.common import wsgi @@ -25,6 +26,7 @@ from keystone.openstack.common import jsonutils CONF = config.CONF +LOG = logging.getLogger(__name__) # Header used to transmit the auth token @@ -158,6 +160,7 @@ class XmlBodyMiddleware(wsgi.Middleware): body_obj = jsonutils.loads(response.body) response.body = serializer.to_xml(body_obj) except Exception: + LOG.exception('Serializer failed') raise exception.Error(message=response.body) return response diff --git a/keystone/openstack/common/timeutils.py b/keystone/openstack/common/timeutils.py index 86004391..8e40660f 100644 --- a/keystone/openstack/common/timeutils.py +++ b/keystone/openstack/common/timeutils.py @@ -25,18 +25,22 @@ import datetime import iso8601 -TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" -PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" +# ISO 8601 extended time format with microseconds +_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' +_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' +PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND -def isotime(at=None): +def isotime(at=None, subsecond=False): """Stringify time in ISO 8601 format""" if not at: at = utcnow() - str = at.strftime(TIME_FORMAT) + st = at.strftime(_ISO8601_TIME_FORMAT + if not subsecond + else _ISO8601_TIME_FORMAT_SUBSECOND) tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - str += ('Z' if tz == 'UTC' else tz) - return str + st += ('Z' if tz == 'UTC' else tz) + return st def parse_isotime(timestr): @@ -71,11 +75,15 @@ def normalize_time(timestamp): def is_older_than(before, seconds): """Return True if before is older than seconds.""" + if isinstance(before, basestring): + before = parse_strtime(before).replace(tzinfo=None) return utcnow() - before > datetime.timedelta(seconds=seconds) def is_newer_than(after, seconds): """Return True if after is newer than seconds.""" + if isinstance(after, basestring): + after = parse_strtime(after).replace(tzinfo=None) return after - utcnow() > datetime.timedelta(seconds=seconds) @@ -87,22 +95,37 @@ def utcnow_ts(): def utcnow(): """Overridable version of utils.utcnow.""" if utcnow.override_time: - return utcnow.override_time + try: + return utcnow.override_time.pop(0) + except AttributeError: + return utcnow.override_time return datetime.datetime.utcnow() +def iso8601_from_timestamp(timestamp): + """Returns a iso8601 formated date from timestamp""" + return isotime(datetime.datetime.utcfromtimestamp(timestamp)) + + utcnow.override_time = None def set_time_override(override_time=datetime.datetime.utcnow()): - """Override utils.utcnow to return a constant time.""" + """ + Override utils.utcnow to return a constant time or a list thereof, + one at a time. + """ utcnow.override_time = override_time def advance_time_delta(timedelta): """Advance overridden time using a datetime.timedelta.""" assert(not utcnow.override_time is None) - utcnow.override_time += timedelta + try: + for dt in utcnow.override_time: + dt += timedelta + except TypeError: + utcnow.override_time += timedelta def advance_time_seconds(seconds): @@ -135,3 +158,29 @@ def unmarshall_time(tyme): minute=tyme['minute'], second=tyme['second'], microsecond=tyme['microsecond']) + + +def delta_seconds(before, after): + """ + Compute the difference in seconds between two date, time, or + datetime objects (as a float, to microsecond resolution). + """ + delta = after - before + try: + return delta.total_seconds() + except AttributeError: + return ((delta.days * 24 * 3600) + delta.seconds + + float(delta.microseconds) / (10 ** 6)) + + +def is_soon(dt, window): + """ + Determines if time is going to happen in the next window seconds. + + :params dt: the time + :params window: minimum seconds to remain to consider the time not soon + + :return: True if expiration is within the given duration + """ + soon = (utcnow() + datetime.timedelta(seconds=window)) + return normalize_time(dt) <= soon 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/run_tests.sh b/run_tests.sh index 915de147..bfa2dbcc 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -51,6 +51,7 @@ function process_option { -f|--force) force=1;; -u|--update) update=1;; -p|--pep8) just_pep8=1;; + -8|--8) short_pep8=1;; -P|--no-pep8) no_pep8=1;; -c|--coverage) coverage=1;; -xintegration) nokeystoneclient=1;; @@ -71,6 +72,7 @@ noseargs= noseopts= wrapper="" just_pep8=0 +short_pep8=0 no_pep8=0 coverage=0 nokeystoneclient=0 @@ -108,6 +110,14 @@ function run_tests { } function run_pep8 { + FLAGS=--show-pep8 + echo $# + if [ $# -gt 0 ] && [ 'short' == ''$1 ] + then + FLAGS='' + fi + + echo "Running pep8 ..." # Opt-out files from pep8 ignore_scripts="*.pyc,*.pyo,*.sh,*.swp,*.rst" @@ -116,7 +126,7 @@ function run_pep8 { ignore="$ignore_scripts,$ignore_files,$ignore_dirs" srcfiles="." # Just run PEP8 in current environment - ${wrapper} pep8 --repeat --show-pep8 --show-source \ + ${wrapper} pep8 --repeat $FLAGS --show-source \ --exclude=${ignore} ${srcfiles} | tee pep8.txt } @@ -162,6 +172,12 @@ if [ $just_pep8 -eq 1 ]; then exit fi +if [ $short_pep8 -eq 1 ]; then + run_pep8 short + exit +fi + + if [ $recreate_db -eq 1 ]; then rm -f tests.sqlite fi diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..54099681 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,41 @@ +# 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 re +import sys + +from keystone.common import utils + + +RE_VERSION = re.compile(r'^OpenSSL 1\.\d\.\d') + + +def setup_package(): + check_dependencies() + + +def check_dependencies(): + check_openssl_version() + + +def check_openssl_version(): + openssl_version = utils.check_output(['openssl', 'version']) + openssl_version = openssl_version.strip() + match = RE_VERSION.match(openssl_version) + if not match: + raise AssertionError('Incorrect version of OpenSSL (%s),' + ' 1.0.0+ required.' % openssl_version) 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 7567d379..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 -import default_fixtures - +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 @@ -60,7 +68,7 @@ class AuthTest(test.TestCase): self.load_backends() self.load_fixtures(default_fixtures) - self.api = token.controllers.Auth() + self.controller = token.controllers.Auth() def assertEqualTokens(self, a, b): """Assert that two tokens are equal. @@ -92,76 +100,85 @@ class AuthBadRequests(AuthTest): not applicable""" self.assertRaises( token.controllers.ExternalAuthNotApplicable, - self.api._authenticate_external, + self.controller._authenticate_external, {}, {}) def test_no_token_in_auth(self): """Verity that _authenticate_token() raises exception if no token""" self.assertRaises( exception.ValidationError, - self.api._authenticate_token, + self.controller._authenticate_token, None, {}) def test_no_credentials_in_auth(self): """Verity that _authenticate_local() raises exception if no creds""" self.assertRaises( exception.ValidationError, - self.api._authenticate_local, + self.controller._authenticate_local, None, {}) def test_authenticate_blank_request_body(self): """Verify sending empty json dict raises the right exception.""" - self.assertRaises(exception.ValidationError, self.api.authenticate, + self.assertRaises(exception.ValidationError, + self.controller.authenticate, {}, {}) def test_authenticate_blank_auth(self): """Verify sending blank 'auth' raises the right exception.""" body_dict = _build_user_auth() - self.assertRaises(exception.ValidationError, self.api.authenticate, + self.assertRaises(exception.ValidationError, + self.controller.authenticate, {}, body_dict) def test_authenticate_invalid_auth_content(self): """Verify sending invalid 'auth' raises the right exception.""" - self.assertRaises(exception.ValidationError, self.api.authenticate, + self.assertRaises(exception.ValidationError, + self.controller.authenticate, {}, {'auth': 'abcd'}) def test_authenticate_user_id_too_large(self): """Verify sending large 'userId' raises the right exception.""" body_dict = _build_user_auth(user_id='0' * 65, username='FOO', password='foo2') - self.assertRaises(exception.ValidationSizeError, self.api.authenticate, + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, {}, body_dict) def test_authenticate_username_too_large(self): """Verify sending large 'username' raises the right exception.""" body_dict = _build_user_auth(username='0' * 65, password='foo2') - self.assertRaises(exception.ValidationSizeError, self.api.authenticate, + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, {}, body_dict) def test_authenticate_tenant_id_too_large(self): """Verify sending large 'tenantId' raises the right exception.""" body_dict = _build_user_auth(username='FOO', password='foo2', tenant_id='0' * 65) - self.assertRaises(exception.ValidationSizeError, self.api.authenticate, + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, {}, body_dict) def test_authenticate_tenant_name_too_large(self): """Verify sending large 'tenantName' raises the right exception.""" body_dict = _build_user_auth(username='FOO', password='foo2', tenant_name='0' * 65) - self.assertRaises(exception.ValidationSizeError, self.api.authenticate, + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, {}, body_dict) def test_authenticate_token_too_large(self): """Verify sending large 'token' raises the right exception.""" body_dict = _build_user_auth(token={'id': '0' * 8193}) - self.assertRaises(exception.ValidationSizeError, self.api.authenticate, + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, {}, body_dict) def test_authenticate_password_too_large(self): """Verify sending large 'password' raises the right exception.""" body_dict = _build_user_auth(username='FOO', password='0' * 8193) - self.assertRaises(exception.ValidationSizeError, self.api.authenticate, + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, {}, body_dict) @@ -173,7 +190,7 @@ class AuthWithToken(AuthTest): """Verify getting an unscoped token with password creds""" body_dict = _build_user_auth(username='FOO', password='foo2') - unscoped_token = self.api.authenticate({}, body_dict) + unscoped_token = self.controller.authenticate({}, body_dict) tenant = unscoped_token["access"]["token"].get("tenant", None) self.assertEqual(tenant, None) @@ -182,7 +199,7 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth(token={"id": uuid.uuid4().hex}) self.assertRaises( exception.Unauthorized, - self.api.authenticate, + self.controller.authenticate, {}, body_dict) def test_auth_bad_formatted_token(self): @@ -190,7 +207,7 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth(token={}) self.assertRaises( exception.ValidationError, - self.api.authenticate, + self.controller.authenticate, {}, body_dict) def test_auth_unscoped_token_no_project(self): @@ -198,11 +215,11 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth( username='FOO', password='foo2') - unscoped_token = self.api.authenticate({}, body_dict) + unscoped_token = self.controller.authenticate({}, body_dict) body_dict = _build_user_auth( token=unscoped_token["access"]["token"]) - unscoped_token_2 = self.api.authenticate({}, body_dict) + unscoped_token_2 = self.controller.authenticate({}, body_dict) self.assertEqualTokens(unscoped_token, unscoped_token_2) @@ -217,12 +234,12 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth( username='FOO', password='foo2') - unscoped_token = self.api.authenticate({}, body_dict) + unscoped_token = self.controller.authenticate({}, body_dict) # Get a token on BAR tenant using the unscoped tenant body_dict = _build_user_auth( token=unscoped_token["access"]["token"], tenant_name="BAR") - scoped_token = self.api.authenticate({}, body_dict) + scoped_token = self.controller.authenticate({}, body_dict) tenant = scoped_token["access"]["token"]["tenant"] roles = scoped_token["access"]["metadata"]["roles"] @@ -253,7 +270,7 @@ class AuthWithToken(AuthTest): password='foo2', tenant_name="BAR") - scoped_token = self.api.authenticate({}, body_dict) + scoped_token = self.controller.authenticate({}, body_dict) tenant = scoped_token["access"]["token"]["tenant"] roles = scoped_token["access"]["metadata"]["roles"] @@ -307,7 +324,7 @@ class AuthWithToken(AuthTest): password=self.user_foo['password'], tenant_name=project1['name']) - scoped_token = self.api.authenticate({}, body_dict) + scoped_token = self.controller.authenticate({}, body_dict) tenant = scoped_token["access"]["token"]["tenant"] roles = scoped_token["access"]["metadata"]["roles"] self.assertEquals(tenant["id"], project1['id']) @@ -328,7 +345,7 @@ class AuthWithPasswordCredentials(AuthTest): password=uuid.uuid4().hex) self.assertRaises( exception.Unauthorized, - self.api.authenticate, + self.controller.authenticate, {}, body_dict) def test_auth_valid_user_invalid_password(self): @@ -338,7 +355,7 @@ class AuthWithPasswordCredentials(AuthTest): password=uuid.uuid4().hex) self.assertRaises( exception.Unauthorized, - self.api.authenticate, + self.controller.authenticate, {}, body_dict) def test_auth_empty_password(self): @@ -348,7 +365,7 @@ class AuthWithPasswordCredentials(AuthTest): password="") self.assertRaises( exception.Unauthorized, - self.api.authenticate, + self.controller.authenticate, {}, body_dict) def test_auth_no_password(self): @@ -356,21 +373,23 @@ class AuthWithPasswordCredentials(AuthTest): body_dict = _build_user_auth(username="FOO") self.assertRaises( exception.ValidationError, - self.api.authenticate, + self.controller.authenticate, {}, body_dict) def test_authenticate_blank_password_credentials(self): """Verify sending empty json dict as passwordCredentials raises the right exception.""" body_dict = {'passwordCredentials': {}, 'tenantName': 'demo'} - self.assertRaises(exception.ValidationError, self.api.authenticate, + self.assertRaises(exception.ValidationError, + self.controller.authenticate, {}, body_dict) def test_authenticate_no_username(self): """Verify skipping username raises the right exception.""" body_dict = _build_user_auth(password="pass", tenant_name="demo") - self.assertRaises(exception.ValidationError, self.api.authenticate, + self.assertRaises(exception.ValidationError, + self.controller.authenticate, {}, body_dict) @@ -383,11 +402,11 @@ class AuthWithRemoteUser(AuthTest): body_dict = _build_user_auth( username='FOO', password='foo2') - local_token = self.api.authenticate( + local_token = self.controller.authenticate( {}, body_dict) body_dict = _build_user_auth() - remote_token = self.api.authenticate( + remote_token = self.controller.authenticate( {'REMOTE_USER': 'FOO'}, body_dict) self.assertEqualTokens(local_token, remote_token) @@ -396,7 +415,7 @@ class AuthWithRemoteUser(AuthTest): """Verify that external auth with invalid request fails""" self.assertRaises( exception.ValidationError, - self.api.authenticate, + self.controller.authenticate, {'REMOTE_USER': 'FOO'}, None) @@ -406,12 +425,12 @@ class AuthWithRemoteUser(AuthTest): username='FOO', password='foo2', tenant_name='BAR') - local_token = self.api.authenticate( + local_token = self.controller.authenticate( {}, body_dict) body_dict = _build_user_auth( tenant_name='BAR') - remote_token = self.api.authenticate( + remote_token = self.controller.authenticate( {'REMOTE_USER': 'FOO'}, body_dict) self.assertEqualTokens(local_token, remote_token) @@ -422,11 +441,11 @@ class AuthWithRemoteUser(AuthTest): username='TWO', password='two2', tenant_name='BAZ') - local_token = self.api.authenticate( + local_token = self.controller.authenticate( {}, body_dict) body_dict = _build_user_auth(tenant_name='BAZ') - remote_token = self.api.authenticate( + remote_token = self.controller.authenticate( {'REMOTE_USER': 'TWO'}, body_dict) self.assertEqualTokens(local_token, remote_token) @@ -436,15 +455,249 @@ class AuthWithRemoteUser(AuthTest): body_dict = _build_user_auth(tenant_name="BAR") self.assertRaises( exception.Unauthorized, - self.api.authenticate, + self.controller.authenticate, {'REMOTE_USER': uuid.uuid4().hex}, 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.""" - r = self.api.authenticate( + r = self.controller.authenticate( {}, auth={ 'passwordCredentials': { @@ -457,14 +710,14 @@ class TokenExpirationTest(AuthTest): time.sleep(0.5) - r = self.api.validate_token( + r = self.controller.validate_token( dict(is_admin=True, query_string={}), token_id=unscoped_token_id) self.assertEqual(original_expiration, r['access']['token']['expires']) time.sleep(0.5) - r = self.api.authenticate( + r = self.controller.authenticate( {}, auth={ 'token': { @@ -477,7 +730,7 @@ class TokenExpirationTest(AuthTest): time.sleep(0.5) - r = self.api.validate_token( + r = self.controller.validate_token( dict(is_admin=True, query_string={}), token_id=scoped_token_id) self.assertEqual(original_expiration, r['access']['token']['expires']) diff --git a/tests/test_auth_plugin.py b/tests/test_auth_plugin.py index d35d5f23..fc44c53d 100644 --- a/tests/test_auth_plugin.py +++ b/tests/test_auth_plugin.py @@ -56,7 +56,7 @@ class TestAuthPlugin(test.TestCase): method_name = uuid.uuid4().hex auth_data = {'methods': [method_name]} auth_data[method_name] = {'test': 'test'} - auth_data = {'authentication': auth_data} + auth_data = {'identity': auth_data} self.assertRaises(exception.AuthMethodNotSupported, auth.controllers.AuthInfo, None, @@ -66,7 +66,7 @@ class TestAuthPlugin(test.TestCase): auth_data = {'methods': ['simple-challenge-response']} auth_data['simple-challenge-response'] = { 'test': 'test'} - auth_data = {'authentication': auth_data} + auth_data = {'identity': auth_data} auth_info = auth.controllers.AuthInfo(None, auth_data) auth_context = {'extras': {}, 'method_names': []} try: @@ -81,7 +81,7 @@ class TestAuthPlugin(test.TestCase): auth_data = {'methods': ['simple-challenge-response']} auth_data['simple-challenge-response'] = { 'response': EXPECTED_RESPONSE} - auth_data = {'authentication': auth_data} + auth_data = {'identity': auth_data} auth_info = auth.controllers.AuthInfo(None, auth_data) auth_context = {'extras': {}, 'method_names': []} self.api.authenticate({}, auth_info, auth_context) @@ -91,7 +91,7 @@ class TestAuthPlugin(test.TestCase): auth_data = {'methods': ['simple-challenge-response']} auth_data['simple-challenge-response'] = { 'response': uuid.uuid4().hex} - auth_data = {'authentication': auth_data} + auth_data = {'identity': auth_data} auth_info = auth.controllers.AuthInfo(None, auth_data) auth_context = {'extras': {}, 'method_names': []} self.assertRaises(exception.Unauthorized, 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 183974fd..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) @@ -173,15 +173,15 @@ class RestfulTestCase(test.TestCase): if response.body is not None and response.body.strip(): # if a body is provided, a Content-Type is also expected header = response.getheader('Content-Type', None) - self.assertIn(self.content_type, header) + self.assertIn(content_type, header) - if self.content_type == 'json': + if content_type == 'json': response.body = jsonutils.loads(response.body) - elif self.content_type == 'xml': + elif content_type == 'xml': response.body = etree.fromstring(response.body) def restful_request(self, method='GET', headers=None, body=None, - token=None, **kwargs): + token=None, content_type=None, **kwargs): """Serializes/deserializes json/xml as request/response body. .. WARNING:: @@ -196,13 +196,13 @@ class RestfulTestCase(test.TestCase): if token is not None: headers['X-Auth-Token'] = token - body = self._to_content_type(body, headers) + body = self._to_content_type(body, headers, content_type) # Perform the HTTP request/response response = self.request(method=method, headers=headers, body=body, **kwargs) - self._from_content_type(response) + self._from_content_type(response, content_type) # we can save some code & improve coverage by always doing this if method != 'HEAD' and response.status >= 400: @@ -742,8 +742,9 @@ class XmlTestCase(RestfulTestCase, CoreApiTests): self.assertIsNotNone(extension.find(self._tag('description'))) self.assertTrue(extension.find(self._tag('description')).text) - self.assertTrue(len(extension.findall(self._tag('link')))) - for link in extension.findall(self._tag('link')): + links = extension.find(self._tag('links')) + self.assertTrue(len(links.findall(self._tag('link')))) + for link in links.findall(self._tag('link')): self.assertValidExtensionLink(link) def assertValidExtensionListResponse(self, r): @@ -763,8 +764,10 @@ class XmlTestCase(RestfulTestCase, CoreApiTests): def assertValidVersion(self, version): super(XmlTestCase, self).assertValidVersion(version) - self.assertTrue(len(version.findall(self._tag('link')))) - for link in version.findall(self._tag('link')): + links = version.find(self._tag('links')) + self.assertIsNotNone(links) + self.assertTrue(len(links.findall(self._tag('link')))) + for link in links.findall(self._tag('link')): self.assertIsNotNone(link.get('rel')) self.assertIsNotNone(link.get('href')) 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_serializer.py b/tests/test_serializer.py index 288e5516..816bad45 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -145,6 +145,17 @@ class XmlSerializerTestCase(test.TestCase): self.assertSerializeDeserialize(d, xml) + def test_policy_list(self): + d = {"policies": [{"id": "ab12cd"}]} + + xml = """ + <?xml version="1.0" encoding="UTF-8"?> + <policies xmlns="http://docs.openstack.org/identity/api/v2.0"> + <policy id="ab12cd"/> + </policies> + """ + self.assertEqualIgnoreWhitespace(serializer.to_xml(d), xml) + def test_values_list(self): d = { "objects": { 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 ef1a1a6c..39807cec 100644 --- a/tests/test_v3.py +++ b/tests/test_v3.py @@ -1,23 +1,34 @@ +import datetime import uuid -from keystone.common.sql import util as sql_util +from lxml import etree + from keystone import auth -from keystone import test +from keystone.common import serializer +from keystone.common.sql import util as sql_util from keystone import config +from keystone.openstack.common import timeutils +from keystone.policy.backends import rules +from keystone import test import test_content_types CONF = config.CONF +TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' + 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() @@ -59,6 +70,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.""" @@ -122,27 +136,61 @@ class RestfulTestCase(test_content_types.RestfulTestCase): ref['type'] = uuid.uuid4().hex return ref + def new_trust_ref(self, trustor_user_id, trustee_user_id, project_id=None, + impersonation=None, expires=None, role_ids=None, + role_names=None): + ref = self.new_ref() + + ref['trustor_user_id'] = trustor_user_id + ref['trustee_user_id'] = trustee_user_id + ref['impersonation'] = impersonation or False + ref['project_id'] = project_id + + if isinstance(expires, basestring): + ref['expires_at'] = expires + elif isinstance(expires, dict): + ref['expires_at'] = timeutils.strtime( + timeutils.utcnow() + datetime.timedelta(**expires), + fmt=TIME_FORMAT) + elif expires is None: + pass + else: + raise NotImplementedError('Unexpected value for "expires"') + + role_ids = role_ids or [] + role_names = role_names or [] + if role_ids or role_names: + ref['roles'] = [] + for role_id in role_ids: + ref['roles'].append({'id': role_id}) + for role_name in role_names: + ref['roles'].append({'name': role_name}) + + return ref + def get_scoped_token(self): """Convenience method so that we can test authenticated requests.""" r = self.admin_request( method='POST', path='/v3/auth/tokens', body={ - 'authentication': { - 'methods': ['password'], - 'password': { - 'user': { - 'name': self.user['name'], - 'password': self.user['password'], - 'domain': { - 'id': self.user['domain_id'] + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': self.user['name'], + 'password': self.user['password'], + 'domain': { + 'id': self.user['domain_id'] + } } } - } - }, - 'scope': { - 'project': { - 'id': self.project['id'], + }, + 'scope': { + 'project': { + 'id': self.project['id'], + } } } }) @@ -191,11 +239,32 @@ class RestfulTestCase(test_content_types.RestfulTestCase): return self.v3_request(method='DELETE', path=path, **kwargs) def assertValidErrorResponse(self, r): - self.assertIsNotNone(r.body.get('error')) - self.assertIsNotNone(r.body['error'].get('code')) - self.assertIsNotNone(r.body['error'].get('title')) - self.assertIsNotNone(r.body['error'].get('message')) - self.assertEqual(r.body['error']['code'], r.status) + if r.getheader('Content-Type') == 'application/xml': + resp = serializer.from_xml(etree.tostring(r.body)) + else: + resp = r.body + self.assertIsNotNone(resp.get('error')) + self.assertIsNotNone(resp['error'].get('code')) + self.assertIsNotNone(resp['error'].get('title')) + self.assertIsNotNone(resp['error'].get('message')) + self.assertEqual(int(resp['error']['code']), r.status) + + def assertValidListLinks(self, links): + self.assertIsNotNone(links) + self.assertIsNotNone(links.get('self')) + self.assertIn(CONF.public_endpoint % CONF, links['self']) + + self.assertIn('next', links) + if links['next'] is not None: + self.assertIn( + CONF.public_endpoint % CONF, + links['next']) + + self.assertIn('previous', links) + if links['previous'] is not None: + self.assertIn( + CONF.public_endpoint % CONF, + links['previous']) def assertValidListResponse(self, resp, key, entity_validator, ref=None, expected_length=None): @@ -215,11 +284,7 @@ class RestfulTestCase(test_content_types.RestfulTestCase): self.assertTrue(len(entities)) # collections should have relational links - self.assertIsNotNone(resp.body.get('links')) - self.assertIn('previous', resp.body['links']) - self.assertIn('self', resp.body['links']) - self.assertIn('next', resp.body['links']) - self.assertIn(CONF.public_endpoint % CONF, resp.body['links']['self']) + self.assertValidListLinks(resp.body.get('links')) for entity in entities: self.assertIsNotNone(entity) @@ -231,12 +296,13 @@ class RestfulTestCase(test_content_types.RestfulTestCase): entity_validator(entity, ref) return entities - def assertValidResponse(self, resp, key, entity_validator, ref): + def assertValidResponse(self, resp, key, entity_validator, *args, + **kwargs): """Make assertions common to all API responses.""" entity = resp.body.get(key) self.assertIsNotNone(entity) - self.assertValidEntity(entity, ref) - entity_validator(entity, ref) + self.assertValidEntity(entity, *args, **kwargs) + entity_validator(entity, *args, **kwargs) return entity def assertValidEntity(self, entity, ref=None): @@ -263,6 +329,411 @@ class RestfulTestCase(test_content_types.RestfulTestCase): return entity + # auth validation + + def assertValidISO8601ExtendedFormatDatetime(self, dt): + try: + return timeutils.parse_strtime(dt, fmt=TIME_FORMAT) + except Exception: + msg = '%s is not a valid ISO 8601 extended format date time.' % dt + raise AssertionError(msg) + self.assertTrue(isinstance(dt, datetime.datetime)) + + def assertValidTokenResponse(self, r, user=None): + self.assertTrue(r.getheader('X-Subject-Token')) + token = r.body + if r.getheader('Content-Type') == 'application/xml': + token = serializer.from_xml(etree.tostring(r.body))['token'] + else: + token = r.body['token'] + + self.assertIsNotNone(token.get('expires_at')) + expires_at = self.assertValidISO8601ExtendedFormatDatetime( + token['expires_at']) + self.assertIsNotNone(token.get('issued_at')) + issued_at = self.assertValidISO8601ExtendedFormatDatetime( + token['issued_at']) + self.assertTrue(issued_at < expires_at) + + self.assertIn('user', token) + self.assertIn('id', token['user']) + self.assertIn('name', token['user']) + self.assertIn('domain', token['user']) + self.assertIn('id', token['user']['domain']) + + if user is not None: + self.assertEqual(user['id'], token['user']['id']) + self.assertEqual(user['name'], token['user']['name']) + self.assertEqual(user['domain_id'], token['user']['domain']['id']) + + return token + + def assertValidUnscopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidTokenResponse(r, *args, **kwargs) + + self.assertNotIn('roles', token) + self.assertNotIn('catalog', token) + self.assertNotIn('project', token) + self.assertNotIn('domain', token) + + return token + + def assertValidScopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidTokenResponse(r, *args, **kwargs) + + self.assertIn('catalog', token) + self.assertIn('roles', token) + self.assertTrue(token['roles']) + for role in token['roles']: + self.assertIn('id', role) + self.assertIn('name', role) + + return token + + def assertValidProjectScopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidScopedTokenResponse(r, *args, **kwargs) + + self.assertIn('project', token) + self.assertIn('id', token['project']) + self.assertIn('name', token['project']) + self.assertIn('domain', token['project']) + self.assertIn('id', token['project']['domain']) + self.assertIn('name', token['project']['domain']) + + self.assertEqual(self.role_id, token['roles'][0]['id']) + + return token + + def assertValidProjectTrustScopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidProjectScopedTokenResponse(r, *args, **kwargs) + + self.assertIsNotNone(token.get('trust')) + self.assertIsNotNone(token['trust'].get('id')) + self.assertTrue(isinstance(token['trust'].get('impersonation'), bool)) + self.assertIsNotNone(token['trust'].get('trustor_user')) + self.assertIsNotNone(token['trust'].get('trustee_user')) + self.assertIsNotNone(token['trust']['trustor_user'].get('id')) + self.assertIsNotNone(token['trust']['trustee_user'].get('id')) + + def assertValidDomainScopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidScopedTokenResponse(r, *args, **kwargs) + + self.assertIn('domain', token) + self.assertIn('id', token['domain']) + self.assertIn('name', token['domain']) + + return token + + def assertEqualTokens(self, a, b): + """Assert that two tokens are equal. + + Compare two tokens except for their ids. This also truncates + the time in the comparison. + """ + def normalize(token): + del token['token']['expires_at'] + del token['token']['issued_at'] + return token + + a_expires_at = self.assertValidISO8601ExtendedFormatDatetime( + a['token']['expires_at']) + b_expires_at = self.assertValidISO8601ExtendedFormatDatetime( + b['token']['expires_at']) + self.assertCloseEnoughForGovernmentWork(a_expires_at, b_expires_at) + + a_issued_at = self.assertValidISO8601ExtendedFormatDatetime( + a['token']['issued_at']) + b_issued_at = self.assertValidISO8601ExtendedFormatDatetime( + b['token']['issued_at']) + self.assertCloseEnoughForGovernmentWork(a_issued_at, b_issued_at) + + return self.assertDictEqual(normalize(a), normalize(b)) + + # service validation + + def assertValidServiceListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'services', + self.assertValidService, + *args, + **kwargs) + + def assertValidServiceResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'service', + self.assertValidService, + *args, + **kwargs) + + def assertValidService(self, entity, ref=None): + self.assertIsNotNone(entity.get('type')) + if ref: + self.assertEqual(ref['type'], entity['type']) + return entity + + # endpoint validation + + def assertValidEndpointListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'endpoints', + self.assertValidEndpoint, + *args, + **kwargs) + + def assertValidEndpointResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'endpoint', + self.assertValidEndpoint, + *args, + **kwargs) + + def assertValidEndpoint(self, entity, ref=None): + self.assertIsNotNone(entity.get('interface')) + self.assertIsNotNone(entity.get('service_id')) + if ref: + self.assertEqual(ref['interface'], entity['interface']) + self.assertEqual(ref['service_id'], entity['service_id']) + return entity + + # domain validation + + def assertValidDomainListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'domains', + self.assertValidDomain, + *args, + **kwargs) + + def assertValidDomainResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'domain', + self.assertValidDomain, + *args, + **kwargs) + + def assertValidDomain(self, entity, ref=None): + if ref: + pass + return entity + + # project validation + + def assertValidProjectListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'projects', + self.assertValidProject, + *args, + **kwargs) + + def assertValidProjectResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'project', + self.assertValidProject, + *args, + **kwargs) + + def assertValidProject(self, entity, ref=None): + self.assertIsNotNone(entity.get('domain_id')) + if ref: + self.assertEqual(ref['domain_id'], entity['domain_id']) + return entity + + # user validation + + def assertValidUserListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'users', + self.assertValidUser, + *args, + **kwargs) + + def assertValidUserResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'user', + self.assertValidUser, + *args, + **kwargs) + + def assertValidUser(self, entity, ref=None): + self.assertIsNotNone(entity.get('domain_id')) + self.assertIsNotNone(entity.get('email')) + self.assertIsNone(entity.get('password')) + if ref: + self.assertEqual(ref['domain_id'], entity['domain_id']) + self.assertEqual(ref['email'], entity['email']) + return entity + + # group validation + + def assertValidGroupListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'groups', + self.assertValidGroup, + *args, + **kwargs) + + def assertValidGroupResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'group', + self.assertValidGroup, + *args, + **kwargs) + + def assertValidGroup(self, entity, ref=None): + self.assertIsNotNone(entity.get('name')) + if ref: + self.assertEqual(ref['name'], entity['name']) + return entity + + # credential validation + + def assertValidCredentialListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'credentials', + self.assertValidCredential, + *args, + **kwargs) + + def assertValidCredentialResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'credential', + self.assertValidCredential, + *args, + **kwargs) + + def assertValidCredential(self, entity, ref=None): + self.assertIsNotNone(entity.get('user_id')) + self.assertIsNotNone(entity.get('blob')) + self.assertIsNotNone(entity.get('type')) + if ref: + self.assertEqual(ref['user_id'], entity['user_id']) + self.assertEqual(ref['blob'], entity['blob']) + self.assertEqual(ref['type'], entity['type']) + self.assertEqual(ref.get('project_id'), entity.get('project_id')) + return entity + + # role validation + + def assertValidRoleListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'roles', + self.assertValidRole, + *args, + **kwargs) + + def assertValidRoleResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'role', + self.assertValidRole, + *args, + **kwargs) + + def assertValidRole(self, entity, ref=None): + self.assertIsNotNone(entity.get('name')) + if ref: + self.assertEqual(ref['name'], entity['name']) + return entity + + # policy validation + + def assertValidPolicyListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'policies', + self.assertValidPolicy, + *args, + **kwargs) + + def assertValidPolicyResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'policy', + self.assertValidPolicy, + *args, + **kwargs) + + def assertValidPolicy(self, entity, ref=None): + self.assertIsNotNone(entity.get('blob')) + self.assertIsNotNone(entity.get('type')) + if ref: + self.assertEqual(ref['blob'], entity['blob']) + self.assertEqual(ref['type'], entity['type']) + return entity + + # trust validation + + def assertValidTrustListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'trusts', + self.assertValidTrust, + *args, + **kwargs) + + def assertValidTrustResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'trust', + self.assertValidTrust, + *args, + **kwargs) + + def assertValidTrust(self, entity, ref=None): + self.assertIsNotNone(entity.get('trustor_user_id')) + self.assertIsNotNone(entity.get('trustee_user_id')) + + self.assertIn('expires_at', entity) + if entity['expires_at'] is not None: + self.assertValidISO8601ExtendedFormatDatetime(entity['expires_at']) + + # always disallow project xor project_id (neither or both is allowed) + has_roles = bool(entity.get('roles')) + has_project = bool(entity.get('project_id')) + self.assertFalse(has_roles ^ has_project) + + for role in entity['roles']: + self.assertIsNotNone(role) + self.assertValidEntity(role) + self.assertValidRole(role) + + self.assertValidListLinks(entity.get('roles_links')) + + # these were used during dev and shouldn't land in final impl + self.assertNotIn('role_ids', entity) + self.assertNotIn('role_names', entity) + + if ref: + self.assertEqual(ref['trustor_user_id'], entity['trustor_user_id']) + self.assertEqual(ref['trustee_user_id'], entity['trustee_user_id']) + self.assertEqual(ref['project_id'], entity['project_id']) + if entity.get('expires_at') or ref.get('expires_at'): + entity_exp = self.assertValidISO8601ExtendedFormatDatetime( + entity['expires_at']) + ref_exp = self.assertValidISO8601ExtendedFormatDatetime( + ref['expires_at']) + self.assertCloseEnoughForGovernmentWork(entity_exp, ref_exp) + else: + self.assertEqual(ref.get('expires_at'), + entity.get('expires_at')) + + return entity + class VersionTestCase(RestfulTestCase): def test_get_version(self): diff --git a/tests/test_v3_auth.py b/tests/test_v3_auth.py index c7f78adf..40a4c846 100644 --- a/tests/test_v3_auth.py +++ b/tests/test_v3_auth.py @@ -14,10 +14,11 @@ import uuid +import nose.exc + from keystone import auth from keystone import config from keystone import exception -from keystone.openstack.common import timeutils from keystone import test import test_v3 @@ -28,7 +29,7 @@ CONF = config.CONF def _build_auth_scope(project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, - domain_id=None, domain_name=None): + domain_id=None, domain_name=None, trust_id=None): scope_data = {} if project_id or project_name: scope_data['project'] = {} @@ -49,6 +50,9 @@ def _build_auth_scope(project_id=None, project_name=None, scope_data['domain']['id'] = domain_id else: scope_data['domain']['name'] = domain_name + if trust_id: + scope_data['trust'] = {} + scope_data['trust']['id'] = trust_id return scope_data @@ -76,116 +80,30 @@ def _build_token_auth(token): def _build_authentication_request(token=None, user_id=None, username=None, user_domain_id=None, user_domain_name=None, - password=None, project_id=None, - project_name=None, project_domain_id=None, - project_domain_name=None, - domain_id=None, domain_name=None): + password=None, **kwargs): """Build auth dictionary. It will create an auth dictionary based on all the arguments that it receives. """ auth_data = {} - auth_data['authentication'] = {'methods': []} + auth_data['identity'] = {'methods': []} if token: - auth_data['authentication']['methods'].append('token') - auth_data['authentication']['token'] = _build_token_auth(token) + auth_data['identity']['methods'].append('token') + auth_data['identity']['token'] = _build_token_auth(token) if user_id or username: - auth_data['authentication']['methods'].append('password') - auth_data['authentication']['password'] = _build_password_auth( + auth_data['identity']['methods'].append('password') + auth_data['identity']['password'] = _build_password_auth( user_id, username, user_domain_id, user_domain_name, password) - if project_id or project_name or domain_id or domain_name: - auth_data['scope'] = _build_auth_scope(project_id, - project_name, - project_domain_id, - project_domain_name, - domain_id, - domain_name) - return auth_data - - -class AuthTest(test_v3.RestfulTestCase): - def assertValidTokenResponse(self, r): - self.assertTrue(r.getheader('X-Subject-Token')) - token = r.body - - self.assertIn('expires', token) - self.assertIn('user', token) - self.assertEqual(self.user['id'], token['user']['id']) - self.assertEqual(self.user['name'], token['user']['name']) - self.assertEqual(self.user['domain_id'], token['user']['domain']['id']) - - return token - - def assertValidUnscopedTokenResponse(self, r): - token = self.assertValidTokenResponse(r) - - self.assertNotIn('roles', token) - self.assertNotIn('catalog', token) - self.assertNotIn('project', token) - self.assertNotIn('domain', token) - - return token - - def assertValidScopedTokenResponse(self, r): - token = self.assertValidTokenResponse(r) - - self.assertIn('catalog', token) - self.assertIn('roles', token) - self.assertTrue(token['roles']) - for role in token['roles']: - self.assertIn('id', role) - self.assertIn('name', role) - - return token - - def assertValidProjectScopedTokenResponse(self, r): - token = self.assertValidScopedTokenResponse(r) - - self.assertIn('project', token) - self.assertIn('id', token['project']) - self.assertIn('name', token['project']) - self.assertIn('domain', token['project']) - self.assertIn('id', token['project']['domain']) - self.assertIn('name', token['project']['domain']) - - self.assertEqual(self.role_id, token['roles'][0]['id']) - - return token - - def assertValidDomainScopedTokenResponse(self, r): - token = self.assertValidScopedTokenResponse(r) - - self.assertIn('domain', token) - self.assertIn('id', token['domain']) - self.assertIn('name', token['domain']) - - return token - - def assertEqualTokens(self, a, b): - """Assert that two tokens are equal. - - Compare two tokens except for their ids. This also truncates - the time in the comparison. - """ - def normalize(token): - del token['expires'] - del token['issued_at'] - return token - - self.assertCloseEnoughForGovernmentWork( - timeutils.parse_isotime(a['expires']), - timeutils.parse_isotime(b['expires'])) - self.assertCloseEnoughForGovernmentWork( - timeutils.parse_isotime(a['issued_at']), - timeutils.parse_isotime(b['issued_at'])) - return self.assertDictEqual(normalize(a), normalize(b)) + if kwargs: + auth_data['scope'] = _build_auth_scope(**kwargs) + return {'auth': auth_data} class TestAuthInfo(test.TestCase): def test_missing_auth_methods(self): - auth_data = {'authentication': {}} - auth_data['authentication']['token'] = {'id': uuid.uuid4().hex} + auth_data = {'identity': {}} + auth_data['identity']['token'] = {'id': uuid.uuid4().hex} self.assertRaises(exception.ValidationError, auth.controllers.AuthInfo, None, @@ -194,7 +112,7 @@ class TestAuthInfo(test.TestCase): def test_unsupported_auth_method(self): auth_data = {'methods': ['abc']} auth_data['abc'] = {'test': 'test'} - auth_data = {'authentication': auth_data} + auth_data = {'identity': auth_data} self.assertRaises(exception.AuthMethodNotSupported, auth.controllers.AuthInfo, None, @@ -202,7 +120,7 @@ class TestAuthInfo(test.TestCase): def test_missing_auth_method_data(self): auth_data = {'methods': ['password']} - auth_data = {'authentication': auth_data} + auth_data = {'identity': auth_data} self.assertRaises(exception.ValidationError, auth.controllers.AuthInfo, None, @@ -211,7 +129,7 @@ class TestAuthInfo(test.TestCase): def test_project_name_no_domain(self): auth_data = _build_authentication_request(username='test', password='test', - project_name='abc') + project_name='abc')['auth'] self.assertRaises(exception.ValidationError, auth.controllers.AuthInfo, None, @@ -221,14 +139,14 @@ class TestAuthInfo(test.TestCase): auth_data = _build_authentication_request(user_id='test', password='test', project_name='test', - domain_name='test') + domain_name='test')['auth'] self.assertRaises(exception.ValidationError, auth.controllers.AuthInfo, None, auth_data) -class TestTokenAPIs(AuthTest): +class TestTokenAPIs(test_v3.RestfulTestCase): def setUp(self): super(TestTokenAPIs, self).setUp() auth_data = _build_authentication_request( @@ -262,11 +180,13 @@ class TestTokenAPIs(AuthTest): method='GET') v2_token = resp.body self.assertEqual(v2_token['access']['user']['id'], - token_data['user']['id']) - self.assertEqual(v2_token['access']['token']['expires'], - token_data['expires']) + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token['access']['token']['expires'][:-1], + token_data['token']['expires_at']) self.assertEqual(v2_token['access']['user']['roles'][0]['id'], - token_data['roles'][0]['id']) + token_data['token']['roles'][0]['id']) def test_v3_v2_pki_token_intermix(self): # FIXME(gyee): PKI tokens are not interchangeable because token @@ -287,11 +207,13 @@ class TestTokenAPIs(AuthTest): method='GET') v2_token = resp.body self.assertEqual(v2_token['access']['user']['id'], - token_data['user']['id']) - self.assertEqual(v2_token['access']['token']['expires'], - token_data['expires']) + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token['access']['token']['expires'][-1], + token_data['token']['expires_at']) self.assertEqual(v2_token['access']['user']['roles'][0]['id'], - token_data['roles'][0]['id']) + token_data['token']['roles'][0]['id']) def test_v2_v3_uuid_token_intermix(self): self.opt_in_group('signing', token_format='UUID') @@ -312,11 +234,13 @@ class TestTokenAPIs(AuthTest): resp = self.get('/auth/tokens', headers=headers) token_data = resp.body self.assertEqual(v2_token_data['access']['user']['id'], - token_data['user']['id']) - self.assertEqual(v2_token_data['access']['token']['expires'], - token_data['expires']) + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token_data['access']['token']['expires'][-1], + token_data['token']['expires_at']) self.assertEqual(v2_token_data['access']['user']['roles'][0]['name'], - token_data['roles'][0]['name']) + token_data['token']['roles'][0]['name']) def test_v2_v3_pki_token_intermix(self): self.opt_in_group('signing', token_format='PKI') @@ -337,21 +261,23 @@ class TestTokenAPIs(AuthTest): resp = self.get('/auth/tokens', headers=headers) token_data = resp.body self.assertEqual(v2_token_data['access']['user']['id'], - token_data['user']['id']) - self.assertEqual(v2_token_data['access']['token']['expires'], - token_data['expires']) + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token_data['access']['token']['expires'][-1], + token_data['token']['expires_at']) self.assertEqual(v2_token_data['access']['user']['roles'][0]['name'], - token_data['roles'][0]['name']) + token_data['token']['roles'][0]['name']) def test_rescoping_token(self): - expires = self.token_data['expires'] + expires = self.token_data['token']['expires_at'] auth_data = _build_authentication_request( token=self.token, project_id=self.project_id) r = self.post('/auth/tokens', body=auth_data) self.assertValidProjectScopedTokenResponse(r) # make sure expires stayed the same - self.assertEqual(expires, r.body['expires']) + self.assertEqual(expires, r.body['token']['expires_at']) def test_check_token(self): self.head('/auth/tokens', headers=self.headers, expected_status=204) @@ -370,7 +296,9 @@ class TestTokenAPIs(AuthTest): self.assertIn('signed', r.body) -class TestAuth(AuthTest): +class TestAuthJSON(test_v3.RestfulTestCase): + content_type = 'json' + def test_unscoped_token_with_user_id(self): auth_data = _build_authentication_request( user_id=self.user['id'], @@ -601,7 +529,7 @@ class TestAuth(AuthTest): def test_remote_user(self): auth_data = _build_authentication_request( user_id=self.user['id'], - password=self.user['password']) + password=self.user['password'])['auth'] api = auth.controllers.Auth() context = {'REMOTE_USER': self.user['name']} auth_info = auth.controllers.AuthInfo(None, auth_data) @@ -612,7 +540,7 @@ class TestAuth(AuthTest): def test_remote_user_no_domain(self): auth_data = _build_authentication_request( username=self.user['name'], - password=self.user['password']) + password=self.user['password'])['auth'] api = auth.controllers.Auth() context = {'REMOTE_USER': self.user['name']} auth_info = auth.controllers.AuthInfo(None, auth_data) @@ -622,3 +550,259 @@ class TestAuth(AuthTest): context, auth_info, auth_context) + + +class TestAuthXML(TestAuthJSON): + content_type = 'xml' + + +class TestTrustAuth(test_v3.RestfulTestCase): + def setUp(self): + super(TestTrustAuth, self).setUp() + + # create a trustee to delegate stuff to + self.trustee_user_id = uuid.uuid4().hex + self.trustee_user = self.new_user_ref(domain_id=self.domain_id) + self.trustee_user['id'] = self.trustee_user_id + self.identity_api.create_user(self.trustee_user_id, self.trustee_user) + + def test_create_trust_400(self): + raise nose.exc.SkipTest('Blocked by bug 1133435') + self.post('/trusts', body={'trust': {}}, expected_status=400) + + def test_create_unscoped_trust(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id) + del ref['id'] + r = self.post('/trusts', body={'trust': ref}) + self.assertValidTrustResponse(r, ref) + + def test_trust_crud(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + del ref['id'] + r = self.post('/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + r = self.get( + '/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=200) + self.assertValidTrustResponse(r, ref) + + # validate roles on the trust + r = self.get( + '/trusts/%(trust_id)s/roles' % { + 'trust_id': trust['id']}, + expected_status=200) + roles = self.assertValidRoleListResponse(r, self.role) + self.assertIn(self.role['id'], [x['id'] for x in roles]) + self.head( + '/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}, + expected_status=204) + r = self.get( + '/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}, + expected_status=200) + self.assertValidRoleResponse(r, self.role) + + r = self.get('/trusts', expected_status=200) + self.assertValidTrustListResponse(r, trust) + + # trusts are immutable + self.patch( + '/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + body={'trust': ref}, + expected_status=404) + + self.delete( + '/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=204) + + self.get( + '/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=404) + + def test_create_trust_trustee_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=uuid.uuid4().hex) + del ref['id'] + self.post('/trusts', body={'trust': ref}, expected_status=404) + + def test_create_trust_trustor_trustee_backwards(self): + ref = self.new_trust_ref( + trustor_user_id=self.trustee_user_id, + trustee_user_id=self.user_id) + del ref['id'] + self.post('/trusts', body={'trust': ref}, expected_status=403) + + def test_create_trust_project_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=uuid.uuid4().hex, + role_ids=[self.role_id]) + del ref['id'] + self.post('/trusts', body={'trust': ref}, expected_status=404) + + def test_create_trust_role_id_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[uuid.uuid4().hex]) + del ref['id'] + self.post('/trusts', body={'trust': ref}, expected_status=404) + + def test_create_trust_role_name_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_names=[uuid.uuid4().hex]) + del ref['id'] + self.post('/trusts', body={'trust': ref}, expected_status=404) + + def test_create_expired_trust(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + expires=dict(seconds=-1), + role_ids=[self.role_id]) + del ref['id'] + r = self.post('/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + self.get('/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=404) + + auth_data = _build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.post('/auth/tokens', body=auth_data, expected_status=401) + + def test_exercise_trust_scoped_token_without_impersonation(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + + r = self.post('/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = _build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.post('/auth/tokens', body=auth_data) + self.assertValidProjectTrustScopedTokenResponse(r, self.trustee_user) + self.assertEqual(r.body['token']['user']['id'], + self.trustee_user['id']) + self.assertEqual(r.body['token']['user']['name'], + self.trustee_user['name']) + self.assertEqual(r.body['token']['user']['domain']['id'], + self.domain['id']) + self.assertEqual(r.body['token']['user']['domain']['name'], + self.domain['name']) + self.assertEqual(r.body['token']['project']['id'], self.project['id']) + self.assertEqual(r.body['token']['project']['name'], + self.project['name']) + + def test_exercise_trust_scoped_token_with_impersonation(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + + r = self.post('/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = _build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.post('/auth/tokens', body=auth_data) + self.assertValidProjectTrustScopedTokenResponse(r, self.user) + self.assertEqual(r.body['token']['user']['id'], self.user['id']) + self.assertEqual(r.body['token']['user']['name'], self.user['name']) + self.assertEqual(r.body['token']['user']['domain']['id'], + self.domain['id']) + self.assertEqual(r.body['token']['user']['domain']['name'], + self.domain['name']) + self.assertEqual(r.body['token']['project']['id'], self.project['id']) + self.assertEqual(r.body['token']['project']['name'], + self.project['name']) + + def test_delete_trust(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + + r = self.post('/trusts', body={'trust': ref}) + + trust = self.assertValidTrustResponse(r, ref) + + self.delete('/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=204) + + self.get('/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=404) + + self.get('/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=404) + + auth_data = _build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.post('/auth/tokens', body=auth_data, expected_status=401) + + def test_list_trusts(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + + for i in range(0, 3): + r = self.post('/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + r = self.get('/trusts?trustor_user_id=%s' % + self.user_id, expected_status=200) + trusts = r.body['trusts'] + self.assertEqual(len(trusts), 3) + + r = self.get('/trusts?trustee_user_id=%s' % + self.user_id, expected_status=200) + trusts = r.body['trusts'] + self.assertEqual(len(trusts), 0) diff --git a/tests/test_v3_catalog.py b/tests/test_v3_catalog.py index 9f2f4f01..2d161db5 100644 --- a/tests/test_v3_catalog.py +++ b/tests/test_v3_catalog.py @@ -23,52 +23,6 @@ class CatalogTestCase(test_v3.RestfulTestCase): self.endpoint_id, self.endpoint.copy()) - # service validation - - def assertValidServiceListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'services', - self.assertValidService, - **kwargs) - - def assertValidServiceResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'service', - self.assertValidService, - ref) - - def assertValidService(self, entity, ref=None): - self.assertIsNotNone(entity.get('type')) - if ref: - self.assertEqual(ref['type'], entity['type']) - return entity - - # endpoint validation - - def assertValidEndpointListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'endpoints', - self.assertValidEndpoint, - **kwargs) - - def assertValidEndpointResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'endpoint', - self.assertValidEndpoint, - ref) - - def assertValidEndpoint(self, entity, ref=None): - self.assertIsNotNone(entity.get('interface')) - self.assertIsNotNone(entity.get('service_id')) - if ref: - self.assertEqual(ref['interface'], entity['interface']) - self.assertEqual(ref['service_id'], entity['service_id']) - return entity - # service crud tests def test_create_service(self): diff --git a/tests/test_v3_identity.py b/tests/test_v3_identity.py index 47e50281..9ef487c3 100644 --- a/tests/test_v3_identity.py +++ b/tests/test_v3_identity.py @@ -40,145 +40,6 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.credential_id, self.credential) - # domain validation - - def assertValidDomainListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'domains', - self.assertValidDomain, - **kwargs) - - def assertValidDomainResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'domain', - self.assertValidDomain, - ref) - - def assertValidDomain(self, entity, ref=None): - if ref: - pass - return entity - - # project validation - - def assertValidProjectListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'projects', - self.assertValidProject, - **kwargs) - - def assertValidProjectResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'project', - self.assertValidProject, - ref) - - def assertValidProject(self, entity, ref=None): - self.assertIsNotNone(entity.get('domain_id')) - if ref: - self.assertEqual(ref['domain_id'], entity['domain_id']) - return entity - - # user validation - - def assertValidUserListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'users', - self.assertValidUser, - **kwargs) - - def assertValidUserResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'user', - self.assertValidUser, - ref) - - def assertValidUser(self, entity, ref=None): - self.assertIsNotNone(entity.get('domain_id')) - self.assertIsNotNone(entity.get('email')) - self.assertIsNone(entity.get('password')) - if ref: - self.assertEqual(ref['domain_id'], entity['domain_id']) - self.assertEqual(ref['email'], entity['email']) - return entity - - # group validation - - def assertValidGroupListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'groups', - self.assertValidGroup, - **kwargs) - - def assertValidGroupResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'group', - self.assertValidGroup, - ref) - - def assertValidGroup(self, entity, ref=None): - self.assertIsNotNone(entity.get('name')) - if ref: - self.assertEqual(ref['name'], entity['name']) - return entity - - # credential validation - - def assertValidCredentialListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'credentials', - self.assertValidCredential, - **kwargs) - - def assertValidCredentialResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'credential', - self.assertValidCredential, - ref) - - def assertValidCredential(self, entity, ref=None): - self.assertIsNotNone(entity.get('user_id')) - self.assertIsNotNone(entity.get('blob')) - self.assertIsNotNone(entity.get('type')) - if ref: - self.assertEqual(ref['user_id'], entity['user_id']) - self.assertEqual(ref['blob'], entity['blob']) - self.assertEqual(ref['type'], entity['type']) - self.assertEqual(ref.get('project_id'), entity.get('project_id')) - return entity - - # role validation - - def assertValidRoleListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'roles', - self.assertValidRole, - **kwargs) - - def assertValidRoleResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'role', - self.assertValidRole, - ref) - - def assertValidRole(self, entity, ref=None): - self.assertIsNotNone(entity.get('name')) - if ref: - self.assertEqual(ref['name'], entity['name']) - return entity - # domain crud tests def test_create_domain(self): diff --git a/tests/test_v3_policy.py b/tests/test_v3_policy.py index 811eb577..1af68b6e 100644 --- a/tests/test_v3_policy.py +++ b/tests/test_v3_policy.py @@ -15,30 +15,6 @@ class PolicyTestCase(test_v3.RestfulTestCase): self.policy_id, self.policy.copy()) - # policy validation - - def assertValidPolicyListResponse(self, resp, **kwargs): - return self.assertValidListResponse( - resp, - 'policies', - self.assertValidPolicy, - **kwargs) - - def assertValidPolicyResponse(self, resp, ref): - return self.assertValidResponse( - resp, - 'policy', - self.assertValidPolicy, - ref) - - def assertValidPolicy(self, entity, ref=None): - self.assertIsNotNone(entity.get('blob')) - self.assertIsNotNone(entity.get('type')) - if ref: - self.assertEqual(ref['blob'], entity['blob']) - self.assertEqual(ref['type'], entity['type']) - return entity - # policy crud tests def test_create_policy(self): diff --git a/tests/test_v3_protection.py b/tests/test_v3_protection.py index bda73415..5b461693 100644 --- a/tests/test_v3_protection.py +++ b/tests/test_v3_protection.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import tempfile import uuid @@ -82,13 +83,14 @@ class IdentityTestProtectedCase(test_v3.RestfulTestCase): # A default auth request we can use - un-scoped user token self.auth = {} - self.auth['authentication'] = {'methods': []} - self.auth['authentication']['methods'].append('password') - self.auth['authentication']['password'] = {'user': {}} - self.auth['authentication']['password']['user']['id'] = ( + self.auth['identity'] = {'methods': []} + self.auth['identity']['methods'].append('password') + self.auth['identity']['password'] = {'user': {}} + self.auth['identity']['password']['user']['id'] = ( self.user1['id']) - self.auth['authentication']['password']['user']['password'] = ( + self.auth['identity']['password']['user']['password'] = ( self.user1['password']) + self.auth = {'auth': self.auth} def tearDown(self): super(IdentityTestProtectedCase, self).tearDown() @@ -130,20 +132,25 @@ class IdentityTestProtectedCase(test_v3.RestfulTestCase): def test_list_users_protected_by_domain(self): """GET /users?domain_id=mydomain (protected)""" - raise nose.exc.SkipTest('Blocked by incomplete ' - 'domain scoping in v3/auth') # Update policy to protect by domain, and then use a domain # scoped token new_policy = """{"identity:list_users": ["domain_id:%(domain_id)s"]}""" with open(self.tmpfilename, "w") as policyfile: policyfile.write(new_policy) - self.auth['scope'] = {'domain': []} - self.auth['domain']['id'] = self.domainA['id'] + self.auth['auth']['scope'] = {'domain': {'id': self.domainA['id']}} url_by_name = '/users?domain_id=%s' % self.user1['domain_id'] r = self.get(url_by_name, auth=self.auth) # We should only get back one user, the one in DomainA id_list = self._get_id_list_from_ref_list(r.body.get('users')) - self.assertIn(self.user2['id'], id_list) + self.assertIn(self.user1['id'], id_list) - # TODO (henry-nash) Add some more tests to cover the various likely - # protection filters + def test_get_user_protected_match_id(self): + """GET /users/{id} (match payload)""" + # Tests the flattening of the payload + policy = {"identity:get_user": [["user_id:%(user_id)s"]]} + with open(self.tmpfilename, "w") as policyfile: + policyfile.write(json.dumps(policy)) + url_by_name = '/users/%s' % self.user1['id'] + r = self.get(url_by_name, auth=self.auth) + body = r.body + self.assertEquals(self.user1['id'], body['user']['id']) 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) diff --git a/tools/pip-requires b/tools/pip-requires index 688da1a7..f949c7fd 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,5 +1,5 @@ # keystone dependencies -pam==0.1.4 +pam>=0.1.4 WebOb==1.2.3 eventlet greenlet |