diff options
Diffstat (limited to 'keystone-moon/keystone')
306 files changed, 15735 insertions, 9942 deletions
diff --git a/keystone-moon/keystone/assignment/backends/ldap.py b/keystone-moon/keystone/assignment/backends/ldap.py index f93e989f..4ca66c4d 100644 --- a/keystone-moon/keystone/assignment/backends/ldap.py +++ b/keystone-moon/keystone/assignment/backends/ldap.py @@ -13,10 +13,10 @@ # under the License. from __future__ import absolute_import -import ldap as ldap import ldap.filter from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from keystone import assignment from keystone.assignment.role_backends import ldap as ldap_role @@ -25,7 +25,6 @@ from keystone.common import models from keystone import exception from keystone.i18n import _ from keystone.identity.backends import ldap as ldap_identity -from keystone.openstack.common import versionutils CONF = cfg.CONF @@ -36,7 +35,7 @@ class Assignment(assignment.Driver): @versionutils.deprecated( versionutils.deprecated.KILO, remove_in=+2, - what='keystone.assignment.backends.ldap.Assignment') + what='ldap') def __init__(self): super(Assignment, self).__init__() self.LDAP_URL = CONF.ldap.url @@ -54,10 +53,10 @@ class Assignment(assignment.Driver): self.role = RoleApi(CONF, self.user) def default_role_driver(self): - return 'keystone.assignment.role_backends.ldap.Role' + return 'ldap' def default_resource_driver(self): - return 'keystone.resource.backends.ldap.Resource' + return 'ldap' def list_role_ids_for_groups_on_project( self, groups, project_id, project_domain_id, project_parents): @@ -181,7 +180,7 @@ class Assignment(assignment.Driver): self.group._id_to_dn(group_id), role_id) # Bulk actions on User From identity - def delete_user(self, user_id): + def delete_user_assignments(self, user_id): user_dn = self.user._id_to_dn(user_id) for ref in self.role.list_global_roles_for_user(user_dn): self.role.delete_user(ref.role_dn, ref.user_dn, @@ -191,7 +190,7 @@ class Assignment(assignment.Driver): self.role.delete_user(ref.role_dn, ref.user_dn, self.role._dn_to_id(ref.role_dn)) - def delete_group(self, group_id): + def delete_group_assignments(self, group_id): """Called when the group was deleted. Any role assignments for the group should be cleaned up. @@ -277,20 +276,39 @@ class Assignment(assignment.Driver): return self._roles_from_role_dicts(metadata_ref.get('roles', []), inherited_to_projects) - def list_role_assignments(self): + def list_role_assignments(self, role_id=None, + user_id=None, group_ids=None, + domain_id=None, project_ids=None, + inherited_to_projects=None): role_assignments = [] - for a in self.role.list_role_assignments(self.project.tree_dn): - if isinstance(a, UserRoleAssociation): - assignment = { - 'role_id': self.role._dn_to_id(a.role_dn), - 'user_id': self.user._dn_to_id(a.user_dn), - 'project_id': self.project._dn_to_id(a.project_dn)} - else: - assignment = { - 'role_id': self.role._dn_to_id(a.role_dn), - 'group_id': self.group._dn_to_id(a.group_dn), - 'project_id': self.project._dn_to_id(a.project_dn)} - role_assignments.append(assignment) + + # Since the LDAP backend does not support assignments to domains, if + # the request is to filter by domain, then the answer is guaranteed + # to be an empty list. + if not domain_id: + for a in self.role.list_role_assignments(self.project.tree_dn): + if isinstance(a, UserRoleAssociation): + assignment = { + 'role_id': self.role._dn_to_id(a.role_dn), + 'user_id': self.user._dn_to_id(a.user_dn), + 'project_id': self.project._dn_to_id(a.project_dn)} + else: + assignment = { + 'role_id': self.role._dn_to_id(a.role_dn), + 'group_id': self.group._dn_to_id(a.group_dn), + 'project_id': self.project._dn_to_id(a.project_dn)} + + if role_id and assignment['role_id'] != role_id: + continue + if user_id and assignment.get('user_id') != user_id: + continue + if group_ids and assignment.get('group_id') not in group_ids: + continue + if project_ids and assignment['project_id'] not in project_ids: + continue + + role_assignments.append(assignment) + return role_assignments def delete_project_assignments(self, project_id): @@ -313,9 +331,7 @@ class ProjectApi(common_ldap.ProjectLdapStructureMixin, or self.DEFAULT_MEMBER_ATTRIBUTE) def get_user_projects(self, user_dn, associations): - """Returns list of tenants a user has access to - """ - + """Returns the list of tenants to which a user has access.""" project_ids = set() for assoc in associations: project_ids.add(self._dn_to_id(assoc.project_dn)) @@ -497,9 +513,7 @@ class RoleApi(ldap_role.RoleLdapStructureMixin, common_ldap.BaseLdap): self.id_attr: role_id}) def list_role_assignments(self, project_tree_dn): - """Returns a list of all the role assignments linked to project_tree_dn - attribute. - """ + """List the role assignments linked to project_tree_dn attribute.""" try: roles = self._ldap_get_list(project_tree_dn, ldap.SCOPE_SUBTREE, attrlist=[self.member_attribute]) diff --git a/keystone-moon/keystone/assignment/backends/sql.py b/keystone-moon/keystone/assignment/backends/sql.py index 2de6ca60..89ff64b5 100644 --- a/keystone-moon/keystone/assignment/backends/sql.py +++ b/keystone-moon/keystone/assignment/backends/sql.py @@ -14,7 +14,6 @@ from oslo_config import cfg from oslo_log import log -import six import sqlalchemy from sqlalchemy.sql.expression import false @@ -53,10 +52,10 @@ class AssignmentType(object): class Assignment(keystone_assignment.Driver): def default_role_driver(self): - return "keystone.assignment.role_backends.sql.Role" + return 'sql' def default_resource_driver(self): - return 'keystone.resource.backends.sql.Resource' + return 'sql' def list_user_ids_for_project(self, tenant_id): with sql.transaction() as session: @@ -336,7 +335,62 @@ class Assignment(keystone_assignment.Driver): 'Cannot remove role that has not been granted, %s') % role_id) - def list_role_assignments(self): + def _get_user_assignment_types(self): + return [AssignmentType.USER_PROJECT, AssignmentType.USER_DOMAIN] + + def _get_group_assignment_types(self): + return [AssignmentType.GROUP_PROJECT, AssignmentType.GROUP_DOMAIN] + + def _get_project_assignment_types(self): + return [AssignmentType.USER_PROJECT, AssignmentType.GROUP_PROJECT] + + def _get_domain_assignment_types(self): + return [AssignmentType.USER_DOMAIN, AssignmentType.GROUP_DOMAIN] + + def _get_assignment_types(self, user, group, project, domain): + """Returns a list of role assignment types based on provided entities + + If one of user or group (the "actor") as well as one of project or + domain (the "target") are provided, the list will contain the role + assignment type for that specific pair of actor and target. + + If only an actor or target is provided, the list will contain the + role assignment types that satisfy the specified entity. + + For example, if user and project are provided, the return will be: + + [AssignmentType.USER_PROJECT] + + However, if only user was provided, the return would be: + + [AssignmentType.USER_PROJECT, AssignmentType.USER_DOMAIN] + + It is not expected that user and group (or project and domain) are + specified - but if they are, the most fine-grained value will be + chosen (i.e. user over group, project over domain). + + """ + actor_types = [] + if user: + actor_types = self._get_user_assignment_types() + elif group: + actor_types = self._get_group_assignment_types() + + target_types = [] + if project: + target_types = self._get_project_assignment_types() + elif domain: + target_types = self._get_domain_assignment_types() + + if actor_types and target_types: + return list(set(actor_types).intersection(target_types)) + + return actor_types or target_types + + def list_role_assignments(self, role_id=None, + user_id=None, group_ids=None, + domain_id=None, project_ids=None, + inherited_to_projects=None): def denormalize_role(ref): assignment = {} @@ -362,8 +416,35 @@ class Assignment(keystone_assignment.Driver): return assignment with sql.transaction() as session: - refs = session.query(RoleAssignment).all() - return [denormalize_role(ref) for ref in refs] + assignment_types = self._get_assignment_types( + user_id, group_ids, project_ids, domain_id) + + targets = None + if project_ids: + targets = project_ids + elif domain_id: + targets = [domain_id] + + actors = None + if group_ids: + actors = group_ids + elif user_id: + actors = [user_id] + + query = session.query(RoleAssignment) + + if role_id: + query = query.filter_by(role_id=role_id) + if actors: + query = query.filter(RoleAssignment.actor_id.in_(actors)) + if targets: + query = query.filter(RoleAssignment.target_id.in_(targets)) + if assignment_types: + query = query.filter(RoleAssignment.type.in_(assignment_types)) + if inherited_to_projects is not None: + query = query.filter_by(inherited=inherited_to_projects) + + return [denormalize_role(ref) for ref in query.all()] def delete_project_assignments(self, project_id): with sql.transaction() as session: @@ -377,13 +458,13 @@ class Assignment(keystone_assignment.Driver): q = q.filter_by(role_id=role_id) q.delete(False) - def delete_user(self, user_id): + def delete_user_assignments(self, user_id): with sql.transaction() as session: q = session.query(RoleAssignment) q = q.filter_by(actor_id=user_id) q.delete(False) - def delete_group(self, group_id): + def delete_group_assignments(self, group_id): with sql.transaction() as session: q = session.query(RoleAssignment) q = q.filter_by(actor_id=group_id) @@ -399,12 +480,15 @@ class RoleAssignment(sql.ModelBase, sql.DictBase): AssignmentType.USER_DOMAIN, AssignmentType.GROUP_DOMAIN, name='type'), nullable=False) - actor_id = sql.Column(sql.String(64), nullable=False, index=True) + actor_id = sql.Column(sql.String(64), nullable=False) target_id = sql.Column(sql.String(64), nullable=False) role_id = sql.Column(sql.String(64), nullable=False) inherited = sql.Column(sql.Boolean, default=False, nullable=False) - __table_args__ = (sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', - 'role_id'), {}) + __table_args__ = ( + sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', 'role_id', + 'inherited'), + sql.Index('ix_actor_id', 'actor_id'), + ) def to_dict(self): """Override parent to_dict() method with a simpler implementation. @@ -412,4 +496,4 @@ class RoleAssignment(sql.ModelBase, sql.DictBase): RoleAssignment doesn't have non-indexed 'extra' attributes, so the parent implementation is not applicable. """ - return dict(six.iteritems(self)) + return dict(self.items()) diff --git a/keystone-moon/keystone/assignment/controllers.py b/keystone-moon/keystone/assignment/controllers.py index ff27fd36..d33dce70 100644 --- a/keystone-moon/keystone/assignment/controllers.py +++ b/keystone-moon/keystone/assignment/controllers.py @@ -15,7 +15,6 @@ """Workflow Logic the Assignment service.""" -import copy import functools import uuid @@ -26,10 +25,10 @@ from six.moves import urllib from keystone.assignment import schema from keystone.common import controller from keystone.common import dependency +from keystone.common import utils from keystone.common import validation from keystone import exception -from keystone.i18n import _, _LW -from keystone.models import token_model +from keystone.i18n import _ from keystone import notifications @@ -51,18 +50,11 @@ class TenantAssignment(controller.V2Controller): Doesn't care about token scopedness. """ - try: - token_data = self.token_provider_api.validate_token( - context['token_id']) - token_ref = token_model.KeystoneToken(token_id=context['token_id'], - token_data=token_data) - except exception.NotFound as e: - LOG.warning(_LW('Authentication failed: %s'), e) - raise exception.Unauthorized(e) + token_ref = utils.get_token_ref(context) tenant_refs = ( self.assignment_api.list_projects_for_user(token_ref.user_id)) - tenant_refs = [self.filter_domain_id(ref) for ref in tenant_refs + tenant_refs = [self.v3_to_v2_project(ref) for ref in tenant_refs if ref['domain_id'] == CONF.identity.default_domain_id] params = { 'limit': context['query_string'].get('limit'), @@ -107,7 +99,14 @@ class Role(controller.V2Controller): msg = _('Name field is required and cannot be empty') raise exception.ValidationError(message=msg) - role_id = uuid.uuid4().hex + if role['name'] == CONF.member_role_name: + # Use the configured member role ID when creating the configured + # member role name. This avoids the potential of creating a + # "member" role with an unexpected ID. + role_id = CONF.member_role_id + else: + role_id = uuid.uuid4().hex + role['id'] = role_id role_ref = self.role_api.create_role(role_id, role) return {'role': role_ref} @@ -152,8 +151,8 @@ class RoleAssignmentV2(controller.V2Controller): """ self.assert_admin(context) if tenant_id is None: - raise exception.NotImplemented(message='User roles not supported: ' - 'tenant_id required') + raise exception.NotImplemented( + message=_('User roles not supported: tenant_id required')) self.assignment_api.add_role_to_user_and_project( user_id, tenant_id, role_id) @@ -171,8 +170,8 @@ class RoleAssignmentV2(controller.V2Controller): """ self.assert_admin(context) if tenant_id is None: - raise exception.NotImplemented(message='User roles not supported: ' - 'tenant_id required') + raise exception.NotImplemented( + message=_('User roles not supported: tenant_id required')) # This still has the weird legacy semantics that adding a role to # a user also adds them to a tenant, so we must follow up on that @@ -282,7 +281,16 @@ class RoleV3(controller.V3Controller): @controller.protected() @validation.validated(schema.role_create, 'role') def create_role(self, context, role): - ref = self._assign_unique_id(self._normalize_dict(role)) + if role['name'] == CONF.member_role_name: + # Use the configured member role ID when creating the configured + # member role name. This avoids the potential of creating a + # "member" role with an unexpected ID. + role['id'] = CONF.member_role_id + else: + role = self._assign_unique_id(role) + + ref = self._normalize_dict(role) + initiator = notifications._get_request_audit_info(context) ref = self.role_api.create_role(ref['id'], ref, initiator) return RoleV3.wrap_member(context, ref) @@ -452,16 +460,25 @@ class RoleAssignmentV3(controller.V3Controller): actor (e.g. user or group), target (e.g. domain or project) and role. If it is an inherited role, then this is also indicated. Examples: + For a non-inherited expanded assignment from group membership: {'user_id': user_id, - 'project_id': domain_id, - 'role_id': role_id} + 'project_id': project_id, + 'role_id': role_id, + 'indirect': {'group_id': group_id}} - or, for an inherited role: + or, for a project inherited role: {'user_id': user_id, - 'domain_id': domain_id, + 'project_id': project_id, 'role_id': role_id, - 'inherited_to_projects': true} + 'indirect': {'project_id': parent_id}} + + It is possible to deduce if a role assignment came from group + membership if it has both 'user_id' in the main body of the dict and + 'group_id' in the 'indirect' subdict, as well as it is possible to + deduce if it has come from inheritance if it contains both a + 'project_id' in the main body of the dict and 'parent_id' in the + 'indirect' subdict. This function maps this into the format to be returned via the API, e.g. for the second example above: @@ -471,262 +488,71 @@ class RoleAssignmentV3(controller.V3Controller): {'id': user_id} }, 'scope': { - 'domain': { - {'id': domain_id} + 'project': { + {'id': project_id} }, - 'OS-INHERIT:inherited_to': 'projects + 'OS-INHERIT:inherited_to': 'projects' }, 'role': { {'id': role_id} }, 'links': { - 'assignment': '/domains/domain_id/users/user_id/roles/' - 'role_id/inherited_to_projects' + 'assignment': '/OS-INHERIT/projects/parent_id/users/user_id/' + 'roles/role_id/inherited_to_projects' } } """ - formatted_entity = {} - suffix = "" - if 'user_id' in entity: - formatted_entity['user'] = {'id': entity['user_id']} - actor_link = 'users/%s' % entity['user_id'] - if 'group_id' in entity: - formatted_entity['group'] = {'id': entity['group_id']} - actor_link = 'groups/%s' % entity['group_id'] - if 'role_id' in entity: - formatted_entity['role'] = {'id': entity['role_id']} + formatted_entity = {'links': {}} + inherited_assignment = entity.get('inherited_to_projects') + if 'project_id' in entity: formatted_entity['scope'] = ( {'project': {'id': entity['project_id']}}) - if 'inherited_to_projects' in entity: - formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( - 'projects') - target_link = '/OS-INHERIT/projects/%s' % entity['project_id'] - suffix = '/inherited_to_projects' - else: - target_link = '/projects/%s' % entity['project_id'] - if 'domain_id' in entity: - formatted_entity['scope'] = ( - {'domain': {'id': entity['domain_id']}}) - if 'inherited_to_projects' in entity: - formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( - 'projects') - target_link = '/OS-INHERIT/domains/%s' % entity['domain_id'] - suffix = '/inherited_to_projects' - else: - target_link = '/domains/%s' % entity['domain_id'] - formatted_entity.setdefault('links', {}) - - path = '%(target)s/%(actor)s/roles/%(role)s%(suffix)s' % { - 'target': target_link, - 'actor': actor_link, - 'role': entity['role_id'], - 'suffix': suffix} - formatted_entity['links']['assignment'] = self.base_url(context, path) - - return formatted_entity - def _expand_indirect_assignments(self, context, refs): - """Processes entity list into all-direct assignments. - - For any group role assignments in the list, create a role assignment - entity for each member of that group, and then remove the group - assignment entity itself from the list. + if 'domain_id' in entity.get('indirect', {}): + inherited_assignment = True + formatted_link = ('/domains/%s' % + entity['indirect']['domain_id']) + elif 'project_id' in entity.get('indirect', {}): + inherited_assignment = True + formatted_link = ('/projects/%s' % + entity['indirect']['project_id']) + else: + formatted_link = '/projects/%s' % entity['project_id'] + elif 'domain_id' in entity: + formatted_entity['scope'] = {'domain': {'id': entity['domain_id']}} + formatted_link = '/domains/%s' % entity['domain_id'] - If the OS-INHERIT extension is enabled, then honor any inherited - roles on the domain by creating the equivalent on all projects - owned by the domain. + if 'user_id' in entity: + formatted_entity['user'] = {'id': entity['user_id']} - For any new entity created by virtue of group membership, add in an - additional link to that membership. + if 'group_id' in entity.get('indirect', {}): + membership_url = ( + self.base_url(context, '/groups/%s/users/%s' % ( + entity['indirect']['group_id'], entity['user_id']))) + formatted_entity['links']['membership'] = membership_url + formatted_link += '/groups/%s' % entity['indirect']['group_id'] + else: + formatted_link += '/users/%s' % entity['user_id'] + elif 'group_id' in entity: + formatted_entity['group'] = {'id': entity['group_id']} + formatted_link += '/groups/%s' % entity['group_id'] - """ - def _get_group_members(ref): - """Get a list of group members. + formatted_entity['role'] = {'id': entity['role_id']} + formatted_link += '/roles/%s' % entity['role_id'] - Get the list of group members. If this fails with - GroupNotFound, then log this as a warning, but allow - overall processing to continue. + if inherited_assignment: + formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( + 'projects') + formatted_link = ('/OS-INHERIT%s/inherited_to_projects' % + formatted_link) - """ - try: - members = self.identity_api.list_users_in_group( - ref['group']['id']) - except exception.GroupNotFound: - members = [] - # The group is missing, which should not happen since - # group deletion should remove any related assignments, so - # log a warning - target = 'Unknown' - # Should always be a domain or project, but since to get - # here things have gone astray, let's be cautious. - if 'scope' in ref: - if 'domain' in ref['scope']: - dom_id = ref['scope']['domain'].get('id', 'Unknown') - target = 'Domain: %s' % dom_id - elif 'project' in ref['scope']: - proj_id = ref['scope']['project'].get('id', 'Unknown') - target = 'Project: %s' % proj_id - role_id = 'Unknown' - if 'role' in ref and 'id' in ref['role']: - role_id = ref['role']['id'] - LOG.warning( - _LW('Group %(group)s not found for role-assignment - ' - '%(target)s with Role: %(role)s'), { - 'group': ref['group']['id'], 'target': target, - 'role': role_id}) - return members - - def _build_user_assignment_equivalent_of_group( - user, group_id, template): - """Create a user assignment equivalent to the group one. - - The template has had the 'group' entity removed, so - substitute a 'user' one. The 'assignment' link stays as it is, - referring to the group assignment that led to this role. - A 'membership' link is added that refers to this particular - user's membership of this group. - - """ - user_entry = copy.deepcopy(template) - user_entry['user'] = {'id': user['id']} - user_entry['links']['membership'] = ( - self.base_url(context, '/groups/%s/users/%s' % - (group_id, user['id']))) - return user_entry - - def _build_project_equivalent_of_user_target_role( - project_id, target_id, target_type, template): - """Create a user project assignment equivalent to the domain one. - - The template has had the 'domain' entity removed, so - substitute a 'project' one, modifying the 'assignment' link - to match. - - """ - project_entry = copy.deepcopy(template) - project_entry['scope']['project'] = {'id': project_id} - project_entry['links']['assignment'] = ( - self.base_url( - context, - '/OS-INHERIT/%s/%s/users/%s/roles/%s' - '/inherited_to_projects' % ( - target_type, target_id, project_entry['user']['id'], - project_entry['role']['id']))) - return project_entry - - def _build_project_equivalent_of_group_target_role( - user_id, group_id, project_id, - target_id, target_type, template): - """Create a user project equivalent to the domain group one. - - The template has had the 'domain' and 'group' entities removed, so - substitute a 'user-project' one, modifying the 'assignment' link - to match. - - """ - project_entry = copy.deepcopy(template) - project_entry['user'] = {'id': user_id} - project_entry['scope']['project'] = {'id': project_id} - project_entry['links']['assignment'] = ( - self.base_url(context, - '/OS-INHERIT/%s/%s/groups/%s/roles/%s' - '/inherited_to_projects' % ( - target_type, target_id, group_id, - project_entry['role']['id']))) - project_entry['links']['membership'] = ( - self.base_url(context, '/groups/%s/users/%s' % - (group_id, user_id))) - return project_entry - - # Scan the list of entities for any assignments that need to be - # expanded. - # - # If the OS-INERIT extension is enabled, the refs lists may - # contain roles to be inherited from domain to project, so expand - # these as well into project equivalents - # - # For any regular group entries, expand these into user entries based - # on membership of that group. - # - # Due to the potentially large expansions, rather than modify the - # list we are enumerating, we build a new one as we go. - # - - new_refs = [] - for r in refs: - if 'OS-INHERIT:inherited_to' in r['scope']: - if 'domain' in r['scope']: - # It's an inherited domain role - so get the list of - # projects owned by this domain. - project_ids = ( - [x['id'] for x in - self.resource_api.list_projects_in_domain( - r['scope']['domain']['id'])]) - base_entry = copy.deepcopy(r) - target_type = 'domains' - target_id = base_entry['scope']['domain']['id'] - base_entry['scope'].pop('domain') - else: - # It's an inherited project role - so get the list of - # projects in this project subtree. - project_id = r['scope']['project']['id'] - project_ids = ( - [x['id'] for x in - self.resource_api.list_projects_in_subtree( - project_id)]) - base_entry = copy.deepcopy(r) - target_type = 'projects' - target_id = base_entry['scope']['project']['id'] - base_entry['scope'].pop('project') - - # For each project, create an equivalent role assignment - for p in project_ids: - # If it's a group assignment, then create equivalent user - # roles based on membership of the group - if 'group' in base_entry: - members = _get_group_members(base_entry) - sub_entry = copy.deepcopy(base_entry) - group_id = sub_entry['group']['id'] - sub_entry.pop('group') - for m in members: - new_entry = ( - _build_project_equivalent_of_group_target_role( - m['id'], group_id, p, - target_id, target_type, sub_entry)) - new_refs.append(new_entry) - else: - new_entry = ( - _build_project_equivalent_of_user_target_role( - p, target_id, target_type, base_entry)) - new_refs.append(new_entry) - elif 'group' in r: - # It's a non-inherited group role assignment, so get the list - # of members. - members = _get_group_members(r) - - # Now replace that group role assignment entry with an - # equivalent user role assignment for each of the group members - base_entry = copy.deepcopy(r) - group_id = base_entry['group']['id'] - base_entry.pop('group') - for m in members: - user_entry = _build_user_assignment_equivalent_of_group( - m, group_id, base_entry) - new_refs.append(user_entry) - else: - new_refs.append(r) + formatted_entity['links']['assignment'] = self.base_url(context, + formatted_link) - return new_refs - - def _filter_inherited(self, entry): - if ('inherited_to_projects' in entry and - not CONF.os_inherit.enabled): - return False - else: - return True + return formatted_entity def _assert_effective_filters(self, inherited, group, domain): """Assert that useless filter combinations are avoided. @@ -762,13 +588,28 @@ class RoleAssignmentV3(controller.V3Controller): 'scope.domain.id', 'scope.project.id', 'scope.OS-INHERIT:inherited_to', 'user.id') def list_role_assignments(self, context, filters): + """List role assignments to user and groups on domains and projects. + + Return a list of all existing role assignments in the system, filtered + by assignments attributes, if provided. - # TODO(henry-nash): This implementation uses the standard filtering - # in the V3.wrap_collection. Given the large number of individual - # assignments, this is pretty inefficient. An alternative would be - # to pass the filters into the driver call, so that the list size is - # kept a minimum. + If effective option is used and OS-INHERIT extension is enabled, the + following functions will be applied: + 1) For any group role assignment on a target, replace it by a set of + role assignments containing one for each user of that group on that + target; + 2) For any inherited role assignment for an actor on a target, replace + it by a set of role assignments for that actor on every project under + that target. + It means that, if effective mode is used, no group or domain inherited + assignments will be present in the resultant list. Thus, combining + effective with them is invalid. + + As a role assignment contains only one actor and one target, providing + both user and group ids or domain and project ids is invalid as well. + + """ params = context['query_string'] effective = 'effective' in params and ( self.query_filter_is_true(params['effective'])) @@ -791,17 +632,17 @@ class RoleAssignmentV3(controller.V3Controller): domain=params.get( 'scope.domain.id')) - hints = self.build_driver_hints(context, filters) - refs = self.assignment_api.list_role_assignments() - formatted_refs = ( - [self._format_entity(context, x) for x in refs - if self._filter_inherited(x)]) + refs = self.assignment_api.list_role_assignments( + role_id=params.get('role.id'), + user_id=params.get('user.id'), + group_id=params.get('group.id'), + domain_id=params.get('scope.domain.id'), + project_id=params.get('scope.project.id'), + inherited=inherited, effective=effective) - if effective: - formatted_refs = self._expand_indirect_assignments(context, - formatted_refs) + formatted_refs = [self._format_entity(context, ref) for ref in refs] - return self.wrap_collection(context, formatted_refs, hints=hints) + return self.wrap_collection(context, formatted_refs) @controller.protected() def get_role_assignment(self, context): diff --git a/keystone-moon/keystone/assignment/core.py b/keystone-moon/keystone/assignment/core.py index 0f9c03e9..a001e6b1 100644 --- a/keystone-moon/keystone/assignment/core.py +++ b/keystone-moon/keystone/assignment/core.py @@ -12,9 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. -"""Main entry point into the assignment service.""" +"""Main entry point into the Assignment service.""" import abc +import copy from oslo_config import cfg from oslo_log import log @@ -28,7 +29,6 @@ from keystone import exception from keystone.i18n import _ from keystone.i18n import _LI from keystone import notifications -from keystone.openstack.common import versionutils CONF = cfg.CONF @@ -36,40 +36,6 @@ LOG = log.getLogger(__name__) MEMOIZE = cache.get_memoization_decorator(section='role') -def deprecated_to_role_api(f): - """Specialized deprecation wrapper for assignment to role api. - - This wraps the standard deprecation wrapper and fills in the method - names automatically. - - """ - @six.wraps(f) - def wrapper(*args, **kwargs): - x = versionutils.deprecated( - what='assignment.' + f.__name__ + '()', - as_of=versionutils.deprecated.KILO, - in_favor_of='role.' + f.__name__ + '()') - return x(f) - return wrapper() - - -def deprecated_to_resource_api(f): - """Specialized deprecation wrapper for assignment to resource api. - - This wraps the standard deprecation wrapper and fills in the method - names automatically. - - """ - @six.wraps(f) - def wrapper(*args, **kwargs): - x = versionutils.deprecated( - what='assignment.' + f.__name__ + '()', - as_of=versionutils.deprecated.KILO, - in_favor_of='resource.' + f.__name__ + '()') - return x(f) - return wrapper() - - @dependency.provider('assignment_api') @dependency.requires('credential_api', 'identity_api', 'resource_api', 'revoke_api', 'role_api') @@ -80,6 +46,9 @@ class Manager(manager.Manager): dynamically calls the backend. """ + + driver_namespace = 'keystone.assignment' + _PROJECT = 'project' _ROLE_REMOVED_FROM_USER = 'role_removed_from_user' _INVALIDATION_USER_PROJECT_TOKENS = 'invalidate_user_project_tokens' @@ -129,7 +98,7 @@ class Manager(manager.Manager): """ def _get_group_project_roles(user_id, project_ref): group_ids = self._get_group_ids_for_user_id(user_id) - return self.driver.list_role_ids_for_groups_on_project( + return self.list_role_ids_for_groups_on_project( group_ids, project_ref['id'], project_ref['domain_id'], @@ -155,7 +124,8 @@ class Manager(manager.Manager): except (exception.MetadataNotFound, exception.NotImplemented): pass # As well inherited roles from parent projects - for p in self.list_project_parents(project_ref['id']): + for p in self.resource_api.list_project_parents( + project_ref['id']): p_roles = self.list_grants( user_id=user_id, project_id=p['id'], inherited_to_projects=True) @@ -207,7 +177,7 @@ class Manager(manager.Manager): return self._roles_from_role_dicts( metadata_ref.get('roles', {}), False) - self.get_domain(domain_id) + self.resource_api.get_domain(domain_id) user_role_list = _get_user_domain_roles(user_id, domain_id) group_role_list = _get_group_domain_roles(user_id, domain_id) # Use set() to process the list to remove any duplicates @@ -218,11 +188,11 @@ class Manager(manager.Manager): if project_id is not None: project = self.resource_api.get_project(project_id) - role_ids = self.driver.list_role_ids_for_groups_on_project( + role_ids = self.list_role_ids_for_groups_on_project( group_ids, project_id, project['domain_id'], self._list_parent_ids_of_project(project_id)) elif domain_id is not None: - role_ids = self.driver.list_role_ids_for_groups_on_domain( + role_ids = self.list_role_ids_for_groups_on_domain( group_ids, domain_id) else: raise AttributeError(_("Must specify either domain or project")) @@ -261,10 +231,24 @@ class Manager(manager.Manager): tenant_id, CONF.member_role_id) - def add_role_to_user_and_project(self, user_id, tenant_id, role_id): - self.resource_api.get_project(tenant_id) + @notifications.role_assignment('created') + def _add_role_to_user_and_project_adapter(self, role_id, user_id=None, + group_id=None, domain_id=None, + project_id=None, + inherited_to_projects=False, + context=None): + + # The parameters for this method must match the parameters for + # create_grant so that the notifications.role_assignment decorator + # will work. + + self.resource_api.get_project(project_id) self.role_api.get_role(role_id) - self.driver.add_role_to_user_and_project(user_id, tenant_id, role_id) + self.driver.add_role_to_user_and_project(user_id, project_id, role_id) + + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + self._add_role_to_user_and_project_adapter( + role_id, user_id=user_id, project_id=tenant_id) def remove_user_from_project(self, tenant_id, user_id): """Remove user from a tenant @@ -299,7 +283,7 @@ class Manager(manager.Manager): # optimization with the various backend technologies (SQL, LDAP etc.). group_ids = self._get_group_ids_for_user_id(user_id) - project_ids = self.driver.list_project_ids_for_user( + project_ids = self.list_project_ids_for_user( user_id, group_ids, hints or driver_hints.Hints()) if not CONF.os_inherit.enabled: @@ -309,7 +293,7 @@ class Manager(manager.Manager): # inherited role (direct or group) on any parent project, in which # case we must add in all the projects in that parent's subtree. project_ids = set(project_ids) - project_ids_inherited = self.driver.list_project_ids_for_user( + project_ids_inherited = self.list_project_ids_for_user( user_id, group_ids, hints or driver_hints.Hints(), inherited=True) for proj_id in project_ids_inherited: project_ids.update( @@ -317,7 +301,7 @@ class Manager(manager.Manager): self.resource_api.list_projects_in_subtree(proj_id))) # Now do the same for any domain inherited roles - domain_ids = self.driver.list_domain_ids_for_user( + domain_ids = self.list_domain_ids_for_user( user_id, group_ids, hints or driver_hints.Hints(), inherited=True) project_ids.update( @@ -335,33 +319,42 @@ class Manager(manager.Manager): # projects for a user is pushed down into the driver to enable # optimization with the various backend technologies (SQL, LDAP etc.). group_ids = self._get_group_ids_for_user_id(user_id) - domain_ids = self.driver.list_domain_ids_for_user( + domain_ids = self.list_domain_ids_for_user( user_id, group_ids, hints or driver_hints.Hints()) return self.resource_api.list_domains_from_ids(domain_ids) def list_domains_for_groups(self, group_ids): - domain_ids = self.driver.list_domain_ids_for_groups(group_ids) + domain_ids = self.list_domain_ids_for_groups(group_ids) return self.resource_api.list_domains_from_ids(domain_ids) def list_projects_for_groups(self, group_ids): project_ids = ( - self.driver.list_project_ids_for_groups(group_ids, - driver_hints.Hints())) + self.list_project_ids_for_groups(group_ids, driver_hints.Hints())) if not CONF.os_inherit.enabled: return self.resource_api.list_projects_from_ids(project_ids) - # Inherited roles are enabled, so check to see if these groups have any - # roles on any domain, in which case we must add in all the projects - # in that domain. + # os_inherit extension is enabled, so check to see if these groups have + # any inherited role assignment on: i) any domain, in which case we + # must add in all the projects in that domain; ii) any project, in + # which case we must add in all the subprojects under that project in + # the hierarchy. - domain_ids = self.driver.list_domain_ids_for_groups( - group_ids, inherited=True) + domain_ids = self.list_domain_ids_for_groups(group_ids, inherited=True) project_ids_from_domains = ( self.resource_api.list_project_ids_from_domain_ids(domain_ids)) + parents_ids = self.list_project_ids_for_groups(group_ids, + driver_hints.Hints(), + inherited=True) + + subproject_ids = [] + for parent_id in parents_ids: + subtree = self.resource_api.list_projects_in_subtree(parent_id) + subproject_ids += [subproject['id'] for subproject in subtree] + return self.resource_api.list_projects_from_ids( - list(set(project_ids + project_ids_from_domains))) + list(set(project_ids + project_ids_from_domains + subproject_ids))) def list_role_assignments_for_role(self, role_id=None): # NOTE(henry-nash): Currently the efficiency of the key driver @@ -374,17 +367,37 @@ class Manager(manager.Manager): return [r for r in self.driver.list_role_assignments() if r['role_id'] == role_id] - def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): - self.driver.remove_role_from_user_and_project(user_id, tenant_id, + @notifications.role_assignment('deleted') + def _remove_role_from_user_and_project_adapter(self, role_id, user_id=None, + group_id=None, + domain_id=None, + project_id=None, + inherited_to_projects=False, + context=None): + + # The parameters for this method must match the parameters for + # delete_grant so that the notifications.role_assignment decorator + # will work. + + self.driver.remove_role_from_user_and_project(user_id, project_id, role_id) self.identity_api.emit_invalidate_user_token_persistence(user_id) self.revoke_api.revoke_by_grant(role_id, user_id=user_id, - project_id=tenant_id) + project_id=project_id) + + def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): + self._remove_role_from_user_and_project_adapter( + role_id, user_id=user_id, project_id=tenant_id) @notifications.internal(notifications.INVALIDATE_USER_TOKEN_PERSISTENCE) def _emit_invalidate_user_token_persistence(self, user_id): self.identity_api.emit_invalidate_user_token_persistence(user_id) + def _emit_invalidate_grant_token_persistence(self, user_id, project_id): + self.identity_api.emit_invalidate_grant_token_persistence( + {'user_id': user_id, 'project_id': project_id} + ) + @notifications.role_assignment('created') def create_grant(self, role_id, user_id=None, group_id=None, domain_id=None, project_id=None, @@ -405,7 +418,7 @@ class Manager(manager.Manager): self.resource_api.get_domain(domain_id) if project_id: self.resource_api.get_project(project_id) - self.driver.check_grant_role_id( + self.check_grant_role_id( role_id, user_id, group_id, domain_id, project_id, inherited_to_projects) return role_ref @@ -417,11 +430,15 @@ class Manager(manager.Manager): self.resource_api.get_domain(domain_id) if project_id: self.resource_api.get_project(project_id) - grant_ids = self.driver.list_grant_role_ids( + grant_ids = self.list_grant_role_ids( user_id, group_id, domain_id, project_id, inherited_to_projects) return self.role_api.list_roles_from_ids(grant_ids) @notifications.role_assignment('deleted') + def _emit_revoke_user_grant(self, role_id, user_id, domain_id, project_id, + inherited_to_projects, context): + self._emit_invalidate_grant_token_persistence(user_id, project_id) + def delete_grant(self, role_id, user_id=None, group_id=None, domain_id=None, project_id=None, inherited_to_projects=False, context=None): @@ -430,17 +447,29 @@ class Manager(manager.Manager): role_id=role_id, domain_id=domain_id, project_id=project_id) + self._emit_revoke_user_grant( + role_id, user_id, domain_id, project_id, + inherited_to_projects, context) else: try: - # NOTE(morganfainberg): The user ids are the important part - # for invalidating tokens below, so extract them here. - for user in self.identity_api.list_users_in_group(group_id): - if user['id'] != user_id: - self._emit_invalidate_user_token_persistence( - user['id']) - self.revoke_api.revoke_by_grant( - user_id=user['id'], role_id=role_id, - domain_id=domain_id, project_id=project_id) + # Group may contain a lot of users so revocation will be + # by role & domain/project + if domain_id is None: + self.revoke_api.revoke_by_project_role_assignment( + project_id, role_id + ) + else: + self.revoke_api.revoke_by_domain_role_assignment( + domain_id, role_id + ) + if CONF.token.revoke_by_id: + # NOTE(morganfainberg): The user ids are the important part + # for invalidating tokens below, so extract them here. + for user in self.identity_api.list_users_in_group( + group_id): + self._emit_revoke_user_grant( + role_id, user['id'], domain_id, project_id, + inherited_to_projects, context) except exception.GroupNotFound: LOG.debug('Group %s not found, no tokens to invalidate.', group_id) @@ -457,8 +486,356 @@ class Manager(manager.Manager): self.resource_api.get_project(project_id) self.driver.delete_grant(role_id, user_id, group_id, domain_id, project_id, inherited_to_projects) - if user_id is not None: - self._emit_invalidate_user_token_persistence(user_id) + + # The methods _expand_indirect_assignment, _list_direct_role_assignments + # and _list_effective_role_assignments below are only used on + # list_role_assignments, but they are not in its scope as nested functions + # since it would significantly increase McCabe complexity, that should be + # kept as it is in order to detect unnecessarily complex code, which is not + # this case. + + def _expand_indirect_assignment(self, ref, user_id=None, + project_id=None): + """Returns a list of expanded role assignments. + + This methods is called for each discovered assignment that either needs + a group assignment expanded into individual user assignments, or needs + an inherited assignment to be applied to its children. + + In all cases, if either user_id and/or project_id is specified, then we + filter the result on those values. + + """ + + def create_group_assignment(base_ref, user_id): + """Creates a group assignment from the provided ref.""" + + ref = copy.deepcopy(base_ref) + + ref['user_id'] = user_id + + indirect = ref.setdefault('indirect', {}) + indirect['group_id'] = ref.pop('group_id') + + return ref + + def expand_group_assignment(ref, user_id): + """Expands group role assignment. + + For any group role assignment on a target, it is replaced by a list + of role assignments containing one for each user of that group on + that target. + + An example of accepted ref is: + + { + 'group_id': group_id, + 'project_id': project_id, + 'role_id': role_id + } + + Once expanded, it should be returned as a list of entities like the + one below, one for each each user_id in the provided group_id. + + { + 'user_id': user_id, + 'project_id': project_id, + 'role_id': role_id, + 'indirect' : { + 'group_id': group_id + } + } + + Returned list will be formatted by the Controller, which will + deduce a role assignment came from group membership if it has both + 'user_id' in the main body of the dict and 'group_id' in indirect + subdict. + + """ + if user_id: + return [create_group_assignment(ref, user_id=user_id)] + + return [create_group_assignment(ref, user_id=m['id']) + for m in self.identity_api.list_users_in_group( + ref['group_id'])] + + def expand_inherited_assignment(ref, user_id, project_id=None): + """Expands inherited role assignments. + + If this is a group role assignment on a target, replace it by a + list of role assignments containing one for each user of that + group, on every project under that target. + + If this is a user role assignment on a target, replace it by a + list of role assignments for that user on every project under + that target. + + An example of accepted ref is: + + { + 'group_id': group_id, + 'project_id': parent_id, + 'role_id': role_id, + 'inherited_to_projects': 'projects' + } + + Once expanded, it should be returned as a list of entities like the + one below, one for each each user_id in the provided group_id and + for each subproject_id in the project_id subtree. + + { + 'user_id': user_id, + 'project_id': subproject_id, + 'role_id': role_id, + 'indirect' : { + 'group_id': group_id, + 'project_id': parent_id + } + } + + Returned list will be formatted by the Controller, which will + deduce a role assignment came from group membership if it has both + 'user_id' in the main body of the dict and 'group_id' in the + 'indirect' subdict, as well as it is possible to deduce if it has + come from inheritance if it contains both a 'project_id' in the + main body of the dict and 'parent_id' in the 'indirect' subdict. + + """ + def create_inherited_assignment(base_ref, project_id): + """Creates a project assignment from the provided ref. + + base_ref can either be a project or domain inherited + assignment ref. + + """ + ref = copy.deepcopy(base_ref) + + indirect = ref.setdefault('indirect', {}) + if ref.get('project_id'): + indirect['project_id'] = ref.pop('project_id') + else: + indirect['domain_id'] = ref.pop('domain_id') + + ref['project_id'] = project_id + ref.pop('inherited_to_projects') + + return ref + + # Define expanded project list to which to apply this assignment + if project_id: + # Since ref is an inherited assignment, it must have come from + # the domain or a parent. We only need apply it to the project + # requested. + project_ids = [project_id] + elif ref.get('domain_id'): + # A domain inherited assignment, so apply it to all projects + # in this domain + project_ids = ( + [x['id'] for x in + self.resource_api.list_projects_in_domain( + ref['domain_id'])]) + else: + # It must be a project assignment, so apply it to the subtree + project_ids = ( + [x['id'] for x in + self.resource_api.list_projects_in_subtree( + ref['project_id'])]) + + new_refs = [] + if 'group_id' in ref: + # Expand role assignment for all members and for all projects + for ref in expand_group_assignment(ref, user_id): + new_refs += [create_inherited_assignment(ref, proj_id) + for proj_id in project_ids] + else: + # Expand role assignment for all projects + new_refs += [create_inherited_assignment(ref, proj_id) + for proj_id in project_ids] + + return new_refs + + if ref.get('inherited_to_projects') == 'projects': + return expand_inherited_assignment(ref, user_id, project_id) + elif 'group_id' in ref: + return expand_group_assignment(ref, user_id) + return [ref] + + def _list_effective_role_assignments(self, role_id, user_id, group_id, + domain_id, project_id, inherited): + """List role assignments in effective mode. + + When using effective mode, besides the direct assignments, the indirect + ones that come from grouping or inheritance are retrieved and will then + be expanded. + + The resulting list of assignments will be filtered by the provided + parameters, although since we are in effective mode, group can never + act as a filter (since group assignments are expanded into user roles) + and domain can only be filter if we want non-inherited assignments, + since domains can't inherit assignments. + + The goal of this method is to only ask the driver for those + assignments as could effect the result based on the parameter filters + specified, hence avoiding retrieving a huge list. + + """ + + def list_role_assignments_for_actor( + role_id, inherited, user_id=None, + group_ids=None, project_id=None, domain_id=None): + """List role assignments for actor on target. + + List direct and indirect assignments for an actor, optionally + for a given target (i.e. project or domain). + + :param role_id: List for a specific role, can be None meaning all + roles + :param inherited: Indicates whether inherited assignments or only + direct assignments are required. If None, then + both are required. + :param user_id: If not None, list only assignments that affect this + user. + :param group_ids: A list of groups required. Only one of user_id + and group_ids can be specified + :param project_id: If specified, only include those assignments + that affect this project + :param domain_id: If specified, only include those assignments + that affect this domain - by definition this will + not include any inherited assignments + + :returns: List of assignments matching the criteria. Any inherited + or group assignments that could affect the resulting + response are included. + + """ + + # List direct project role assignments + project_ids = [project_id] if project_id else None + + non_inherited_refs = [] + if inherited is False or inherited is None: + # Get non inherited assignments + non_inherited_refs = self.driver.list_role_assignments( + role_id=role_id, domain_id=domain_id, + project_ids=project_ids, user_id=user_id, + group_ids=group_ids, inherited_to_projects=False) + + inherited_refs = [] + if inherited is True or inherited is None: + # Get inherited assignments + if project_id: + # If we are filtering by a specific project, then we can + # only get inherited assignments from its domain or from + # any of its parents. + + # List inherited assignments from the project's domain + proj_domain_id = self.resource_api.get_project( + project_id)['domain_id'] + inherited_refs += self.driver.list_role_assignments( + role_id=role_id, domain_id=proj_domain_id, + user_id=user_id, group_ids=group_ids, + inherited_to_projects=True) + + # And those assignments that could be inherited from the + # project's parents. + parent_ids = [project['id'] for project in + self.resource_api.list_project_parents( + project_id)] + if parent_ids: + inherited_refs += self.driver.list_role_assignments( + role_id=role_id, project_ids=parent_ids, + user_id=user_id, group_ids=group_ids, + inherited_to_projects=True) + else: + # List inherited assignments without filtering by target + inherited_refs = self.driver.list_role_assignments( + role_id=role_id, user_id=user_id, group_ids=group_ids, + inherited_to_projects=True) + + return non_inherited_refs + inherited_refs + + # If filtering by group or inherited domain assignment the list is + # guranteed to be empty + if group_id or (domain_id and inherited): + return [] + + # If filtering by domain, then only non-inherited assignments are + # relevant, since domains don't inherit assignments + inherited = False if domain_id else inherited + + # List user assignments + direct_refs = list_role_assignments_for_actor( + role_id=role_id, user_id=user_id, project_id=project_id, + domain_id=domain_id, inherited=inherited) + + # And those from the user's groups + group_refs = [] + if user_id: + group_ids = self._get_group_ids_for_user_id(user_id) + if group_ids: + group_refs = list_role_assignments_for_actor( + role_id=role_id, project_id=project_id, + group_ids=group_ids, domain_id=domain_id, + inherited=inherited) + + # Expand grouping and inheritance on retrieved role assignments + refs = [] + for ref in (direct_refs + group_refs): + refs += self._expand_indirect_assignment(ref=ref, user_id=user_id, + project_id=project_id) + + return refs + + def _list_direct_role_assignments(self, role_id, user_id, group_id, + domain_id, project_id, inherited): + """List role assignments without applying expansion. + + Returns a list of direct role assignments, where their attributes match + the provided filters. + + """ + group_ids = [group_id] if group_id else None + project_ids = [project_id] if project_id else None + + return self.driver.list_role_assignments( + role_id=role_id, user_id=user_id, group_ids=group_ids, + domain_id=domain_id, project_ids=project_ids, + inherited_to_projects=inherited) + + def list_role_assignments(self, role_id=None, user_id=None, group_id=None, + domain_id=None, project_id=None, inherited=None, + effective=None): + """List role assignments, honoring effective mode and provided filters. + + Returns a list of role assignments, where their attributes match the + provided filters (role_id, user_id, group_id, domain_id, project_id and + inherited). The inherited filter defaults to None, meaning to get both + non-inherited and inherited role assignments. + + If effective mode is specified, this means that rather than simply + return the assignments that match the filters, any group or + inheritance assignments will be expanded. Group assignments will + become assignments for all the users in that group, and inherited + assignments will be shown on the projects below the assignment point. + Think of effective mode as being the list of assignments that actually + affect a user, for example the roles that would be placed in a token. + + If OS-INHERIT extension is disabled or the used driver does not support + inherited roles retrieval, inherited role assignments will be ignored. + + """ + + if not CONF.os_inherit.enabled: + if inherited: + return [] + inherited = False + + if effective: + return self._list_effective_role_assignments( + role_id, user_id, group_id, domain_id, project_id, inherited) + else: + return self._list_direct_role_assignments( + role_id, user_id, group_id, domain_id, project_id, inherited) def delete_tokens_for_role_assignments(self, role_id): assignments = self.list_role_assignments_for_role(role_id=role_id) @@ -532,98 +909,6 @@ class Manager(manager.Manager): # from persistence if persistence is enabled. pass - @deprecated_to_role_api - def create_role(self, role_id, role): - return self.role_api.create_role(role_id, role) - - @deprecated_to_role_api - def get_role(self, role_id): - return self.role_api.get_role(role_id) - - @deprecated_to_role_api - def update_role(self, role_id, role): - return self.role_api.update_role(role_id, role) - - @deprecated_to_role_api - def delete_role(self, role_id): - return self.role_api.delete_role(role_id) - - @deprecated_to_role_api - def list_roles(self, hints=None): - return self.role_api.list_roles(hints=hints) - - @deprecated_to_resource_api - def create_project(self, project_id, project): - return self.resource_api.create_project(project_id, project) - - @deprecated_to_resource_api - def get_project_by_name(self, tenant_name, domain_id): - return self.resource_api.get_project_by_name(tenant_name, domain_id) - - @deprecated_to_resource_api - def get_project(self, project_id): - return self.resource_api.get_project(project_id) - - @deprecated_to_resource_api - def update_project(self, project_id, project): - return self.resource_api.update_project(project_id, project) - - @deprecated_to_resource_api - def delete_project(self, project_id): - return self.resource_api.delete_project(project_id) - - @deprecated_to_resource_api - def list_projects(self, hints=None): - return self.resource_api.list_projects(hints=hints) - - @deprecated_to_resource_api - def list_projects_in_domain(self, domain_id): - return self.resource_api.list_projects_in_domain(domain_id) - - @deprecated_to_resource_api - def create_domain(self, domain_id, domain): - return self.resource_api.create_domain(domain_id, domain) - - @deprecated_to_resource_api - def get_domain_by_name(self, domain_name): - return self.resource_api.get_domain_by_name(domain_name) - - @deprecated_to_resource_api - def get_domain(self, domain_id): - return self.resource_api.get_domain(domain_id) - - @deprecated_to_resource_api - def update_domain(self, domain_id, domain): - return self.resource_api.update_domain(domain_id, domain) - - @deprecated_to_resource_api - def delete_domain(self, domain_id): - return self.resource_api.delete_domain(domain_id) - - @deprecated_to_resource_api - def list_domains(self, hints=None): - return self.resource_api.list_domains(hints=hints) - - @deprecated_to_resource_api - def assert_domain_enabled(self, domain_id, domain=None): - return self.resource_api.assert_domain_enabled(domain_id, domain) - - @deprecated_to_resource_api - def assert_project_enabled(self, project_id, project=None): - return self.resource_api.assert_project_enabled(project_id, project) - - @deprecated_to_resource_api - def is_leaf_project(self, project_id): - return self.resource_api.is_leaf_project(project_id) - - @deprecated_to_resource_api - def list_project_parents(self, project_id, user_id=None): - return self.resource_api.list_project_parents(project_id, user_id) - - @deprecated_to_resource_api - def list_projects_in_subtree(self, project_id, user_id=None): - return self.resource_api.list_projects_in_subtree(project_id, user_id) - @six.add_metaclass(abc.ABCMeta) class Driver(object): @@ -642,26 +927,6 @@ class Driver(object): role_list.append(d['id']) return role_list - def _add_role_to_role_dicts(self, role_id, inherited, dict_list, - allow_existing=True): - # There is a difference in error semantics when trying to - # assign a role that already exists between the coded v2 and v3 - # API calls. v2 will error if the assignment already exists, - # while v3 is silent. Setting the 'allow_existing' parameter - # appropriately lets this call be used for both. - role_set = set([frozenset(r.items()) for r in dict_list]) - key = frozenset(self._role_to_dict(role_id, inherited).items()) - if not allow_existing and key in role_set: - raise KeyError - role_set.add(key) - return [dict(r) for r in role_set] - - def _remove_role_from_role_dicts(self, role_id, inherited, dict_list): - role_set = set([frozenset(r.items()) for r in dict_list]) - role_set.remove(frozenset(self._role_to_dict(role_id, - inherited).items())) - return [dict(r) for r in role_set] - def _get_list_limit(self): return CONF.assignment.list_limit or CONF.list_limit @@ -740,8 +1005,16 @@ class Driver(object): raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod - def list_role_assignments(self): + def list_role_assignments(self, role_id=None, + user_id=None, group_ids=None, + domain_id=None, project_ids=None, + inherited_to_projects=None): + """Returns a list of role assignments for actors on targets. + + Available parameters represent values in which the returned role + assignments attributes need to be filtered on. + """ raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod @@ -866,12 +1139,8 @@ class Driver(object): raise exception.NotImplemented() # pragma: no cover - # TODO(henry-nash): Rename the following two methods to match the more - # meaningfully named ones above. - -# TODO(ayoung): determine what else these two functions raise @abc.abstractmethod - def delete_user(self, user_id): + def delete_user_assignments(self, user_id): """Deletes all assignments for a user. :raises: keystone.exception.RoleNotFound @@ -880,7 +1149,7 @@ class Driver(object): raise exception.NotImplemented() # pragma: no cover @abc.abstractmethod - def delete_group(self, group_id): + def delete_group_assignments(self, group_id): """Deletes all assignments for a group. :raises: keystone.exception.RoleNotFound @@ -894,6 +1163,8 @@ class Driver(object): class RoleManager(manager.Manager): """Default pivot point for the Role backend.""" + driver_namespace = 'keystone.role' + _ROLE = 'role' def __init__(self): @@ -902,9 +1173,8 @@ class RoleManager(manager.Manager): role_driver = CONF.role.driver if role_driver is None: - assignment_driver = ( - dependency.get_provider('assignment_api').driver) - role_driver = assignment_driver.default_role_driver() + assignment_manager = dependency.get_provider('assignment_api') + role_driver = assignment_manager.default_role_driver() super(RoleManager, self).__init__(role_driver) diff --git a/keystone-moon/keystone/auth/controllers.py b/keystone-moon/keystone/auth/controllers.py index 065f1f01..04124696 100644 --- a/keystone-moon/keystone/auth/controllers.py +++ b/keystone-moon/keystone/auth/controllers.py @@ -17,16 +17,18 @@ import sys from keystoneclient.common import cms from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_serialization import jsonutils from oslo_utils import importutils -from oslo_utils import timeutils import six +import stevedore from keystone.common import controller from keystone.common import dependency +from keystone.common import utils from keystone.common import wsgi from keystone import config -from keystone.contrib import federation +from keystone.contrib.federation import constants as federation_constants from keystone import exception from keystone.i18n import _, _LI, _LW from keystone.resource import controllers as resource_controllers @@ -41,6 +43,27 @@ AUTH_METHODS = {} AUTH_PLUGINS_LOADED = False +def load_auth_method(method): + plugin_name = CONF.auth.get(method) or 'default' + try: + namespace = 'keystone.auth.%s' % method + driver_manager = stevedore.DriverManager(namespace, plugin_name, + invoke_on_load=True) + return driver_manager.driver + except RuntimeError: + LOG.debug('Failed to load the %s driver (%s) using stevedore, will ' + 'attempt to load using import_object instead.', + method, plugin_name) + + @versionutils.deprecated(as_of=versionutils.deprecated.LIBERTY, + in_favor_of='entrypoints', + what='direct import of driver') + def _load_using_import(plugin_name): + return importutils.import_object(plugin_name) + + return _load_using_import(plugin_name) + + def load_auth_methods(): global AUTH_PLUGINS_LOADED @@ -50,28 +73,8 @@ def load_auth_methods(): # config.setup_authentication should be idempotent, call it to ensure we # have setup all the appropriate configuration options we may need. config.setup_authentication() - for plugin in CONF.auth.methods: - if '.' in plugin: - # NOTE(morganfainberg): if '.' is in the plugin name, it should be - # imported rather than used as a plugin identifier. - plugin_class = plugin - driver = importutils.import_object(plugin) - if not hasattr(driver, 'method'): - raise ValueError(_('Cannot load an auth-plugin by class-name ' - 'without a "method" attribute defined: %s'), - plugin_class) - - LOG.info(_LI('Loading auth-plugins by class-name is deprecated.')) - plugin_name = driver.method - else: - plugin_name = plugin - plugin_class = CONF.auth.get(plugin) - driver = importutils.import_object(plugin_class) - if plugin_name in AUTH_METHODS: - raise ValueError(_('Auth plugin %(plugin)s is requesting ' - 'previously registered method %(method)s') % - {'plugin': plugin_class, 'method': driver.method}) - AUTH_METHODS[plugin_name] = driver + for plugin in set(CONF.auth.methods): + AUTH_METHODS[plugin] = load_auth_method(plugin) AUTH_PLUGINS_LOADED = True @@ -121,11 +124,7 @@ class AuthContext(dict): return super(AuthContext, self).__setitem__(key, val) -# TODO(blk-u): this class doesn't use identity_api directly, but makes it -# available for consumers. Consumers should probably not be getting -# identity_api from this since it's available in global registry, then -# identity_api should be removed from this list. -@dependency.requires('identity_api', 'resource_api', 'trust_api') +@dependency.requires('resource_api', 'trust_api') class AuthInfo(object): """Encapsulation of "auth" request.""" @@ -217,8 +216,6 @@ class AuthInfo(object): raise exception.ValidationError(attribute='trust_id', target='trust') trust = self.trust_api.get_trust(trust_id) - if not trust: - raise exception.TrustNotFound(trust_id=trust_id) return trust def _validate_and_normalize_scope_data(self): @@ -415,7 +412,7 @@ class Auth(controller.V3Controller): return # Skip scoping when unscoped federated token is being issued - if federation.IDENTITY_PROVIDER in auth_context: + if federation_constants.IDENTITY_PROVIDER in auth_context: return # Do not scope if request is for explicitly unscoped token @@ -546,7 +543,7 @@ class Auth(controller.V3Controller): for t in tokens: expires = t['expires'] if not (expires and isinstance(expires, six.text_type)): - t['expires'] = timeutils.isotime(expires) + t['expires'] = utils.isotime(expires) data = {'revoked': tokens} json_data = jsonutils.dumps(data) signed_text = cms.cms_sign_text(json_data, diff --git a/keystone-moon/keystone/auth/plugins/core.py b/keystone-moon/keystone/auth/plugins/core.py index 96a5ecf8..bcad27e5 100644 --- a/keystone-moon/keystone/auth/plugins/core.py +++ b/keystone-moon/keystone/auth/plugins/core.py @@ -21,6 +21,7 @@ import six from keystone.common import dependency from keystone import exception + CONF = cfg.CONF LOG = log.getLogger(__name__) @@ -51,7 +52,7 @@ def convert_method_list_to_integer(methods): method_ints = [] for method in methods: - for k, v in six.iteritems(method_map): + for k, v in method_map.items(): if v == method: method_ints.append(k) return sum(method_ints) @@ -71,7 +72,7 @@ def convert_integer_to_method_list(method_int): method_map = construct_method_map_from_config() method_ints = [] - for k, v in six.iteritems(method_map): + for k, v in method_map.items(): method_ints.append(k) method_ints.sort(reverse=True) diff --git a/keystone-moon/keystone/auth/plugins/external.py b/keystone-moon/keystone/auth/plugins/external.py index 2322649f..cabe6282 100644 --- a/keystone-moon/keystone/auth/plugins/external.py +++ b/keystone-moon/keystone/auth/plugins/external.py @@ -23,7 +23,6 @@ from keystone import auth from keystone.common import dependency from keystone import exception from keystone.i18n import _ -from keystone.openstack.common import versionutils CONF = cfg.CONF @@ -31,9 +30,6 @@ CONF = cfg.CONF @six.add_metaclass(abc.ABCMeta) class Base(auth.AuthMethodHandler): - - method = 'external' - def authenticate(self, context, auth_info, auth_context): """Use REMOTE_USER to look up the user in the identity backend. @@ -96,91 +92,10 @@ class Domain(Base): return user_ref -@dependency.requires('assignment_api', 'identity_api') class KerberosDomain(Domain): """Allows `kerberos` as a method.""" - method = 'kerberos' - def _authenticate(self, remote_user, context): auth_type = context['environment'].get('AUTH_TYPE') if auth_type != 'Negotiate': raise exception.Unauthorized(_("auth_type is not Negotiate")) return super(KerberosDomain, self)._authenticate(remote_user, context) - - -class ExternalDefault(DefaultDomain): - """Deprecated. Please use keystone.auth.external.DefaultDomain instead.""" - - @versionutils.deprecated( - as_of=versionutils.deprecated.ICEHOUSE, - in_favor_of='keystone.auth.external.DefaultDomain', - remove_in=+1) - def __init__(self): - super(ExternalDefault, self).__init__() - - -class ExternalDomain(Domain): - """Deprecated. Please use keystone.auth.external.Domain instead.""" - - @versionutils.deprecated( - as_of=versionutils.deprecated.ICEHOUSE, - in_favor_of='keystone.auth.external.Domain', - remove_in=+1) - def __init__(self): - super(ExternalDomain, self).__init__() - - -@dependency.requires('identity_api') -class LegacyDefaultDomain(Base): - """Deprecated. Please use keystone.auth.external.DefaultDomain instead. - - This plugin exists to provide compatibility for the unintended behavior - described here: https://bugs.launchpad.net/keystone/+bug/1253484 - - """ - - @versionutils.deprecated( - as_of=versionutils.deprecated.ICEHOUSE, - in_favor_of='keystone.auth.external.DefaultDomain', - remove_in=+1) - def __init__(self): - super(LegacyDefaultDomain, self).__init__() - - def _authenticate(self, remote_user, context): - """Use remote_user to look up the user in the identity backend.""" - # NOTE(dolph): this unintentionally discards half the REMOTE_USER value - names = remote_user.split('@') - username = names.pop(0) - domain_id = CONF.identity.default_domain_id - user_ref = self.identity_api.get_user_by_name(username, domain_id) - return user_ref - - -@dependency.requires('identity_api', 'resource_api') -class LegacyDomain(Base): - """Deprecated. Please use keystone.auth.external.Domain instead.""" - - @versionutils.deprecated( - as_of=versionutils.deprecated.ICEHOUSE, - in_favor_of='keystone.auth.external.Domain', - remove_in=+1) - def __init__(self): - super(LegacyDomain, self).__init__() - - def _authenticate(self, remote_user, context): - """Use remote_user to look up the user in the identity backend. - - If remote_user contains an `@` assume that the substring before the - rightmost `@` is the username, and the substring after the @ is the - domain name. - """ - names = remote_user.rsplit('@', 1) - username = names.pop(0) - if names: - domain_name = names[0] - domain_ref = self.resource_api.get_domain_by_name(domain_name) - domain_id = domain_ref['id'] - else: - domain_id = CONF.identity.default_domain_id - user_ref = self.identity_api.get_user_by_name(username, domain_id) - return user_ref diff --git a/keystone-moon/keystone/auth/plugins/mapped.py b/keystone-moon/keystone/auth/plugins/mapped.py index abf44481..220ff013 100644 --- a/keystone-moon/keystone/auth/plugins/mapped.py +++ b/keystone-moon/keystone/auth/plugins/mapped.py @@ -13,14 +13,13 @@ import functools from oslo_log import log -from oslo_serialization import jsonutils from pycadf import cadftaxonomy as taxonomy from six.moves.urllib import parse from keystone import auth from keystone.auth import plugins as auth_plugins from keystone.common import dependency -from keystone.contrib import federation +from keystone.contrib.federation import constants as federation_constants from keystone.contrib.federation import utils from keystone import exception from keystone.i18n import _ @@ -33,8 +32,8 @@ LOG = log.getLogger(__name__) METHOD_NAME = 'mapped' -@dependency.requires('assignment_api', 'federation_api', 'identity_api', - 'token_provider_api') +@dependency.requires('federation_api', 'identity_api', + 'resource_api', 'token_provider_api') class Mapped(auth.AuthMethodHandler): def _get_token_ref(self, auth_payload): @@ -44,7 +43,7 @@ class Mapped(auth.AuthMethodHandler): token_data=response) def authenticate(self, context, auth_payload, auth_context): - """Authenticate mapped user and return an authentication context. + """Authenticate mapped user and set an authentication context. :param context: keystone's request context :param auth_payload: the content of the authentication for a @@ -66,7 +65,7 @@ class Mapped(auth.AuthMethodHandler): self.token_provider_api) else: handle_unscoped_token(context, auth_payload, auth_context, - self.assignment_api, self.federation_api, + self.resource_api, self.federation_api, self.identity_api) @@ -101,12 +100,12 @@ def handle_scoped_token(context, auth_payload, auth_context, token_ref, auth_context['user_id'] = user_id auth_context['group_ids'] = group_ids - auth_context[federation.IDENTITY_PROVIDER] = identity_provider - auth_context[federation.PROTOCOL] = protocol + auth_context[federation_constants.IDENTITY_PROVIDER] = identity_provider + auth_context[federation_constants.PROTOCOL] = protocol def handle_unscoped_token(context, auth_payload, auth_context, - assignment_api, federation_api, identity_api): + resource_api, federation_api, identity_api): def is_ephemeral_user(mapped_properties): return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL @@ -115,8 +114,9 @@ def handle_unscoped_token(context, auth_payload, auth_context, identity_provider, protocol): auth_context['user_id'] = user['id'] auth_context['group_ids'] = mapped_properties['group_ids'] - auth_context[federation.IDENTITY_PROVIDER] = identity_provider - auth_context[federation.PROTOCOL] = protocol + auth_context[federation_constants.IDENTITY_PROVIDER] = ( + identity_provider) + auth_context[federation_constants.PROTOCOL] = protocol def build_local_user_context(auth_context, mapped_properties): user_info = auth_plugins.UserAuthInfo.create(mapped_properties, @@ -139,17 +139,15 @@ def handle_unscoped_token(context, auth_payload, auth_context, user_id = None try: - mapped_properties = apply_mapping_filter( - identity_provider, protocol, assertion, assignment_api, + mapped_properties, mapping_id = apply_mapping_filter( + identity_provider, protocol, assertion, resource_api, federation_api, identity_api) if is_ephemeral_user(mapped_properties): user = setup_username(context, mapped_properties) user_id = user['id'] group_ids = mapped_properties['group_ids'] - mapping = federation_api.get_mapping_from_idp_and_protocol( - identity_provider, protocol) - utils.validate_groups_cardinality(group_ids, mapping['id']) + utils.validate_groups_cardinality(group_ids, mapping_id) build_ephemeral_user_context(auth_context, user, mapped_properties, identity_provider, protocol) @@ -182,32 +180,29 @@ def extract_assertion_data(context): def apply_mapping_filter(identity_provider, protocol, assertion, - assignment_api, federation_api, identity_api): + resource_api, federation_api, identity_api): idp = federation_api.get_idp(identity_provider) - utils.validate_idp(idp, assertion) - mapping = federation_api.get_mapping_from_idp_and_protocol( - identity_provider, protocol) - rules = jsonutils.loads(mapping['rules']) - LOG.debug('using the following rules: %s', rules) - rule_processor = utils.RuleProcessor(rules) - mapped_properties = rule_processor.process(assertion) + utils.validate_idp(idp, protocol, assertion) + + mapped_properties, mapping_id = federation_api.evaluate( + identity_provider, protocol, assertion) # NOTE(marek-denis): We update group_ids only here to avoid fetching # groups identified by name/domain twice. # NOTE(marek-denis): Groups are translated from name/domain to their # corresponding ids in the auth plugin, as we need information what - # ``mapping_id`` was used as well as idenity_api and assignment_api + # ``mapping_id`` was used as well as idenity_api and resource_api # objects. group_ids = mapped_properties['group_ids'] utils.validate_groups_in_backend(group_ids, - mapping['id'], + mapping_id, identity_api) group_ids.extend( utils.transform_to_group_ids( - mapped_properties['group_names'], mapping['id'], - identity_api, assignment_api)) + mapped_properties['group_names'], mapping_id, + identity_api, resource_api)) mapped_properties['group_ids'] = list(set(group_ids)) - return mapped_properties + return mapped_properties, mapping_id def setup_username(context, mapped_properties): @@ -241,12 +236,17 @@ def setup_username(context, mapped_properties): user_name = user.get('name') or context['environment'].get('REMOTE_USER') if not any([user_id, user_name]): - raise exception.Unauthorized(_("Could not map user")) + msg = _("Could not map user while setting ephemeral user identity. " + "Either mapping rules must specify user id/name or " + "REMOTE_USER environment variable must be set.") + raise exception.Unauthorized(msg) elif not user_name: user['name'] = user_id elif not user_id: - user['id'] = parse.quote(user_name) + user_id = user_name + + user['id'] = parse.quote(user_id) return user diff --git a/keystone-moon/keystone/auth/plugins/oauth1.py b/keystone-moon/keystone/auth/plugins/oauth1.py index 2f1cc2fa..e081cd62 100644 --- a/keystone-moon/keystone/auth/plugins/oauth1.py +++ b/keystone-moon/keystone/auth/plugins/oauth1.py @@ -29,15 +29,9 @@ LOG = log.getLogger(__name__) @dependency.requires('oauth_api') class OAuth(auth.AuthMethodHandler): - - method = 'oauth1' - def authenticate(self, context, auth_info, auth_context): """Turn a signed request with an access key into a keystone token.""" - if not self.oauth_api: - raise exception.Unauthorized(_('%s not supported') % self.method) - headers = context['headers'] oauth_headers = oauth.get_oauth_headers(headers) access_token_id = oauth_headers.get('oauth_token') diff --git a/keystone-moon/keystone/auth/plugins/password.py b/keystone-moon/keystone/auth/plugins/password.py index c5770445..16492a32 100644 --- a/keystone-moon/keystone/auth/plugins/password.py +++ b/keystone-moon/keystone/auth/plugins/password.py @@ -20,6 +20,7 @@ from keystone.common import dependency from keystone import exception from keystone.i18n import _ + METHOD_NAME = 'password' LOG = log.getLogger(__name__) @@ -28,11 +29,9 @@ LOG = log.getLogger(__name__) @dependency.requires('identity_api') class Password(auth.AuthMethodHandler): - method = METHOD_NAME - def authenticate(self, context, auth_payload, auth_context): """Try to authenticate against the identity backend.""" - user_info = auth_plugins.UserAuthInfo.create(auth_payload, self.method) + user_info = auth_plugins.UserAuthInfo.create(auth_payload, METHOD_NAME) # FIXME(gyee): identity.authenticate() can use some refactoring since # all we care is password matches diff --git a/keystone-moon/keystone/auth/plugins/saml2.py b/keystone-moon/keystone/auth/plugins/saml2.py index 744f26a9..cf7a8a50 100644 --- a/keystone-moon/keystone/auth/plugins/saml2.py +++ b/keystone-moon/keystone/auth/plugins/saml2.py @@ -23,5 +23,4 @@ This plugin subclasses mapped.Mapped, and may be specified in keystone.conf: class Saml2(mapped.Mapped): - - method = 'saml2' + pass diff --git a/keystone-moon/keystone/auth/plugins/token.py b/keystone-moon/keystone/auth/plugins/token.py index 5ca0b257..069f1140 100644 --- a/keystone-moon/keystone/auth/plugins/token.py +++ b/keystone-moon/keystone/auth/plugins/token.py @@ -33,8 +33,6 @@ CONF = cfg.CONF @dependency.requires('federation_api', 'identity_api', 'token_provider_api') class Token(auth.AuthMethodHandler): - method = 'token' - def _get_token_ref(self, auth_payload): token_id = auth_payload['id'] response = self.token_provider_api.validate_token(token_id) @@ -44,7 +42,7 @@ class Token(auth.AuthMethodHandler): def authenticate(self, context, auth_payload, user_context): if 'id' not in auth_payload: raise exception.ValidationError(attribute='id', - target=self.method) + target='token') token_ref = self._get_token_ref(auth_payload) if token_ref.is_federated_user and self.federation_api: mapped.handle_scoped_token( diff --git a/keystone-moon/keystone/catalog/backends/sql.py b/keystone-moon/keystone/catalog/backends/sql.py index 8ab82305..0db6d498 100644 --- a/keystone-moon/keystone/catalog/backends/sql.py +++ b/keystone-moon/keystone/catalog/backends/sql.py @@ -16,7 +16,6 @@ import itertools from oslo_config import cfg -import six import sqlalchemy from sqlalchemy.sql import true @@ -269,10 +268,28 @@ class Catalog(catalog.Driver): return ref.to_dict() def get_catalog(self, user_id, tenant_id): + """Retrieve and format the V2 service catalog. + + :param user_id: The id of the user who has been authenticated for + creating service catalog. + :param tenant_id: The id of the project. 'tenant_id' will be None + in the case this being called to create a catalog to go in a + domain scoped token. In this case, any endpoint that requires + a tenant_id as part of their URL will be skipped (as would a whole + service if, as a consequence, it has no valid endpoints). + + :returns: A nested dict representing the service catalog or an + empty dict. + + """ substitutions = dict( - itertools.chain(six.iteritems(CONF), - six.iteritems(CONF.eventlet_server))) - substitutions.update({'tenant_id': tenant_id, 'user_id': user_id}) + itertools.chain(CONF.items(), CONF.eventlet_server.items())) + substitutions.update({'user_id': user_id}) + silent_keyerror_failures = [] + if tenant_id: + substitutions.update({'tenant_id': tenant_id}) + else: + silent_keyerror_failures = ['tenant_id'] session = sql.get_session() endpoints = (session.query(Endpoint). @@ -285,7 +302,13 @@ class Catalog(catalog.Driver): if not endpoint.service['enabled']: continue try: - url = core.format_url(endpoint['url'], substitutions) + formatted_url = core.format_url( + endpoint['url'], substitutions, + silent_keyerror_failures=silent_keyerror_failures) + if formatted_url is not None: + url = formatted_url + else: + continue except exception.MalformedEndpoint: continue # this failure is already logged in format_url() @@ -304,11 +327,26 @@ class Catalog(catalog.Driver): return catalog def get_v3_catalog(self, user_id, tenant_id): + """Retrieve and format the current V3 service catalog. + + :param user_id: The id of the user who has been authenticated for + creating service catalog. + :param tenant_id: The id of the project. 'tenant_id' will be None in + the case this being called to create a catalog to go in a domain + scoped token. In this case, any endpoint that requires a + tenant_id as part of their URL will be skipped. + + :returns: A list representing the service catalog or an empty list + + """ d = dict( - itertools.chain(six.iteritems(CONF), - six.iteritems(CONF.eventlet_server))) - d.update({'tenant_id': tenant_id, - 'user_id': user_id}) + itertools.chain(CONF.items(), CONF.eventlet_server.items())) + d.update({'user_id': user_id}) + silent_keyerror_failures = [] + if tenant_id: + d.update({'tenant_id': tenant_id}) + else: + silent_keyerror_failures = ['tenant_id'] session = sql.get_session() services = (session.query(Service).filter(Service.enabled == true()). @@ -322,12 +360,20 @@ class Catalog(catalog.Driver): del endpoint['enabled'] endpoint['region'] = endpoint['region_id'] try: - endpoint['url'] = core.format_url(endpoint['url'], d) + formatted_url = core.format_url( + endpoint['url'], d, + silent_keyerror_failures=silent_keyerror_failures) + if formatted_url: + endpoint['url'] = formatted_url + else: + continue except exception.MalformedEndpoint: continue # this failure is already logged in format_url() yield endpoint + # TODO(davechen): If there is service with no endpoints, we should skip + # the service instead of keeping it in the catalog, see bug #1436704. def make_v3_service(svc): eps = list(make_v3_endpoints(svc.endpoints)) service = {'endpoints': eps, 'id': svc.id, 'type': svc.type} diff --git a/keystone-moon/keystone/catalog/backends/templated.py b/keystone-moon/keystone/catalog/backends/templated.py index d3ee105d..31d8b9e0 100644 --- a/keystone-moon/keystone/catalog/backends/templated.py +++ b/keystone-moon/keystone/catalog/backends/templated.py @@ -17,7 +17,6 @@ import os.path from oslo_config import cfg from oslo_log import log -import six from keystone.catalog.backends import kvs from keystone.catalog import core @@ -107,19 +106,43 @@ class Catalog(kvs.Catalog): raise def get_catalog(self, user_id, tenant_id): + """Retrieve and format the V2 service catalog. + + :param user_id: The id of the user who has been authenticated for + creating service catalog. + :param tenant_id: The id of the project. 'tenant_id' will be None in + the case this being called to create a catalog to go in a domain + scoped token. In this case, any endpoint that requires a tenant_id + as part of their URL will be skipped. + + :returns: A nested dict representing the service catalog or an + empty dict. + + """ substitutions = dict( - itertools.chain(six.iteritems(CONF), - six.iteritems(CONF.eventlet_server))) - substitutions.update({'tenant_id': tenant_id, 'user_id': user_id}) + itertools.chain(CONF.items(), CONF.eventlet_server.items())) + substitutions.update({'user_id': user_id}) + silent_keyerror_failures = [] + if tenant_id: + substitutions.update({'tenant_id': tenant_id}) + else: + silent_keyerror_failures = ['tenant_id'] catalog = {} - for region, region_ref in six.iteritems(self.templates): + # TODO(davechen): If there is service with no endpoints, we should + # skip the service instead of keeping it in the catalog. + # see bug #1436704. + for region, region_ref in self.templates.items(): catalog[region] = {} - for service, service_ref in six.iteritems(region_ref): + for service, service_ref in region_ref.items(): service_data = {} try: - for k, v in six.iteritems(service_ref): - service_data[k] = core.format_url(v, substitutions) + for k, v in service_ref.items(): + formatted_value = core.format_url( + v, substitutions, + silent_keyerror_failures=silent_keyerror_failures) + if formatted_value: + service_data[k] = formatted_value except exception.MalformedEndpoint: continue # this failure is already logged in format_url() catalog[region][service] = service_data diff --git a/keystone-moon/keystone/catalog/controllers.py b/keystone-moon/keystone/catalog/controllers.py index 3518c4bf..92046e8a 100644 --- a/keystone-moon/keystone/catalog/controllers.py +++ b/keystone-moon/keystone/catalog/controllers.py @@ -15,8 +15,7 @@ import uuid -import six - +from keystone.catalog import core from keystone.catalog import schema from keystone.common import controller from keystone.common import dependency @@ -88,7 +87,7 @@ class Endpoint(controller.V2Controller): # add the legacy endpoint with an interface url legacy_ep['%surl' % endpoint['interface']] = endpoint['url'] - return {'endpoints': legacy_endpoints.values()} + return {'endpoints': list(legacy_endpoints.values())} @controller.v2_deprecated def create_endpoint(self, context, endpoint): @@ -100,6 +99,14 @@ class Endpoint(controller.V2Controller): # service_id is necessary self._require_attribute(endpoint, 'service_id') + # we should check publicurl, adminurl, internalurl + # if invalid, we should raise an exception to reject + # the request + for interface in INTERFACES: + interface_url = endpoint.get(interface + 'url') + if interface_url: + core.check_endpoint_url(interface_url) + initiator = notifications._get_request_audit_info(context) if endpoint.get('region') is not None: @@ -124,7 +131,7 @@ class Endpoint(controller.V2Controller): legacy_endpoint_ref.pop(url) legacy_endpoint_id = uuid.uuid4().hex - for interface, url in six.iteritems(urls): + for interface, url in urls.items(): endpoint_ref = endpoint.copy() endpoint_ref['id'] = uuid.uuid4().hex endpoint_ref['legacy_endpoint_id'] = legacy_endpoint_id @@ -301,13 +308,14 @@ class EndpointV3(controller.V3Controller): @controller.protected() @validation.validated(schema.endpoint_create, 'endpoint') def create_endpoint(self, context, endpoint): + core.check_endpoint_url(endpoint['url']) ref = self._assign_unique_id(self._normalize_dict(endpoint)) ref = self._validate_endpoint_region(ref, context) initiator = notifications._get_request_audit_info(context) ref = self.catalog_api.create_endpoint(ref['id'], ref, initiator) return EndpointV3.wrap_member(context, ref) - @controller.filterprotected('interface', 'service_id') + @controller.filterprotected('interface', 'service_id', 'region_id') def list_endpoints(self, context, filters): hints = EndpointV3.build_driver_hints(context, filters) refs = self.catalog_api.list_endpoints(hints=hints) diff --git a/keystone-moon/keystone/catalog/core.py b/keystone-moon/keystone/catalog/core.py index fba26b89..6883b024 100644 --- a/keystone-moon/keystone/catalog/core.py +++ b/keystone-moon/keystone/catalog/core.py @@ -16,6 +16,7 @@ """Main entry point into the Catalog service.""" import abc +import itertools from oslo_config import cfg from oslo_log import log @@ -35,25 +36,27 @@ from keystone import notifications CONF = cfg.CONF LOG = log.getLogger(__name__) MEMOIZE = cache.get_memoization_decorator(section='catalog') +WHITELISTED_PROPERTIES = [ + 'tenant_id', 'user_id', 'public_bind_host', 'admin_bind_host', + 'compute_host', 'admin_port', 'public_port', + 'public_endpoint', 'admin_endpoint', ] -def format_url(url, substitutions): +def format_url(url, substitutions, silent_keyerror_failures=None): """Formats a user-defined URL with the given substitutions. :param string url: the URL to be formatted :param dict substitutions: the dictionary used for substitution + :param list silent_keyerror_failures: keys for which we should be silent + if there is a KeyError exception on substitution attempt :returns: a formatted URL """ - WHITELISTED_PROPERTIES = [ - 'tenant_id', 'user_id', 'public_bind_host', 'admin_bind_host', - 'compute_host', 'compute_port', 'admin_port', 'public_port', - 'public_endpoint', 'admin_endpoint', ] - substitutions = utils.WhiteListedItemFilter( WHITELISTED_PROPERTIES, substitutions) + allow_keyerror = silent_keyerror_failures or [] try: result = url.replace('$(', '%(') % substitutions except AttributeError: @@ -61,10 +64,14 @@ def format_url(url, substitutions): {"url": url}) raise exception.MalformedEndpoint(endpoint=url) except KeyError as e: - LOG.error(_LE("Malformed endpoint %(url)s - unknown key %(keyerror)s"), - {"url": url, - "keyerror": e}) - raise exception.MalformedEndpoint(endpoint=url) + if not e.args or e.args[0] not in allow_keyerror: + LOG.error(_LE("Malformed endpoint %(url)s - unknown key " + "%(keyerror)s"), + {"url": url, + "keyerror": e}) + raise exception.MalformedEndpoint(endpoint=url) + else: + result = None except TypeError as e: LOG.error(_LE("Malformed endpoint '%(url)s'. The following type error " "occurred during string substitution: %(typeerror)s"), @@ -78,6 +85,28 @@ def format_url(url, substitutions): return result +def check_endpoint_url(url): + """Check substitution of url. + + The invalid urls are as follows: + urls with substitutions that is not in the whitelist + + Check the substitutions in the URL to make sure they are valid + and on the whitelist. + + :param str url: the URL to validate + :rtype: None + :raises keystone.exception.URLValidationError: if the URL is invalid + """ + # check whether the property in the path is exactly the same + # with that in the whitelist below + substitutions = dict(zip(WHITELISTED_PROPERTIES, itertools.repeat(''))) + try: + url.replace('$(', '%(') % substitutions + except (KeyError, TypeError, ValueError): + raise exception.URLValidationError(url) + + @dependency.provider('catalog_api') class Manager(manager.Manager): """Default pivot point for the Catalog backend. @@ -86,6 +115,9 @@ class Manager(manager.Manager): dynamically calls the backend. """ + + driver_namespace = 'keystone.catalog' + _ENDPOINT = 'endpoint' _SERVICE = 'service' _REGION = 'region' @@ -103,10 +135,12 @@ class Manager(manager.Manager): msg = _('Duplicate ID, %s.') % region_ref['id'] raise exception.Conflict(type='region', details=msg) - # NOTE(lbragstad): The description column of the region database - # can not be null. So if the user doesn't pass in a description then - # set it to an empty string. - region_ref.setdefault('description', '') + # NOTE(lbragstad,dstanek): The description column of the region + # database cannot be null. So if the user doesn't pass in a + # description or passes in a null description then set it to an + # empty string. + if region_ref.get('description') is None: + region_ref['description'] = '' try: ret = self.driver.create_region(region_ref) except exception.NotFound: @@ -124,6 +158,11 @@ class Manager(manager.Manager): raise exception.RegionNotFound(region_id=region_id) def update_region(self, region_id, region_ref, initiator=None): + # NOTE(lbragstad,dstanek): The description column of the region + # database cannot be null. So if the user passes in a null + # description set it to an empty string. + if 'description' in region_ref and region_ref['description'] is None: + region_ref['description'] = '' ref = self.driver.update_region(region_id, region_ref) notifications.Audit.updated(self._REGION, region_id, initiator) self.get_region.invalidate(self, region_id) @@ -475,14 +514,14 @@ class Driver(object): v2_catalog = self.get_catalog(user_id, tenant_id) v3_catalog = [] - for region_name, region in six.iteritems(v2_catalog): - for service_type, service in six.iteritems(region): + for region_name, region in v2_catalog.items(): + for service_type, service in region.items(): service_v3 = { 'type': service_type, 'endpoints': [] } - for attr, value in six.iteritems(service): + for attr, value in service.items(): # Attributes that end in URL are interfaces. In the V2 # catalog, these are internalURL, publicURL, and adminURL. # For example, <region_name>.publicURL=<URL> in the V2 diff --git a/keystone-moon/keystone/catalog/schema.py b/keystone-moon/keystone/catalog/schema.py index a779ad02..671f1233 100644 --- a/keystone-moon/keystone/catalog/schema.py +++ b/keystone-moon/keystone/catalog/schema.py @@ -14,7 +14,9 @@ from keystone.common.validation import parameter_types _region_properties = { - 'description': parameter_types.description, + 'description': { + 'type': ['string', 'null'], + }, # NOTE(lbragstad): Regions use ID differently. The user can specify the ID # or it will be generated automatically. 'id': { diff --git a/keystone-moon/keystone/cmd/__init__.py b/keystone-moon/keystone/cmd/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/cmd/__init__.py diff --git a/keystone-moon/keystone/cmd/all.py b/keystone-moon/keystone/cmd/all.py new file mode 100644 index 00000000..c583accd --- /dev/null +++ b/keystone-moon/keystone/cmd/all.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# Copyright 2013 OpenStack Foundation +# +# 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 os +import sys + + +# If ../../keystone/__init__.py exists, add ../../ to Python search path, so +# that it will override what happens to be installed in +# /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(__file__), + os.pardir, + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, + 'keystone', + '__init__.py')): + sys.path.insert(0, possible_topdir) + + +from keystone.server import eventlet as eventlet_server + + +# entry point. +def main(): + eventlet_server.run(possible_topdir) diff --git a/keystone-moon/keystone/cmd/cli.py b/keystone-moon/keystone/cmd/cli.py new file mode 100644 index 00000000..d993d71c --- /dev/null +++ b/keystone-moon/keystone/cmd/cli.py @@ -0,0 +1,685 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 __future__ import absolute_import +from __future__ import print_function + +import os + +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +import pbr.version + +from keystone.common import driver_hints +from keystone.common import openssl +from keystone.common import sql +from keystone.common.sql import migration_helpers +from keystone.common import utils +from keystone import config +from keystone import exception +from keystone.i18n import _, _LW +from keystone.server import backends +from keystone import token + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class BaseApp(object): + + name = None + + @classmethod + def add_argument_parser(cls, subparsers): + parser = subparsers.add_parser(cls.name, help=cls.__doc__) + parser.set_defaults(cmd_class=cls) + return parser + + +class DbSync(BaseApp): + """Sync the database.""" + + name = 'db_sync' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(DbSync, cls).add_argument_parser(subparsers) + parser.add_argument('version', default=None, nargs='?', + help=('Migrate the database up to a specified ' + 'version. If not provided, db_sync will ' + 'migrate the database to the latest known ' + 'version. Schema downgrades are not ' + 'supported.')) + parser.add_argument('--extension', default=None, + help=('Migrate the database for the specified ' + 'extension. If not provided, db_sync will ' + 'migrate the common repository.')) + + return parser + + @staticmethod + def main(): + version = CONF.command.version + extension = CONF.command.extension + migration_helpers.sync_database_to_version(extension, version) + + +class DbVersion(BaseApp): + """Print the current migration version of the database.""" + + name = 'db_version' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(DbVersion, cls).add_argument_parser(subparsers) + parser.add_argument('--extension', default=None, + help=('Print the migration version of the ' + 'database for the specified extension. If ' + 'not provided, print it for the common ' + 'repository.')) + + @staticmethod + def main(): + extension = CONF.command.extension + migration_helpers.print_db_version(extension) + + +class BasePermissionsSetup(BaseApp): + """Common user/group setup for file permissions.""" + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(BasePermissionsSetup, + cls).add_argument_parser(subparsers) + running_as_root = (os.geteuid() == 0) + parser.add_argument('--keystone-user', required=running_as_root) + parser.add_argument('--keystone-group', required=running_as_root) + return parser + + @staticmethod + def get_user_group(): + keystone_user_id = None + keystone_group_id = None + + try: + a = CONF.command.keystone_user + if a: + keystone_user_id = utils.get_unix_user(a)[0] + except KeyError: + raise ValueError("Unknown user '%s' in --keystone-user" % a) + + try: + a = CONF.command.keystone_group + if a: + keystone_group_id = utils.get_unix_group(a)[0] + except KeyError: + raise ValueError("Unknown group '%s' in --keystone-group" % a) + + return keystone_user_id, keystone_group_id + + +class BaseCertificateSetup(BasePermissionsSetup): + """Provides common options for certificate setup.""" + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(BaseCertificateSetup, + cls).add_argument_parser(subparsers) + parser.add_argument('--rebuild', default=False, action='store_true', + help=('Rebuild certificate files: erase previous ' + 'files and regenerate them.')) + return parser + + +class PKISetup(BaseCertificateSetup): + """Set up Key pairs and certificates for token signing and verification. + + This is NOT intended for production use, see Keystone Configuration + documentation for details. + """ + + name = 'pki_setup' + + @classmethod + def main(cls): + LOG.warn(_LW('keystone-manage pki_setup is not recommended for ' + 'production use.')) + keystone_user_id, keystone_group_id = cls.get_user_group() + conf_pki = openssl.ConfigurePKI(keystone_user_id, keystone_group_id, + rebuild=CONF.command.rebuild) + conf_pki.run() + + +class SSLSetup(BaseCertificateSetup): + """Create key pairs and certificates for HTTPS connections. + + This is NOT intended for production use, see Keystone Configuration + documentation for details. + """ + + name = 'ssl_setup' + + @classmethod + def main(cls): + LOG.warn(_LW('keystone-manage ssl_setup is not recommended for ' + 'production use.')) + keystone_user_id, keystone_group_id = cls.get_user_group() + conf_ssl = openssl.ConfigureSSL(keystone_user_id, keystone_group_id, + rebuild=CONF.command.rebuild) + conf_ssl.run() + + +class FernetSetup(BasePermissionsSetup): + """Setup a key repository for Fernet tokens. + + This also creates a primary key used for both creating and validating + Fernet tokens. To improve security, you should rotate your keys (using + keystone-manage fernet_rotate, for example). + + """ + + name = 'fernet_setup' + + @classmethod + def main(cls): + from keystone.token.providers.fernet import utils as fernet + + keystone_user_id, keystone_group_id = cls.get_user_group() + fernet.create_key_directory(keystone_user_id, keystone_group_id) + if fernet.validate_key_repository(): + fernet.initialize_key_repository( + keystone_user_id, keystone_group_id) + + +class FernetRotate(BasePermissionsSetup): + """Rotate Fernet encryption keys. + + This assumes you have already run keystone-manage fernet_setup. + + A new primary key is placed into rotation, which is used for new tokens. + The old primary key is demoted to secondary, which can then still be used + for validating tokens. Excess secondary keys (beyond [fernet_tokens] + max_active_keys) are revoked. Revoked keys are permanently deleted. A new + staged key will be created and used to validate tokens. The next time key + rotation takes place, the staged key will be put into rotation as the + primary key. + + Rotating keys too frequently, or with [fernet_tokens] max_active_keys set + too low, will cause tokens to become invalid prior to their expiration. + + """ + + name = 'fernet_rotate' + + @classmethod + def main(cls): + from keystone.token.providers.fernet import utils as fernet + + keystone_user_id, keystone_group_id = cls.get_user_group() + if fernet.validate_key_repository(): + fernet.rotate_keys(keystone_user_id, keystone_group_id) + + +class TokenFlush(BaseApp): + """Flush expired tokens from the backend.""" + + name = 'token_flush' + + @classmethod + def main(cls): + token_manager = token.persistence.PersistenceManager() + token_manager.flush_expired_tokens() + + +class MappingPurge(BaseApp): + """Purge the mapping table.""" + + name = 'mapping_purge' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(MappingPurge, cls).add_argument_parser(subparsers) + parser.add_argument('--all', default=False, action='store_true', + help=('Purge all mappings.')) + parser.add_argument('--domain-name', default=None, + help=('Purge any mappings for the domain ' + 'specified.')) + parser.add_argument('--public-id', default=None, + help=('Purge the mapping for the Public ID ' + 'specified.')) + parser.add_argument('--local-id', default=None, + help=('Purge the mappings for the Local ID ' + 'specified.')) + parser.add_argument('--type', default=None, choices=['user', 'group'], + help=('Purge any mappings for the type ' + 'specified.')) + return parser + + @staticmethod + def main(): + def validate_options(): + # NOTE(henry-nash); It would be nice to use the argparse automated + # checking for this validation, but the only way I can see doing + # that is to make the default (i.e. if no optional parameters + # are specified) to purge all mappings - and that sounds too + # dangerous as a default. So we use it in a slightly + # unconventional way, where all parameters are optional, but you + # must specify at least one. + if (CONF.command.all is False and + CONF.command.domain_name is None and + CONF.command.public_id is None and + CONF.command.local_id is None and + CONF.command.type is None): + raise ValueError(_('At least one option must be provided')) + + if (CONF.command.all is True and + (CONF.command.domain_name is not None or + CONF.command.public_id is not None or + CONF.command.local_id is not None or + CONF.command.type is not None)): + raise ValueError(_('--all option cannot be mixed with ' + 'other options')) + + def get_domain_id(name): + try: + return resource_manager.get_domain_by_name(name)['id'] + except KeyError: + raise ValueError(_("Unknown domain '%(name)s' specified by " + "--domain-name") % {'name': name}) + + validate_options() + drivers = backends.load_backends() + resource_manager = drivers['resource_api'] + mapping_manager = drivers['id_mapping_api'] + + # Now that we have validated the options, we know that at least one + # option has been specified, and if it was the --all option then this + # was the only option specified. + # + # The mapping dict is used to filter which mappings are purged, so + # leaving it empty means purge them all + mapping = {} + if CONF.command.domain_name is not None: + mapping['domain_id'] = get_domain_id(CONF.command.domain_name) + if CONF.command.public_id is not None: + mapping['public_id'] = CONF.command.public_id + if CONF.command.local_id is not None: + mapping['local_id'] = CONF.command.local_id + if CONF.command.type is not None: + mapping['type'] = CONF.command.type + + mapping_manager.purge_mappings(mapping) + + +DOMAIN_CONF_FHEAD = 'keystone.' +DOMAIN_CONF_FTAIL = '.conf' + + +class DomainConfigUploadFiles(object): + + def __init__(self): + super(DomainConfigUploadFiles, self).__init__() + self.load_backends() + + def load_backends(self): + drivers = backends.load_backends() + self.resource_manager = drivers['resource_api'] + self.domain_config_manager = drivers['domain_config_api'] + + def valid_options(self): + """Validate the options, returning True if they are indeed valid. + + It would be nice to use the argparse automated checking for this + validation, but the only way I can see doing that is to make the + default (i.e. if no optional parameters are specified) to upload + all configuration files - and that sounds too dangerous as a + default. So we use it in a slightly unconventional way, where all + parameters are optional, but you must specify at least one. + + """ + if (CONF.command.all is False and + CONF.command.domain_name is None): + print(_('At least one option must be provided, use either ' + '--all or --domain-name')) + raise ValueError + + if (CONF.command.all is True and + CONF.command.domain_name is not None): + print(_('The --all option cannot be used with ' + 'the --domain-name option')) + raise ValueError + + def upload_config_to_database(self, file_name, domain_name): + """Upload a single config file to the database. + + :param file_name: the file containing the config options + :param domain_name: the domain name + + :raises: ValueError: the domain does not exist or already has domain + specific configurations defined + :raises: Exceptions from oslo config: there is an issue with options + defined in the config file or its + format + + The caller of this method should catch the errors raised and handle + appropriately in order that the best UX experience can be provided for + both the case of when a user has asked for a specific config file to + be uploaded, as well as all config files in a directory. + + """ + try: + domain_ref = ( + self.resource_manager.get_domain_by_name(domain_name)) + except exception.DomainNotFound: + print(_('Invalid domain name: %(domain)s found in config file ' + 'name: %(file)s - ignoring this file.') % { + 'domain': domain_name, + 'file': file_name}) + raise ValueError + + if self.domain_config_manager.get_config_with_sensitive_info( + domain_ref['id']): + print(_('Domain: %(domain)s already has a configuration ' + 'defined - ignoring file: %(file)s.') % { + 'domain': domain_name, + 'file': file_name}) + raise ValueError + + sections = {} + try: + parser = cfg.ConfigParser(file_name, sections) + parser.parse() + except Exception: + # We explicitly don't try and differentiate the error cases, in + # order to keep the code in this tool more robust as oslo.config + # changes. + print(_('Error parsing configuration file for domain: %(domain)s, ' + 'file: %(file)s.') % { + 'domain': domain_name, + 'file': file_name}) + raise + + for group in sections: + for option in sections[group]: + sections[group][option] = sections[group][option][0] + self.domain_config_manager.create_config(domain_ref['id'], sections) + + def upload_configs_to_database(self, file_name, domain_name): + """Upload configs from file and load into database. + + This method will be called repeatedly for all the config files in the + config directory. To provide a better UX, we differentiate the error + handling in this case (versus when the user has asked for a single + config file to be uploaded). + + """ + try: + self.upload_config_to_database(file_name, domain_name) + except ValueError: + # We've already given all the info we can in a message, so carry + # on to the next one + pass + except Exception: + # Some other error occurred relating to this specific config file + # or domain. Since we are trying to upload all the config files, + # we'll continue and hide this exception. However, we tell the + # user how to get more info about this error by re-running with + # just the domain at fault. When we run in single-domain mode we + # will NOT hide the exception. + print(_('To get a more detailed information on this error, re-run ' + 'this command for the specific domain, i.e.: ' + 'keystone-manage domain_config_upload --domain-name %s') % + domain_name) + pass + + def read_domain_configs_from_files(self): + """Read configs from file(s) and load into database. + + The command line parameters have already been parsed and the CONF + command option will have been set. It is either set to the name of an + explicit domain, or it's None to indicate that we want all domain + config files. + + """ + domain_name = CONF.command.domain_name + conf_dir = CONF.identity.domain_config_dir + if not os.path.exists(conf_dir): + print(_('Unable to locate domain config directory: %s') % conf_dir) + raise ValueError + + if domain_name: + # Request is to upload the configs for just one domain + fname = DOMAIN_CONF_FHEAD + domain_name + DOMAIN_CONF_FTAIL + self.upload_config_to_database( + os.path.join(conf_dir, fname), domain_name) + return + + # Request is to transfer all config files, so let's read all the + # files in the config directory, and transfer those that match the + # filename pattern of 'keystone.<domain_name>.conf' + for r, d, f in os.walk(conf_dir): + for fname in f: + if (fname.startswith(DOMAIN_CONF_FHEAD) and + fname.endswith(DOMAIN_CONF_FTAIL)): + if fname.count('.') >= 2: + self.upload_configs_to_database( + os.path.join(r, fname), + fname[len(DOMAIN_CONF_FHEAD): + -len(DOMAIN_CONF_FTAIL)]) + else: + LOG.warn(_LW('Ignoring file (%s) while scanning ' + 'domain config directory'), fname) + + def run(self): + # First off, let's just check we can talk to the domain database + try: + self.resource_manager.list_domains(driver_hints.Hints()) + except Exception: + # It is likely that there is some SQL or other backend error + # related to set up + print(_('Unable to access the keystone database, please check it ' + 'is configured correctly.')) + raise + + try: + self.valid_options() + self.read_domain_configs_from_files() + except ValueError: + # We will already have printed out a nice message, so indicate + # to caller the non-success error code to be used. + return 1 + + +class DomainConfigUpload(BaseApp): + """Upload the domain specific configuration files to the database.""" + + name = 'domain_config_upload' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(DomainConfigUpload, cls).add_argument_parser(subparsers) + parser.add_argument('--all', default=False, action='store_true', + help='Upload contents of all domain specific ' + 'configuration files. Either use this option ' + 'or use the --domain-name option to choose a ' + 'specific domain.') + parser.add_argument('--domain-name', default=None, + help='Upload contents of the specific ' + 'configuration file for the given domain. ' + 'Either use this option or use the --all ' + 'option to upload contents for all domains.') + return parser + + @staticmethod + def main(): + dcu = DomainConfigUploadFiles() + status = dcu.run() + if status is not None: + exit(status) + + +class SamlIdentityProviderMetadata(BaseApp): + """Generate Identity Provider metadata.""" + + name = 'saml_idp_metadata' + + @staticmethod + def main(): + # NOTE(marek-denis): Since federation is currently an extension import + # corresponding modules only when they are really going to be used. + from keystone.contrib.federation import idp + metadata = idp.MetadataGenerator().generate_metadata() + print(metadata.to_string()) + + +class MappingEngineTester(BaseApp): + """Execute mapping engine locally.""" + + name = 'mapping_engine' + + @staticmethod + def read_rules(path): + try: + with open(path) as file: + return jsonutils.load(file) + except ValueError as e: + raise SystemExit(_('Error while parsing rules ' + '%(path)s: %(err)s') % {'path': path, 'err': e}) + + @staticmethod + def read_file(path): + try: + with open(path) as file: + return file.read().strip() + except IOError as e: + raise SystemExit(_("Error while opening file " + "%(path)s: %(err)s") % {'path': path, 'err': e}) + + @staticmethod + def normalize_assertion(assertion): + def split(line): + try: + k, v = line.split(':', 1) + return k.strip(), v.strip() + except ValueError as e: + msg = _("Error while parsing line: '%(line)s': %(err)s") + raise SystemExit(msg % {'line': line, 'err': e}) + assertion = assertion.split('\n') + assertion_dict = {} + prefix = CONF.command.prefix + for line in assertion: + k, v = split(line) + if prefix: + if k.startswith(prefix): + assertion_dict[k] = v + else: + assertion_dict[k] = v + return assertion_dict + + @staticmethod + def normalize_rules(rules): + if isinstance(rules, list): + return {'rules': rules} + else: + return rules + + @classmethod + def main(cls): + from keystone.contrib.federation import utils as mapping_engine + if not CONF.command.engine_debug: + mapping_engine.LOG.logger.setLevel('WARN') + + rules = MappingEngineTester.read_rules(CONF.command.rules) + rules = MappingEngineTester.normalize_rules(rules) + mapping_engine.validate_mapping_structure(rules) + + assertion = MappingEngineTester.read_file(CONF.command.input) + assertion = MappingEngineTester.normalize_assertion(assertion) + rp = mapping_engine.RuleProcessor(rules['rules']) + print(jsonutils.dumps(rp.process(assertion), indent=2)) + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(MappingEngineTester, + cls).add_argument_parser(subparsers) + + parser.add_argument('--rules', default=None, required=True, + help=("Path to the file with " + "rules to be executed. " + "Content must be a proper JSON structure, " + "with a top-level key 'rules' and " + "corresponding value being a list.")) + parser.add_argument('--input', default=None, required=True, + help=("Path to the file with input attributes. " + "The content consists of ':' separated " + "parameter names and their values. " + "There is only one key-value pair per line. " + "A ';' in the value is a separator and then " + "a value is treated as a list. Example:\n " + "EMAIL: me@example.com\n" + "LOGIN: me\n" + "GROUPS: group1;group2;group3")) + parser.add_argument('--prefix', default=None, + help=("A prefix used for each environment " + "variable in the assertion. For example, " + "all environment variables may have the " + "prefix ASDF_.")) + parser.add_argument('--engine-debug', + default=False, action="store_true", + help=("Enable debug messages from the mapping " + "engine.")) + + +CMDS = [ + DbSync, + DbVersion, + DomainConfigUpload, + FernetRotate, + FernetSetup, + MappingPurge, + MappingEngineTester, + PKISetup, + SamlIdentityProviderMetadata, + SSLSetup, + TokenFlush, +] + + +def add_command_parsers(subparsers): + for cmd in CMDS: + cmd.add_argument_parser(subparsers) + + +command_opt = cfg.SubCommandOpt('command', + title='Commands', + help='Available commands', + handler=add_command_parsers) + + +def main(argv=None, config_files=None): + CONF.register_cli_opt(command_opt) + + config.configure() + sql.initialize() + config.set_default_for_default_log_levels() + + CONF(args=argv[1:], + project='keystone', + version=pbr.version.VersionInfo('keystone').version_string(), + usage='%(prog)s [' + '|'.join([cmd.name for cmd in CMDS]) + ']', + default_config_files=config_files) + config.setup_logging() + CONF.command.cmd_class.main() diff --git a/keystone-moon/keystone/cmd/manage.py b/keystone-moon/keystone/cmd/manage.py new file mode 100644 index 00000000..da38278e --- /dev/null +++ b/keystone-moon/keystone/cmd/manage.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +# Copyright 2013 OpenStack Foundation +# +# 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 os +import sys + +# If ../../keystone/__init__.py exists, add ../../ to Python search path, so +# that it will override what happens to be installed in +# /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, + 'keystone', + '__init__.py')): + sys.path.insert(0, possible_topdir) + +from keystone.cmd import cli +from keystone.common import environment + + +# entry point. +def main(): + environment.use_stdlib() + + dev_conf = os.path.join(possible_topdir, + 'etc', + 'keystone.conf') + config_files = None + if os.path.exists(dev_conf): + config_files = [dev_conf] + + cli.main(argv=sys.argv, config_files=config_files) diff --git a/keystone-moon/keystone/common/authorization.py b/keystone-moon/keystone/common/authorization.py index 5cb1e630..8db618df 100644 --- a/keystone-moon/keystone/common/authorization.py +++ b/keystone-moon/keystone/common/authorization.py @@ -59,6 +59,7 @@ def token_to_auth_context(token): auth_context['project_id'] = token.project_id elif token.domain_scoped: auth_context['domain_id'] = token.domain_id + auth_context['domain_name'] = token.domain_name else: LOG.debug('RBAC: Proceeding without project or domain scope') diff --git a/keystone-moon/keystone/common/base64utils.py b/keystone-moon/keystone/common/base64utils.py index 1a636f9b..d19eade7 100644 --- a/keystone-moon/keystone/common/base64utils.py +++ b/keystone-moon/keystone/common/base64utils.py @@ -57,8 +57,13 @@ base64url_non_alphabet_re = re.compile(r'[^A-Za-z0-9---_=]+') _strip_formatting_re = re.compile(r'\s+') -_base64_to_base64url_trans = string.maketrans('+/', '-_') -_base64url_to_base64_trans = string.maketrans('-_', '+/') +if six.PY2: + str_ = string +else: + str_ = str + +_base64_to_base64url_trans = str_.maketrans('+/', '-_') +_base64url_to_base64_trans = str_.maketrans('-_', '+/') def _check_padding_length(pad): diff --git a/keystone-moon/keystone/common/cache/_memcache_pool.py b/keystone-moon/keystone/common/cache/_memcache_pool.py index b15332db..2bfcc3bb 100644 --- a/keystone-moon/keystone/common/cache/_memcache_pool.py +++ b/keystone-moon/keystone/common/cache/_memcache_pool.py @@ -27,7 +27,7 @@ import time import memcache from oslo_log import log -from six.moves import queue +from six.moves import queue, zip from keystone import exception from keystone.i18n import _ @@ -35,11 +35,22 @@ from keystone.i18n import _ LOG = log.getLogger(__name__) -# This 'class' is taken from http://stackoverflow.com/a/22520633/238308 -# Don't inherit client from threading.local so that we can reuse clients in -# different threads -_MemcacheClient = type('_MemcacheClient', (object,), - dict(memcache.Client.__dict__)) + +class _MemcacheClient(memcache.Client): + """Thread global memcache client + + As client is inherited from threading.local we have to restore object + methods overloaded by threading.local so we can reuse clients in + different threads + """ + __delattr__ = object.__delattr__ + __getattribute__ = object.__getattribute__ + __new__ = object.__new__ + __setattr__ = object.__setattr__ + + def __del__(self): + pass + _PoolItem = collections.namedtuple('_PoolItem', ['ttl', 'connection']) diff --git a/keystone-moon/keystone/common/cache/backends/mongo.py b/keystone-moon/keystone/common/cache/backends/mongo.py index b5de9bc4..cb5ad833 100644 --- a/keystone-moon/keystone/common/cache/backends/mongo.py +++ b/keystone-moon/keystone/common/cache/backends/mongo.py @@ -360,8 +360,12 @@ class MongoApi(object): self._assign_data_mainpulator() if self.read_preference: - self.read_preference = pymongo.read_preferences.mongos_enum( - self.read_preference) + # pymongo 3.0 renamed mongos_enum to read_pref_mode_from_name + f = getattr(pymongo.read_preferences, + 'read_pref_mode_from_name', None) + if not f: + f = pymongo.read_preferences.mongos_enum + self.read_preference = f(self.read_preference) coll.read_preference = self.read_preference if self.w > -1: coll.write_concern['w'] = self.w @@ -395,7 +399,7 @@ class MongoApi(object): Refer to MongoDB documentation around TTL index for further details. """ indexes = collection.index_information() - for indx_name, index_data in six.iteritems(indexes): + for indx_name, index_data in indexes.items(): if all(k in index_data for k in ('key', 'expireAfterSeconds')): existing_value = index_data['expireAfterSeconds'] fld_present = 'doc_date' in index_data['key'][0] @@ -447,7 +451,7 @@ class MongoApi(object): doc_date = self._get_doc_date() insert_refs = [] update_refs = [] - existing_docs = self._get_results_as_dict(mapping.keys()) + existing_docs = self._get_results_as_dict(list(mapping.keys())) for key, value in mapping.items(): ref = self._get_cache_entry(key, value.payload, value.metadata, doc_date) @@ -532,7 +536,7 @@ class BaseTransform(AbstractManipulator): def transform_incoming(self, son, collection): """Used while saving data to MongoDB.""" - for (key, value) in son.items(): + for (key, value) in list(son.items()): if isinstance(value, api.CachedValue): son[key] = value.payload # key is 'value' field here son['meta'] = value.metadata @@ -549,7 +553,7 @@ class BaseTransform(AbstractManipulator): ('_id', 'value', 'meta', 'doc_date')): payload = son.pop('value', None) metadata = son.pop('meta', None) - for (key, value) in son.items(): + for (key, value) in list(son.items()): if isinstance(value, dict): son[key] = self.transform_outgoing(value, collection) if metadata is not None: diff --git a/keystone-moon/keystone/common/clean.py b/keystone-moon/keystone/common/clean.py new file mode 100644 index 00000000..38564e0b --- /dev/null +++ b/keystone-moon/keystone/common/clean.py @@ -0,0 +1,87 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 six + +from keystone import exception +from keystone.i18n import _ + + +def check_length(property_name, value, min_length=1, max_length=64): + if len(value) < min_length: + if min_length == 1: + msg = _("%s cannot be empty.") % property_name + else: + msg = (_("%(property_name)s cannot be less than " + "%(min_length)s characters.") % dict( + property_name=property_name, min_length=min_length)) + raise exception.ValidationError(msg) + if len(value) > max_length: + msg = (_("%(property_name)s should not be greater than " + "%(max_length)s characters.") % dict( + property_name=property_name, max_length=max_length)) + + raise exception.ValidationError(msg) + + +def check_type(property_name, value, expected_type, display_expected_type): + if not isinstance(value, expected_type): + msg = (_("%(property_name)s is not a " + "%(display_expected_type)s") % dict( + property_name=property_name, + display_expected_type=display_expected_type)) + raise exception.ValidationError(msg) + + +def check_enabled(property_name, enabled): + # Allow int and it's subclass bool + check_type('%s enabled' % property_name, enabled, int, 'boolean') + return bool(enabled) + + +def check_name(property_name, name, min_length=1, max_length=64): + check_type('%s name' % property_name, name, six.string_types, + 'str or unicode') + name = name.strip() + check_length('%s name' % property_name, name, + min_length=min_length, max_length=max_length) + return name + + +def domain_name(name): + return check_name('Domain', name) + + +def domain_enabled(enabled): + return check_enabled('Domain', enabled) + + +def project_name(name): + return check_name('Project', name) + + +def project_enabled(enabled): + return check_enabled('Project', enabled) + + +def user_name(name): + return check_name('User', name, max_length=255) + + +def user_enabled(enabled): + return check_enabled('User', enabled) + + +def group_name(name): + return check_name('Group', name) diff --git a/keystone-moon/keystone/common/config.py b/keystone-moon/keystone/common/config.py index bcaedeef..6cc848b4 100644 --- a/keystone-moon/keystone/common/config.py +++ b/keystone-moon/keystone/common/config.py @@ -14,6 +14,7 @@ from oslo_config import cfg import oslo_messaging +import passlib.utils _DEFAULT_AUTH_METHODS = ['external', 'password', 'token', 'oauth1'] @@ -32,14 +33,6 @@ FILE_OPTIONS = { 'AdminTokenAuthMiddleware from your paste ' 'application pipelines (for example, in ' 'keystone-paste.ini).'), - cfg.IntOpt('compute_port', default=8774, - help='(Deprecated) The port which the OpenStack Compute ' - 'service listens on. This option was only used for ' - 'string replacement in the templated catalog backend. ' - 'Templated catalogs should replace the ' - '"$(compute_port)s" substitution with the static port ' - 'of the compute service. As of Juno, this option is ' - 'deprecated and will be removed in the L release.'), cfg.StrOpt('public_endpoint', help='The base public endpoint URL for Keystone that is ' 'advertised to clients (NOTE: this does NOT affect ' @@ -81,7 +74,13 @@ FILE_OPTIONS = { help='This is the role name used in combination with the ' 'member_role_id option; see that option for more ' 'detail.'), - cfg.IntOpt('crypt_strength', default=40000, + # NOTE(lbragstad/morganfainberg): This value of 10k was + # measured as having an approximate 30% clock-time savings + # over the old default of 40k. The passlib default is not + # static and grows over time to constantly approximate ~300ms + # of CPU time to hash; this was considered too high. This + # value still exceeds the glibc default of 5k. + cfg.IntOpt('crypt_strength', default=10000, min=1000, max=100000, help='The value passed as the keyword "rounds" to ' 'passlib\'s encrypt method.'), cfg.IntOpt('list_limit', @@ -149,9 +148,10 @@ FILE_OPTIONS = { 'identity configuration files if ' 'domain_specific_drivers_enabled is set to true.'), cfg.StrOpt('driver', - default=('keystone.identity.backends' - '.sql.Identity'), - help='Identity backend driver.'), + default='sql', + help='Entrypoint for the identity backend driver in the ' + 'keystone.identity namespace. Supplied drivers are ' + 'ldap and sql.'), cfg.BoolOpt('caching', default=True, help='Toggle for identity caching. This has no ' 'effect unless global caching is enabled.'), @@ -160,6 +160,7 @@ FILE_OPTIONS = { 'no effect unless global and identity caching are ' 'enabled.'), cfg.IntOpt('max_password_length', default=4096, + max=passlib.utils.MAX_PASSWORD_SIZE, help='Maximum supported length for user passwords; ' 'decrease to improve performance.'), cfg.IntOpt('list_limit', @@ -168,15 +169,16 @@ FILE_OPTIONS = { ], 'identity_mapping': [ cfg.StrOpt('driver', - default=('keystone.identity.mapping_backends' - '.sql.Mapping'), - help='Keystone Identity Mapping backend driver.'), + default='sql', + help='Entrypoint for the identity mapping backend driver ' + 'in the keystone.identity.id_mapping namespace.'), cfg.StrOpt('generator', - default=('keystone.identity.id_generators' - '.sha256.Generator'), - help='Public ID generator for user and group entities. ' - 'The Keystone identity mapper only supports ' - 'generators that produce no more than 64 characters.'), + default='sha256', + help='Entrypoint for the public ID generator for user and ' + 'group entities in the keystone.identity.id_generator ' + 'namespace. The Keystone identity mapper only ' + 'supports generators that produce no more than 64 ' + 'characters.'), cfg.BoolOpt('backward_compatible_ids', default=True, help='The format of user and group IDs changed ' @@ -209,8 +211,9 @@ FILE_OPTIONS = { cfg.IntOpt('max_redelegation_count', default=3, help='Maximum depth of trust redelegation.'), cfg.StrOpt('driver', - default='keystone.trust.backends.sql.Trust', - help='Trust backend driver.')], + default='sql', + help='Entrypoint for the trust backend driver in the ' + 'keystone.trust namespace.')], 'os_inherit': [ cfg.BoolOpt('enabled', default=False, help='role-assignment inheritance to projects from ' @@ -245,14 +248,17 @@ FILE_OPTIONS = { help='Amount of time a token should remain valid ' '(in seconds).'), cfg.StrOpt('provider', - default='keystone.token.providers.uuid.Provider', + default='uuid', help='Controls the token construction, validation, and ' - 'revocation operations. Core providers are ' - '"keystone.token.providers.[fernet|pkiz|pki|uuid].' - 'Provider".'), + 'revocation operations. Entrypoint in the ' + 'keystone.token.provider namespace. Core providers ' + 'are [fernet|pkiz|pki|uuid].'), cfg.StrOpt('driver', - default='keystone.token.persistence.backends.sql.Token', - help='Token persistence backend driver.'), + default='sql', + help='Entrypoint for the token persistence backend driver ' + 'in the keystone.token.persistence namespace. ' + 'Supplied drivers are kvs, memcache, memcache_pool, ' + 'and sql.'), cfg.BoolOpt('caching', default=True, help='Toggle for token system caching. This has no ' 'effect unless global caching is enabled.'), @@ -282,9 +288,10 @@ FILE_OPTIONS = { ], 'revoke': [ cfg.StrOpt('driver', - default='keystone.contrib.revoke.backends.sql.Revoke', - help='An implementation of the backend for persisting ' - 'revocation events.'), + default='sql', + help='Entrypoint for an implementation of the backend for ' + 'persisting revocation events in the keystone.revoke ' + 'namespace. Supplied drivers are kvs and sql.'), cfg.IntOpt('expiration_buffer', default=1800, help='This value (calculated in seconds) is added to token ' 'expiration before a revocation event may be removed ' @@ -326,7 +333,7 @@ FILE_OPTIONS = { 'deployments. Small workloads (single process) ' 'like devstack can use the dogpile.cache.memory ' 'backend.'), - cfg.MultiStrOpt('backend_argument', default=[], + cfg.MultiStrOpt('backend_argument', default=[], secret=True, help='Arguments supplied to the backend module. ' 'Specify this option once per argument to be ' 'passed to the dogpile.cache backend. Example ' @@ -379,7 +386,7 @@ FILE_OPTIONS = { cfg.StrOpt('ca_key', default='/etc/keystone/ssl/private/cakey.pem', help='Path of the CA key file for SSL.'), - cfg.IntOpt('key_size', default=1024, + cfg.IntOpt('key_size', default=1024, min=1024, help='SSL key length (in bits) (auto generated ' 'certificate).'), cfg.IntOpt('valid_days', default=3650, @@ -406,7 +413,7 @@ FILE_OPTIONS = { cfg.StrOpt('ca_key', default='/etc/keystone/ssl/private/cakey.pem', help='Path of the CA key for token signing.'), - cfg.IntOpt('key_size', default=2048, + cfg.IntOpt('key_size', default=2048, min=1024, help='Key size (in bits) for token signing cert ' '(auto generated certificate).'), cfg.IntOpt('valid_days', default=3650, @@ -419,17 +426,20 @@ FILE_OPTIONS = { 'token signing.'), ], 'assignment': [ - # assignment has no default for backward compatibility reasons. - # If assignment driver is not specified, the identity driver chooses - # the backend cfg.StrOpt('driver', - help='Assignment backend driver.'), + help='Entrypoint for the assignment backend driver in the ' + 'keystone.assignment namespace. Supplied drivers are ' + 'ldap and sql. If an assignment driver is not ' + 'specified, the identity driver will choose the ' + 'assignment driver.'), ], 'resource': [ cfg.StrOpt('driver', - help='Resource backend driver. If a resource driver is ' - 'not specified, the assignment driver will choose ' - 'the resource driver.'), + help='Entrypoint for the resource backend driver in the ' + 'keystone.resource namespace. Supplied drivers are ' + 'ldap and sql. If a resource driver is not specified, ' + 'the assignment driver will choose the resource ' + 'driver.'), cfg.BoolOpt('caching', default=True, deprecated_opts=[cfg.DeprecatedOpt('caching', group='assignment')], @@ -448,16 +458,25 @@ FILE_OPTIONS = { ], 'domain_config': [ cfg.StrOpt('driver', - default='keystone.resource.config_backends.sql.' - 'DomainConfig', - help='Domain config backend driver.'), + default='sql', + help='Entrypoint for the domain config backend driver in ' + 'the keystone.resource.domain_config namespace.'), + cfg.BoolOpt('caching', default=True, + help='Toggle for domain config caching. This has no ' + 'effect unless global caching is enabled.'), + cfg.IntOpt('cache_time', default=300, + help='TTL (in seconds) to cache domain config data. This ' + 'has no effect unless domain config caching is ' + 'enabled.'), ], 'role': [ # The role driver has no default for backward compatibility reasons. # If role driver is not specified, the assignment driver chooses # the backend cfg.StrOpt('driver', - help='Role backend driver.'), + help='Entrypoint for the role backend driver in the ' + 'keystone.role namespace. Supplied drivers are ldap ' + 'and sql.'), cfg.BoolOpt('caching', default=True, help='Toggle for role caching. This has no effect ' 'unless global caching is enabled.'), @@ -470,14 +489,15 @@ FILE_OPTIONS = { ], 'credential': [ cfg.StrOpt('driver', - default=('keystone.credential.backends' - '.sql.Credential'), - help='Credential backend driver.'), + default='sql', + help='Entrypoint for the credential backend driver in the ' + 'keystone.credential namespace.'), ], 'oauth1': [ cfg.StrOpt('driver', - default='keystone.contrib.oauth1.backends.sql.OAuth1', - help='Credential backend driver.'), + default='sql', + help='Entrypoint for hte OAuth backend driver in the ' + 'keystone.oauth1 namespace.'), cfg.IntOpt('request_token_duration', default=28800, help='Duration (in seconds) for the OAuth Request Token.'), cfg.IntOpt('access_token_duration', default=86400, @@ -485,9 +505,9 @@ FILE_OPTIONS = { ], 'federation': [ cfg.StrOpt('driver', - default='keystone.contrib.federation.' - 'backends.sql.Federation', - help='Federation backend driver.'), + default='sql', + help='Entrypoint for the federation backend driver in the ' + 'keystone.federation namespace.'), cfg.StrOpt('assertion_prefix', default='', help='Value to be used when filtering assertion parameters ' 'from the environment.'), @@ -502,9 +522,7 @@ FILE_OPTIONS = { 'an admin will not be able to create a domain with ' 'this name or update an existing domain to this ' 'name. You are not advised to change this value ' - 'unless you really have to. Changing this option ' - 'to empty string or None will not have any impact and ' - 'default name will be used.'), + 'unless you really have to.'), cfg.MultiStrOpt('trusted_dashboard', default=[], help='A list of trusted dashboard hosts. Before ' 'accepting a Single Sign-On request to return a ' @@ -519,26 +537,31 @@ FILE_OPTIONS = { ], 'policy': [ cfg.StrOpt('driver', - default='keystone.policy.backends.sql.Policy', - help='Policy backend driver.'), + default='sql', + help='Entrypoint for the policy backend driver in the ' + 'keystone.policy namespace. Supplied drivers are ' + 'rules and sql.'), cfg.IntOpt('list_limit', help='Maximum number of entities that will be returned ' 'in a policy collection.'), ], 'endpoint_filter': [ cfg.StrOpt('driver', - default='keystone.contrib.endpoint_filter.backends' - '.sql.EndpointFilter', - help='Endpoint Filter backend driver'), + default='sql', + help='Entrypoint for the endpoint filter backend driver in ' + 'the keystone.endpoint_filter namespace.'), cfg.BoolOpt('return_all_endpoints_if_no_filter', default=True, help='Toggle to return all active endpoints if no filter ' 'exists.'), ], 'endpoint_policy': [ + cfg.BoolOpt('enabled', + default=True, + help='Enable endpoint_policy functionality.'), cfg.StrOpt('driver', - default='keystone.contrib.endpoint_policy.backends' - '.sql.EndpointPolicy', - help='Endpoint policy backend driver'), + default='sql', + help='Entrypoint for the endpoint policy backend driver in ' + 'the keystone.endpoint_policy namespace.'), ], 'ldap': [ cfg.StrOpt('url', default='ldap://localhost', @@ -561,18 +584,19 @@ FILE_OPTIONS = { 'Only enable this option if your LDAP server ' 'supports subtree deletion.'), cfg.StrOpt('query_scope', default='one', - help='The LDAP scope for queries, this can be either ' - '"one" (onelevel/singleLevel) or "sub" ' - '(subtree/wholeSubtree).'), + choices=['one', 'sub'], + help='The LDAP scope for queries, "one" represents ' + 'oneLevel/singleLevel and "sub" represents ' + 'subtree/wholeSubtree options.'), cfg.IntOpt('page_size', default=0, help='Maximum results per page; a value of zero ("0") ' 'disables paging.'), cfg.StrOpt('alias_dereferencing', default='default', - help='The LDAP dereferencing option for queries. This ' - 'can be either "never", "searching", "always", ' - '"finding" or "default". The "default" option falls ' - 'back to using default dereferencing configured by ' - 'your ldap.conf.'), + choices=['never', 'searching', 'always', 'finding', + 'default'], + help='The LDAP dereferencing option for queries. The ' + '"default" option falls back to using default ' + 'dereferencing configured by your ldap.conf.'), cfg.IntOpt('debug_level', help='Sets the LDAP debugging level for LDAP calls. ' 'A value of 0 means that debugging is not enabled. ' @@ -582,7 +606,8 @@ FILE_OPTIONS = { help='Override the system\'s default referral chasing ' 'behavior for queries.'), cfg.StrOpt('user_tree_dn', - help='Search base for users.'), + help='Search base for users. ' + 'Defaults to the suffix value.'), cfg.StrOpt('user_filter', help='LDAP search filter for users.'), cfg.StrOpt('user_objectclass', default='inetOrgPerson', @@ -622,7 +647,7 @@ FILE_OPTIONS = { 'the typical value is "512". This is typically used ' 'when "user_enabled_attribute = userAccountControl".'), cfg.ListOpt('user_attribute_ignore', - default=['default_project_id', 'tenants'], + default=['default_project_id'], help='List of attributes stripped off the user on ' 'update.'), cfg.StrOpt('user_default_project_id_attribute', @@ -653,61 +678,76 @@ FILE_OPTIONS = { cfg.StrOpt('project_tree_dn', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_tree_dn', group='ldap')], - help='Search base for projects'), + deprecated_for_removal=True, + help='Search base for projects. ' + 'Defaults to the suffix value.'), cfg.StrOpt('project_filter', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_filter', group='ldap')], + deprecated_for_removal=True, help='LDAP search filter for projects.'), cfg.StrOpt('project_objectclass', default='groupOfNames', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_objectclass', group='ldap')], + deprecated_for_removal=True, help='LDAP objectclass for projects.'), cfg.StrOpt('project_id_attribute', default='cn', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_id_attribute', group='ldap')], + deprecated_for_removal=True, help='LDAP attribute mapped to project id.'), cfg.StrOpt('project_member_attribute', default='member', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_member_attribute', group='ldap')], + deprecated_for_removal=True, help='LDAP attribute mapped to project membership for ' 'user.'), cfg.StrOpt('project_name_attribute', default='ou', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_name_attribute', group='ldap')], + deprecated_for_removal=True, help='LDAP attribute mapped to project name.'), cfg.StrOpt('project_desc_attribute', default='description', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_desc_attribute', group='ldap')], + deprecated_for_removal=True, help='LDAP attribute mapped to project description.'), cfg.StrOpt('project_enabled_attribute', default='enabled', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_enabled_attribute', group='ldap')], + deprecated_for_removal=True, help='LDAP attribute mapped to project enabled.'), cfg.StrOpt('project_domain_id_attribute', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_domain_id_attribute', group='ldap')], + deprecated_for_removal=True, default='businessCategory', help='LDAP attribute mapped to project domain_id.'), cfg.ListOpt('project_attribute_ignore', default=[], deprecated_opts=[cfg.DeprecatedOpt( 'tenant_attribute_ignore', group='ldap')], + deprecated_for_removal=True, help='List of attributes stripped off the project on ' 'update.'), cfg.BoolOpt('project_allow_create', default=True, deprecated_opts=[cfg.DeprecatedOpt( 'tenant_allow_create', group='ldap')], + deprecated_for_removal=True, help='Allow project creation in LDAP backend.'), cfg.BoolOpt('project_allow_update', default=True, deprecated_opts=[cfg.DeprecatedOpt( 'tenant_allow_update', group='ldap')], + deprecated_for_removal=True, help='Allow project update in LDAP backend.'), cfg.BoolOpt('project_allow_delete', default=True, deprecated_opts=[cfg.DeprecatedOpt( 'tenant_allow_delete', group='ldap')], + deprecated_for_removal=True, help='Allow project deletion in LDAP backend.'), cfg.BoolOpt('project_enabled_emulation', default=False, deprecated_opts=[cfg.DeprecatedOpt( 'tenant_enabled_emulation', group='ldap')], + deprecated_for_removal=True, help='If true, Keystone uses an alternative method to ' 'determine if a project is enabled or not by ' 'checking if they are a member of the ' @@ -715,11 +755,13 @@ FILE_OPTIONS = { cfg.StrOpt('project_enabled_emulation_dn', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_enabled_emulation_dn', group='ldap')], + deprecated_for_removal=True, help='DN of the group entry to hold enabled projects when ' 'using enabled emulation.'), cfg.ListOpt('project_additional_attribute_mapping', deprecated_opts=[cfg.DeprecatedOpt( 'tenant_additional_attribute_mapping', group='ldap')], + deprecated_for_removal=True, default=[], help='Additional attribute mappings for projects. ' 'Attribute mapping format is ' @@ -728,27 +770,39 @@ FILE_OPTIONS = { 'Identity API attribute.'), cfg.StrOpt('role_tree_dn', - help='Search base for roles.'), + deprecated_for_removal=True, + help='Search base for roles. ' + 'Defaults to the suffix value.'), cfg.StrOpt('role_filter', + deprecated_for_removal=True, help='LDAP search filter for roles.'), cfg.StrOpt('role_objectclass', default='organizationalRole', + deprecated_for_removal=True, help='LDAP objectclass for roles.'), cfg.StrOpt('role_id_attribute', default='cn', + deprecated_for_removal=True, help='LDAP attribute mapped to role id.'), cfg.StrOpt('role_name_attribute', default='ou', + deprecated_for_removal=True, help='LDAP attribute mapped to role name.'), cfg.StrOpt('role_member_attribute', default='roleOccupant', + deprecated_for_removal=True, help='LDAP attribute mapped to role membership.'), cfg.ListOpt('role_attribute_ignore', default=[], + deprecated_for_removal=True, help='List of attributes stripped off the role on ' 'update.'), cfg.BoolOpt('role_allow_create', default=True, + deprecated_for_removal=True, help='Allow role creation in LDAP backend.'), cfg.BoolOpt('role_allow_update', default=True, + deprecated_for_removal=True, help='Allow role update in LDAP backend.'), cfg.BoolOpt('role_allow_delete', default=True, + deprecated_for_removal=True, help='Allow role deletion in LDAP backend.'), cfg.ListOpt('role_additional_attribute_mapping', + deprecated_for_removal=True, default=[], help='Additional attribute mappings for roles. Attribute ' 'mapping format is <ldap_attr>:<user_attr>, where ' @@ -756,7 +810,8 @@ FILE_OPTIONS = { 'user_attr is the Identity API attribute.'), cfg.StrOpt('group_tree_dn', - help='Search base for groups.'), + help='Search base for groups. ' + 'Defaults to the suffix value.'), cfg.StrOpt('group_filter', help='LDAP search filter for groups.'), cfg.StrOpt('group_objectclass', default='groupOfNames', @@ -794,8 +849,9 @@ FILE_OPTIONS = { cfg.BoolOpt('use_tls', default=False, help='Enable TLS for communicating with LDAP servers.'), cfg.StrOpt('tls_req_cert', default='demand', - help='Valid options for tls_req_cert are demand, never, ' - 'and allow.'), + choices=['demand', 'never', 'allow'], + help='Specifies what checks to perform on client ' + 'certificates in an incoming TLS session.'), cfg.BoolOpt('use_pool', default=False, help='Enable LDAP connection pooling.'), cfg.IntOpt('pool_size', default=10, @@ -821,20 +877,22 @@ FILE_OPTIONS = { ], 'auth': [ cfg.ListOpt('methods', default=_DEFAULT_AUTH_METHODS, - help='Default auth methods.'), + help='Allowed authentication methods.'), cfg.StrOpt('password', - default='keystone.auth.plugins.password.Password', - help='The password auth plugin module.'), + help='Entrypoint for the password auth plugin module in ' + 'the keystone.auth.password namespace.'), cfg.StrOpt('token', - default='keystone.auth.plugins.token.Token', - help='The token auth plugin module.'), + help='Entrypoint for the token auth plugin module in the ' + 'keystone.auth.token namespace.'), # deals with REMOTE_USER authentication cfg.StrOpt('external', - default='keystone.auth.plugins.external.DefaultDomain', - help='The external (REMOTE_USER) auth plugin module.'), + help='Entrypoint for the external (REMOTE_USER) auth ' + 'plugin module in the keystone.auth.external ' + 'namespace. Supplied drivers are DefaultDomain and ' + 'Domain. The default driver is DefaultDomain.'), cfg.StrOpt('oauth1', - default='keystone.auth.plugins.oauth1.OAuth', - help='The oAuth1.0 auth plugin module.'), + help='Entrypoint for the oAuth1.0 auth plugin module in ' + 'the keystone.auth.oauth1 namespace.'), ], 'paste_deploy': [ cfg.StrOpt('config_file', default='keystone-paste.ini', @@ -880,8 +938,10 @@ FILE_OPTIONS = { help='Catalog template file name for use with the ' 'template catalog backend.'), cfg.StrOpt('driver', - default='keystone.catalog.backends.sql.Catalog', - help='Catalog backend driver.'), + default='sql', + help='Entrypoint for the catalog backend driver in the ' + 'keystone.catalog namespace. Supplied drivers are ' + 'kvs, sql, templated, and endpoint_filter.sql'), cfg.BoolOpt('caching', default=True, help='Toggle for catalog caching. This has no ' 'effect unless global caching is enabled.'), @@ -963,25 +1023,33 @@ FILE_OPTIONS = { cfg.StrOpt('idp_contact_telephone', help='Telephone number of contact person.'), cfg.StrOpt('idp_contact_type', default='other', - help='Contact type. Allowed values are: ' - 'technical, support, administrative ' - 'billing, and other'), + choices=['technical', 'support', 'administrative', + 'billing', 'other'], + help='The contact type describing the main point of ' + 'contact for the identity provider.'), cfg.StrOpt('idp_metadata_path', default='/etc/keystone/saml2_idp_metadata.xml', help='Path to the Identity Provider Metadata file. ' 'This file should be generated with the ' 'keystone-manage saml_idp_metadata command.'), + cfg.StrOpt('relay_state_prefix', + default='ss:mem:', + help='The prefix to use for the RelayState SAML ' + 'attribute, used when generating ECP wrapped ' + 'assertions.'), ], 'eventlet_server': [ cfg.IntOpt('public_workers', deprecated_name='public_workers', deprecated_group='DEFAULT', + deprecated_for_removal=True, help='The number of worker processes to serve the public ' 'eventlet application. Defaults to number of CPUs ' '(minimum of 2).'), cfg.IntOpt('admin_workers', deprecated_name='admin_workers', deprecated_group='DEFAULT', + deprecated_for_removal=True, help='The number of worker processes to serve the admin ' 'eventlet application. Defaults to number of CPUs ' '(minimum of 2).'), @@ -991,10 +1059,13 @@ FILE_OPTIONS = { group='DEFAULT'), cfg.DeprecatedOpt('public_bind_host', group='DEFAULT'), ], + deprecated_for_removal=True, help='The IP address of the network interface for the ' 'public service to listen on.'), - cfg.IntOpt('public_port', default=5000, deprecated_name='public_port', + cfg.IntOpt('public_port', default=5000, min=1, max=65535, + deprecated_name='public_port', deprecated_group='DEFAULT', + deprecated_for_removal=True, help='The port number which the public service listens ' 'on.'), cfg.StrOpt('admin_bind_host', @@ -1003,15 +1074,28 @@ FILE_OPTIONS = { group='DEFAULT'), cfg.DeprecatedOpt('admin_bind_host', group='DEFAULT')], + deprecated_for_removal=True, help='The IP address of the network interface for the ' 'admin service to listen on.'), - cfg.IntOpt('admin_port', default=35357, deprecated_name='admin_port', + cfg.IntOpt('admin_port', default=35357, min=1, max=65535, + deprecated_name='admin_port', deprecated_group='DEFAULT', + deprecated_for_removal=True, help='The port number which the admin service listens ' 'on.'), + cfg.BoolOpt('wsgi_keep_alive', default=True, + help="If set to false, disables keepalives on the server; " + "all connections will be closed after serving one " + "request."), + cfg.IntOpt('client_socket_timeout', default=900, + help="Timeout for socket operations on a client " + "connection. If an incoming connection is idle for " + "this number of seconds it will be closed. A value " + "of '0' means wait forever."), cfg.BoolOpt('tcp_keepalive', default=False, deprecated_name='tcp_keepalive', deprecated_group='DEFAULT', + deprecated_for_removal=True, help='Set this to true if you want to enable ' 'TCP_KEEPALIVE on server sockets, i.e. sockets used ' 'by the Keystone wsgi server for client ' @@ -1020,6 +1104,7 @@ FILE_OPTIONS = { default=600, deprecated_name='tcp_keepidle', deprecated_group='DEFAULT', + deprecated_for_removal=True, help='Sets the value of TCP_KEEPIDLE in seconds for each ' 'server socket. Only applies if tcp_keepalive is ' 'true.'), @@ -1027,11 +1112,13 @@ FILE_OPTIONS = { 'eventlet_server_ssl': [ cfg.BoolOpt('enable', default=False, deprecated_name='enable', deprecated_group='ssl', + deprecated_for_removal=True, help='Toggle for SSL support on the Keystone ' 'eventlet servers.'), cfg.StrOpt('certfile', default="/etc/keystone/ssl/certs/keystone.pem", deprecated_name='certfile', deprecated_group='ssl', + deprecated_for_removal=True, help='Path of the certfile for SSL. For non-production ' 'environments, you may be interested in using ' '`keystone-manage ssl_setup` to generate self-signed ' @@ -1039,13 +1126,16 @@ FILE_OPTIONS = { cfg.StrOpt('keyfile', default='/etc/keystone/ssl/private/keystonekey.pem', deprecated_name='keyfile', deprecated_group='ssl', + deprecated_for_removal=True, help='Path of the keyfile for SSL.'), cfg.StrOpt('ca_certs', default='/etc/keystone/ssl/certs/ca.pem', deprecated_name='ca_certs', deprecated_group='ssl', + deprecated_for_removal=True, help='Path of the CA cert file for SSL.'), cfg.BoolOpt('cert_required', default=False, deprecated_name='cert_required', deprecated_group='ssl', + deprecated_for_removal=True, help='Require client certificate.'), ], } @@ -1080,7 +1170,7 @@ def configure(conf=None): cfg.StrOpt('pydev-debug-host', help='Host to connect to for remote debugger.')) conf.register_cli_opt( - cfg.IntOpt('pydev-debug-port', + cfg.IntOpt('pydev-debug-port', min=1, max=65535, help='Port to connect to for remote debugger.')) for section in FILE_OPTIONS: @@ -1115,4 +1205,4 @@ def list_opts(): :returns: a list of (group_name, opts) tuples """ - return FILE_OPTIONS.items() + return list(FILE_OPTIONS.items()) diff --git a/keystone-moon/keystone/common/controller.py b/keystone-moon/keystone/common/controller.py index bd26b7c4..bc7074ac 100644 --- a/keystone-moon/keystone/common/controller.py +++ b/keystone-moon/keystone/common/controller.py @@ -17,6 +17,7 @@ import uuid from oslo_config import cfg from oslo_log import log +from oslo_utils import strutils import six from keystone.common import authorization @@ -39,7 +40,7 @@ def v2_deprecated(f): This is a placeholder for the pending deprecation of v2. The implementation of this decorator can be replaced with:: - from keystone.openstack.common import versionutils + from oslo_log import versionutils v2_deprecated = versionutils.deprecated( @@ -52,9 +53,12 @@ def v2_deprecated(f): def _build_policy_check_credentials(self, action, context, kwargs): + kwargs_str = ', '.join(['%s=%s' % (k, kwargs[k]) for k in kwargs]) + kwargs_str = strutils.mask_password(kwargs_str) + LOG.debug('RBAC: Authorizing %(action)s(%(kwargs)s)', { 'action': action, - 'kwargs': ', '.join(['%s=%s' % (k, kwargs[k]) for k in kwargs])}) + 'kwargs': kwargs_str}) # see if auth context has already been created. If so use it. if ('environment' in context and @@ -219,7 +223,11 @@ class V2Controller(wsgi.Application): @staticmethod def filter_domain_id(ref): """Remove domain_id since v2 calls are not domain-aware.""" - ref.pop('domain_id', None) + if 'domain_id' in ref: + if ref['domain_id'] != CONF.identity.default_domain_id: + raise exception.Unauthorized( + _('Non-default domain is not supported')) + del ref['domain_id'] return ref @staticmethod @@ -239,6 +247,18 @@ class V2Controller(wsgi.Application): return ref @staticmethod + def filter_project_parent_id(ref): + """Remove parent_id since v2 calls are not hierarchy-aware.""" + ref.pop('parent_id', None) + return ref + + @staticmethod + def filter_is_domain(ref): + """Remove is_domain field since v2 calls are not domain-aware.""" + ref.pop('is_domain', None) + return ref + + @staticmethod def normalize_username_in_response(ref): """Adds username to outgoing user refs to match the v2 spec. @@ -266,9 +286,12 @@ class V2Controller(wsgi.Application): def v3_to_v2_user(ref): """Convert a user_ref from v3 to v2 compatible. - * v2.0 users are not domain aware, and should have domain_id removed - * v2.0 users expect the use of tenantId instead of default_project_id - * v2.0 users have a username attribute + - v2.0 users are not domain aware, and should have domain_id validated + to be the default domain, and then removed. + + - v2.0 users expect the use of tenantId instead of default_project_id. + + - v2.0 users have a username attribute. This method should only be applied to user_refs being returned from the v2.0 controller(s). @@ -304,6 +327,35 @@ class V2Controller(wsgi.Application): else: raise ValueError(_('Expected dict or list: %s') % type(ref)) + @staticmethod + def v3_to_v2_project(ref): + """Convert a project_ref from v3 to v2. + + * v2.0 projects are not domain aware, and should have domain_id removed + * v2.0 projects are not hierarchy aware, and should have parent_id + removed + + This method should only be applied to project_refs being returned from + the v2.0 controller(s). + + If ref is a list type, we will iterate through each element and do the + conversion. + """ + + def _filter_project_properties(ref): + """Run through the various filter methods.""" + V2Controller.filter_domain_id(ref) + V2Controller.filter_project_parent_id(ref) + V2Controller.filter_is_domain(ref) + return ref + + if isinstance(ref, dict): + return _filter_project_properties(ref) + elif isinstance(ref, list): + return [_filter_project_properties(x) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + def format_project_list(self, tenant_refs, **kwargs): """Format a v2 style project list, including marker/limits.""" marker = kwargs.get('marker') @@ -656,19 +708,7 @@ class V3Controller(wsgi.Application): if context['query_string'].get('domain_id') is not None: return context['query_string'].get('domain_id') - try: - token_ref = token_model.KeystoneToken( - token_id=context['token_id'], - token_data=self.token_provider_api.validate_token( - context['token_id'])) - except KeyError: - raise exception.ValidationError( - _('domain_id is required as part of entity')) - except (exception.TokenNotFound, - exception.UnsupportedTokenVersionException): - LOG.warning(_LW('Invalid token found while getting domain ID ' - 'for list request')) - raise exception.Unauthorized() + token_ref = utils.get_token_ref(context) if token_ref.domain_scoped: return token_ref.domain_id @@ -685,25 +725,7 @@ class V3Controller(wsgi.Application): being used. """ - # We could make this more efficient by loading the domain_id - # into the context in the wrapper function above (since - # this version of normalize_domain will only be called inside - # a v3 protected call). However, this optimization is probably not - # worth the duplication of state - try: - token_ref = token_model.KeystoneToken( - token_id=context['token_id'], - token_data=self.token_provider_api.validate_token( - context['token_id'])) - except KeyError: - # This might happen if we use the Admin token, for instance - raise exception.ValidationError( - _('A domain-scoped token must be used')) - except (exception.TokenNotFound, - exception.UnsupportedTokenVersionException): - LOG.warning(_LW('Invalid token found while getting domain ID ' - 'for list request')) - raise exception.Unauthorized() + token_ref = utils.get_token_ref(context) if token_ref.domain_scoped: return token_ref.domain_id diff --git a/keystone-moon/keystone/common/dependency.py b/keystone-moon/keystone/common/dependency.py index 14a68f19..e19f705f 100644 --- a/keystone-moon/keystone/common/dependency.py +++ b/keystone-moon/keystone/common/dependency.py @@ -15,9 +15,9 @@ """This module provides support for dependency injection. Providers are registered via the ``@provider()`` decorator, and dependencies on -them are registered with ``@requires()`` or ``@optional()``. Providers are -available to their consumers via an attribute. See the documentation for the -individual functions for more detail. +them are registered with ``@requires()``. Providers are available to their +consumers via an attribute. See the documentation for the individual functions +for more detail. See also: @@ -27,16 +27,12 @@ See also: import traceback -import six - from keystone.i18n import _ -from keystone import notifications _REGISTRY = {} _future_dependencies = {} -_future_optionals = {} _factories = {} @@ -94,44 +90,10 @@ def provider(name): """ def wrapper(cls): def wrapped(init): - def register_event_callbacks(self): - # NOTE(morganfainberg): A provider who has an implicit - # dependency on other providers may utilize the event callback - # mechanism to react to any changes in those providers. This is - # performed at the .provider() mechanism so that we can ensure - # that the callback is only ever called once and guaranteed - # to be on the properly configured and instantiated backend. - if not hasattr(self, 'event_callbacks'): - return - - if not isinstance(self.event_callbacks, dict): - msg = _('event_callbacks must be a dict') - raise ValueError(msg) - - for event in self.event_callbacks: - if not isinstance(self.event_callbacks[event], dict): - msg = _('event_callbacks[%s] must be a dict') % event - raise ValueError(msg) - for resource_type in self.event_callbacks[event]: - # Make sure we register the provider for each event it - # cares to call back. - callbacks = self.event_callbacks[event][resource_type] - if not callbacks: - continue - if not hasattr(callbacks, '__iter__'): - # ensure the callback information is a list - # allowing multiple callbacks to exist - callbacks = [callbacks] - notifications.register_event_callback(event, - resource_type, - callbacks) - def __wrapped_init__(self, *args, **kwargs): """Initialize the wrapped object and add it to the registry.""" init(self, *args, **kwargs) _set_provider(name, self) - register_event_callbacks(self) - resolve_future_dependencies(__provider_name=name) return __wrapped_init__ @@ -157,7 +119,6 @@ def _process_dependencies(obj): setattr(obj, dependency, get_provider(dependency)) process(obj, '_dependencies', _future_dependencies) - process(obj, '_optionals', _future_optionals) def requires(*dependencies): @@ -210,34 +171,6 @@ def requires(*dependencies): return wrapped -def optional(*dependencies): - """Similar to ``@requires()``, except that the dependencies are optional. - - If no provider is available, the attributes will be set to ``None``. - - """ - def wrapper(self, *args, **kwargs): - """Inject each dependency from the registry.""" - self.__wrapped_init__(*args, **kwargs) - _process_dependencies(self) - - def wrapped(cls): - """Note the optional dependencies on the object for later injection. - - The dependencies of the parent class are combined with that of the - child class to create a new set of dependencies. - - """ - existing_optionals = getattr(cls, '_optionals', set()) - cls._optionals = existing_optionals.union(dependencies) - if not hasattr(cls, '__wrapped_init__'): - cls.__wrapped_init__ = cls.__init__ - cls.__init__ = wrapper - return cls - - return wrapped - - def resolve_future_dependencies(__provider_name=None): """Forces injection of all dependencies. @@ -259,29 +192,16 @@ def resolve_future_dependencies(__provider_name=None): # A provider was registered, so take care of any objects depending on # it. targets = _future_dependencies.pop(__provider_name, []) - targets.extend(_future_optionals.pop(__provider_name, [])) for target in targets: setattr(target, __provider_name, get_provider(__provider_name)) return - # Resolve optional dependencies, sets the attribute to None if there's no - # provider registered. - for dependency, targets in six.iteritems(_future_optionals.copy()): - provider = get_provider(dependency, optional=GET_OPTIONAL) - if provider is None: - factory = _factories.get(dependency) - if factory: - provider = factory() - new_providers[dependency] = provider - for target in targets: - setattr(target, dependency, provider) - # Resolve future dependencies, raises UnresolvableDependencyException if # there's no provider registered. try: - for dependency, targets in six.iteritems(_future_dependencies.copy()): + for dependency, targets in _future_dependencies.copy().items(): if dependency not in _REGISTRY: # a Class was registered that could fulfill the dependency, but # it has not yet been initialized. @@ -308,4 +228,3 @@ def reset(): _REGISTRY.clear() _future_dependencies.clear() - _future_optionals.clear() diff --git a/keystone-moon/keystone/common/driver_hints.py b/keystone-moon/keystone/common/driver_hints.py index 0361e314..ff0a774c 100644 --- a/keystone-moon/keystone/common/driver_hints.py +++ b/keystone-moon/keystone/common/driver_hints.py @@ -30,6 +30,10 @@ class Hints(object): accessed publicly. Also it contains a dict called limit, which will indicate the amount of data we want to limit our listing to. + If the filter is discovered to never match, then `cannot_match` can be set + to indicate that there will not be any matches and the backend work can be + short-circuited. + Each filter term consists of: * ``name``: the name of the attribute being matched @@ -44,6 +48,7 @@ class Hints(object): def __init__(self): self.limit = None self.filters = list() + self.cannot_match = False def add_filter(self, name, value, comparator='equals', case_sensitive=False): diff --git a/keystone-moon/keystone/common/environment/__init__.py b/keystone-moon/keystone/common/environment/__init__.py index da1de890..3edf6b0b 100644 --- a/keystone-moon/keystone/common/environment/__init__.py +++ b/keystone-moon/keystone/common/environment/__init__.py @@ -17,6 +17,7 @@ import os from oslo_log import log + LOG = log.getLogger(__name__) @@ -93,7 +94,7 @@ def use_eventlet(monkeypatch_thread=None): def use_stdlib(): global httplib, subprocess - import httplib as _httplib + import six.moves.http_client as _httplib import subprocess as _subprocess httplib = _httplib diff --git a/keystone-moon/keystone/common/environment/eventlet_server.py b/keystone-moon/keystone/common/environment/eventlet_server.py index 639e074a..398952e1 100644 --- a/keystone-moon/keystone/common/environment/eventlet_server.py +++ b/keystone-moon/keystone/common/environment/eventlet_server.py @@ -25,12 +25,17 @@ import sys import eventlet import eventlet.wsgi import greenlet +from oslo_config import cfg from oslo_log import log from oslo_log import loggers +from oslo_service import service from keystone.i18n import _LE, _LI +CONF = cfg.CONF + + LOG = log.getLogger(__name__) # The size of a pool that is used to spawn a single green thread in which @@ -62,7 +67,7 @@ class EventletFilteringLogger(loggers.WritableLogger): self.logger.log(self.level, msg.rstrip()) -class Server(object): +class Server(service.ServiceBase): """Server class to manage multiple WSGI sockets and applications.""" def __init__(self, application, host=None, port=None, keepalive=False, @@ -173,7 +178,7 @@ class Server(object): The service interface is used by the launcher when receiving a SIGHUP. The service interface is defined in - keystone.openstack.common.service.Service. + oslo_service.service.Service. Keystone does not need to do anything here. """ @@ -182,10 +187,17 @@ class Server(object): def _run(self, application, socket): """Start a WSGI server with a new green thread pool.""" logger = log.getLogger('eventlet.wsgi.server') + + # NOTE(dolph): [eventlet_server] client_socket_timeout is required to + # be an integer in keystone.conf, but in order to make + # eventlet.wsgi.server() wait forever, we pass None instead of 0. + socket_timeout = CONF.eventlet_server.client_socket_timeout or None + try: - eventlet.wsgi.server(socket, application, - log=EventletFilteringLogger(logger), - debug=False) + eventlet.wsgi.server( + socket, application, log=EventletFilteringLogger(logger), + debug=False, keepalive=CONF.eventlet_server.wsgi_keep_alive, + socket_timeout=socket_timeout) except greenlet.GreenletExit: # Wait until all servers have completed running pass diff --git a/keystone-moon/keystone/common/json_home.py b/keystone-moon/keystone/common/json_home.py index 215d596a..c048a356 100644 --- a/keystone-moon/keystone/common/json_home.py +++ b/keystone-moon/keystone/common/json_home.py @@ -13,7 +13,8 @@ # under the License. -import six +from keystone import exception +from keystone.i18n import _ def build_v3_resource_relation(resource_name): @@ -62,14 +63,24 @@ class Status(object): STABLE = 'stable' @classmethod - def is_supported(cls, status): - return status in [cls.DEPRECATED, cls.EXPERIMENTAL, cls.STABLE] + def update_resource_data(cls, resource_data, status): + if status is cls.STABLE: + # We currently do not add a status if the resource is stable, the + # absence of the status property can be taken as meaning that the + # resource is stable. + return + if status is cls.DEPRECATED or status is cls.EXPERIMENTAL: + resource_data['hints'] = {'status': status} + return + + raise exception.Error(message=_( + 'Unexpected status requested for JSON Home response, %s') % status) def translate_urls(json_home, new_prefix): """Given a JSON Home document, sticks new_prefix on each of the urls.""" - for dummy_rel, resource in six.iteritems(json_home['resources']): + for dummy_rel, resource in json_home['resources'].items(): if 'href' in resource: resource['href'] = new_prefix + resource['href'] elif 'href-template' in resource: diff --git a/keystone-moon/keystone/common/kvs/backends/memcached.py b/keystone-moon/keystone/common/kvs/backends/memcached.py index db453143..f54c1a01 100644 --- a/keystone-moon/keystone/common/kvs/backends/memcached.py +++ b/keystone-moon/keystone/common/kvs/backends/memcached.py @@ -23,9 +23,9 @@ from dogpile.cache import api from dogpile.cache.backends import memcached from oslo_config import cfg from oslo_log import log +from six.moves import range from keystone.common.cache.backends import memcache_pool -from keystone.common import manager from keystone import exception from keystone.i18n import _ @@ -73,12 +73,13 @@ class MemcachedLock(object): client.delete(self.key) -class MemcachedBackend(manager.Manager): +class MemcachedBackend(object): """Pivot point to leverage the various dogpile.cache memcached backends. - To specify a specific dogpile.cache memcached driver, pass the argument - `memcached_driver` set to one of the provided memcached drivers (at this - time `memcached`, `bmemcached`, `pylibmc` are valid). + To specify a specific dogpile.cache memcached backend, pass the argument + `memcached_backend` set to one of the provided memcached backends (at this + time `memcached`, `bmemcached`, `pylibmc` and `pooled_memcached` are + valid). """ def __init__(self, arguments): self._key_mangler = None @@ -105,13 +106,19 @@ class MemcachedBackend(manager.Manager): else: if backend not in VALID_DOGPILE_BACKENDS: raise ValueError( - _('Backend `%(driver)s` is not a valid memcached ' - 'backend. Valid drivers: %(driver_list)s') % - {'driver': backend, - 'driver_list': ','.join(VALID_DOGPILE_BACKENDS.keys())}) + _('Backend `%(backend)s` is not a valid memcached ' + 'backend. Valid backends: %(backend_list)s') % + {'backend': backend, + 'backend_list': ','.join(VALID_DOGPILE_BACKENDS.keys())}) else: self.driver = VALID_DOGPILE_BACKENDS[backend](arguments) + def __getattr__(self, name): + """Forward calls to the underlying driver.""" + f = getattr(self.driver, name) + setattr(self, name, f) + return f + def _get_set_arguments_driver_attr(self, exclude_expiry=False): # NOTE(morganfainberg): Shallow copy the .set_arguments dict to diff --git a/keystone-moon/keystone/common/kvs/core.py b/keystone-moon/keystone/common/kvs/core.py index cbbb7462..6ce7b318 100644 --- a/keystone-moon/keystone/common/kvs/core.py +++ b/keystone-moon/keystone/common/kvs/core.py @@ -25,7 +25,6 @@ from dogpile.core import nameregistry from oslo_config import cfg from oslo_log import log from oslo_utils import importutils -import six from keystone import exception from keystone.i18n import _ @@ -147,24 +146,24 @@ class KeyValueStore(object): self._region.name) def _set_keymangler_on_backend(self, key_mangler): - try: - self._region.backend.key_mangler = key_mangler - except Exception as e: - # NOTE(morganfainberg): The setting of the key_mangler on the - # backend is used to allow the backend to - # calculate a hashed key value as needed. Not all backends - # require the ability to calculate hashed keys. If the - # backend does not support/require this feature log a - # debug line and move on otherwise raise the proper exception. - # Support of the feature is implied by the existence of the - # 'raw_no_expiry_keys' attribute. - if not hasattr(self._region.backend, 'raw_no_expiry_keys'): - LOG.debug(('Non-expiring keys not supported/required by ' - '%(region)s backend; unable to set ' - 'key_mangler for backend: %(err)s'), - {'region': self._region.name, 'err': e}) - else: - raise + try: + self._region.backend.key_mangler = key_mangler + except Exception as e: + # NOTE(morganfainberg): The setting of the key_mangler on the + # backend is used to allow the backend to + # calculate a hashed key value as needed. Not all backends + # require the ability to calculate hashed keys. If the + # backend does not support/require this feature log a + # debug line and move on otherwise raise the proper exception. + # Support of the feature is implied by the existence of the + # 'raw_no_expiry_keys' attribute. + if not hasattr(self._region.backend, 'raw_no_expiry_keys'): + LOG.debug(('Non-expiring keys not supported/required by ' + '%(region)s backend; unable to set ' + 'key_mangler for backend: %(err)s'), + {'region': self._region.name, 'err': e}) + else: + raise def _set_key_mangler(self, key_mangler): # Set the key_mangler that is appropriate for the given region being @@ -232,7 +231,7 @@ class KeyValueStore(object): if config_args['lock_timeout'] > 0: config_args['lock_timeout'] += LOCK_WINDOW - for argument, value in six.iteritems(config_args): + for argument, value in config_args.items(): arg_key = '.'.join([prefix, 'arguments', argument]) conf_dict[arg_key] = value diff --git a/keystone-moon/keystone/common/kvs/legacy.py b/keystone-moon/keystone/common/kvs/legacy.py index ba036016..7e27d97f 100644 --- a/keystone-moon/keystone/common/kvs/legacy.py +++ b/keystone-moon/keystone/common/kvs/legacy.py @@ -12,8 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import versionutils + from keystone import exception -from keystone.openstack.common import versionutils class DictKvs(dict): diff --git a/keystone-moon/keystone/common/ldap/core.py b/keystone-moon/keystone/common/ldap/core.py index 144c0cfd..0bb3830c 100644 --- a/keystone-moon/keystone/common/ldap/core.py +++ b/keystone-moon/keystone/common/ldap/core.py @@ -24,11 +24,13 @@ import ldap.filter import ldappool from oslo_log import log import six +from six.moves import map, zip from keystone import exception from keystone.i18n import _ from keystone.i18n import _LW + LOG = log.getLogger(__name__) LDAP_VALUES = {'TRUE': True, 'FALSE': False} @@ -159,7 +161,7 @@ def convert_ldap_result(ldap_result): at_least_one_referral = True continue - for kind, values in six.iteritems(attrs): + for kind, values in attrs.items(): try: val2py = enabled2py if kind == 'enabled' else ldap2py ldap_attrs[kind] = [val2py(x) for x in values] @@ -327,7 +329,7 @@ def dn_startswith(descendant_dn, dn): @six.add_metaclass(abc.ABCMeta) class LDAPHandler(object): - '''Abstract class which defines methods for a LDAP API provider. + """Abstract class which defines methods for a LDAP API provider. Native Keystone values cannot be passed directly into and from the python-ldap API. Type conversion must occur at the LDAP API @@ -415,7 +417,8 @@ class LDAPHandler(object): method to any derivations of the abstract class the code will fail to load and run making it impossible to forget updating all the derived classes. - ''' + + """ @abc.abstractmethod def __init__(self, conn=None): self.conn = conn @@ -481,13 +484,13 @@ class LDAPHandler(object): class PythonLDAPHandler(LDAPHandler): - '''Implementation of the LDAPHandler interface which calls the - python-ldap API. + """LDAPHandler implementation which calls the python-ldap API. - Note, the python-ldap API requires all string values to be UTF-8 - encoded. The KeystoneLDAPHandler enforces this prior to invoking - the methods in this class. - ''' + Note, the python-ldap API requires all string values to be UTF-8 encoded. + The KeystoneLDAPHandler enforces this prior to invoking the methods in this + class. + + """ def __init__(self, conn=None): super(PythonLDAPHandler, self).__init__(conn=conn) @@ -569,10 +572,7 @@ class PythonLDAPHandler(LDAPHandler): def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None, tls_cacertdir=None, tls_req_cert=None, debug_level=None): - '''Method for common ldap initialization between PythonLDAPHandler and - PooledLDAPHandler. - ''' - + """LDAP initialization for PythonLDAPHandler and PooledLDAPHandler.""" LOG.debug("LDAP init: url=%s", url) LOG.debug('LDAP init: use_tls=%s tls_cacertfile=%s tls_cacertdir=%s ' 'tls_req_cert=%s tls_avail=%s', @@ -616,7 +616,7 @@ def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None, "or is not a directory") % tls_cacertdir) ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir) - if tls_req_cert in LDAP_TLS_CERTS.values(): + if tls_req_cert in list(LDAP_TLS_CERTS.values()): ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert) else: LOG.debug("LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s", @@ -624,15 +624,16 @@ def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None, class MsgId(list): - '''Wrapper class to hold connection and msgid.''' + """Wrapper class to hold connection and msgid.""" pass def use_conn_pool(func): - '''Use this only for connection pool specific ldap API. + """Use this only for connection pool specific ldap API. This adds connection object to decorated API as next argument after self. - ''' + + """ def wrapper(self, *args, **kwargs): # assert isinstance(self, PooledLDAPHandler) with self._get_pool_connection() as conn: @@ -642,8 +643,7 @@ def use_conn_pool(func): class PooledLDAPHandler(LDAPHandler): - '''Implementation of the LDAPHandler interface which uses pooled - connection manager. + """LDAPHandler implementation which uses pooled connection manager. Pool specific configuration is defined in [ldap] section. All other LDAP configuration is still used from [ldap] section @@ -663,8 +663,8 @@ class PooledLDAPHandler(LDAPHandler): Note, the python-ldap API requires all string values to be UTF-8 encoded. The KeystoneLDAPHandler enforces this prior to invoking the methods in this class. - ''' + """ # Added here to allow override for testing Connector = ldappool.StateConnector auth_pool_prefix = 'auth_pool_' @@ -737,7 +737,7 @@ class PooledLDAPHandler(LDAPHandler): # if connection has a lifetime, then it already has options specified if conn.get_lifetime() > 30: return - for option, invalue in six.iteritems(self.conn_options): + for option, invalue in self.conn_options.items(): conn.set_option(option, invalue) def _get_pool_connection(self): @@ -745,9 +745,8 @@ class PooledLDAPHandler(LDAPHandler): def simple_bind_s(self, who='', cred='', serverctrls=None, clientctrls=None): - '''Not using use_conn_pool decorator here as this API takes cred as - input. - ''' + # Not using use_conn_pool decorator here as this API takes cred as + # input. self.who = who self.cred = cred with self._get_pool_connection() as conn: @@ -773,16 +772,17 @@ class PooledLDAPHandler(LDAPHandler): filterstr='(objectClass=*)', attrlist=None, attrsonly=0, serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0): - '''This API is asynchoronus API which returns MsgId instance to be used - in result3 call. + """Asynchronous API to return a ``MsgId`` instance. + + The ``MsgId`` instance can be safely used in a call to ``result3()``. - To work with result3 API in predicatable manner, same LDAP connection - is needed which provided msgid. So wrapping used connection and msgid - in MsgId class. The connection associated with search_ext is released - once last hard reference to MsgId object is freed. This will happen - when the method is done with returned MsgId usage. - ''' + To work with ``result3()`` API in predictable manner, the same LDAP + connection is needed which originally provided the ``msgid``. So, this + method wraps the existing connection and ``msgid`` in a new ``MsgId`` + instance. The connection associated with ``search_ext`` is released + once last hard reference to the ``MsgId`` instance is freed. + """ conn_ctxt = self._get_pool_connection() conn = conn_ctxt.__enter__() try: @@ -800,11 +800,12 @@ class PooledLDAPHandler(LDAPHandler): def result3(self, msgid, all=1, timeout=None, resp_ctrl_classes=None): - '''This method is used to wait for and return the result of an - operation previously initiated by one of the LDAP asynchronous - operation routines (eg search_ext()) It returned an invocation - identifier (a message id) upon successful initiation of their - operation. + """This method is used to wait for and return result. + + This method returns the result of an operation previously initiated by + one of the LDAP asynchronous operation routines (eg search_ext()). It + returned an invocation identifier (a message id) upon successful + initiation of their operation. Input msgid is expected to be instance of class MsgId which has LDAP session/connection used to execute search_ext and message idenfier. @@ -812,7 +813,8 @@ class PooledLDAPHandler(LDAPHandler): The connection associated with search_ext is released once last hard reference to MsgId object is freed. This will happen when function which requested msgId and used it in result3 exits. - ''' + + """ conn, msg_id = msgid return conn.result3(msg_id, all, timeout) @@ -831,7 +833,7 @@ class PooledLDAPHandler(LDAPHandler): class KeystoneLDAPHandler(LDAPHandler): - '''Convert data types and perform logging. + """Convert data types and perform logging. This LDAP inteface wraps the python-ldap based interfaces. The python-ldap interfaces require string values encoded in UTF-8. The @@ -854,7 +856,8 @@ class KeystoneLDAPHandler(LDAPHandler): Data returned from the LDAP call is converted back from UTF-8 encoded strings into the Python data type used internally in OpenStack. - ''' + + """ def __init__(self, conn=None): super(KeystoneLDAPHandler, self).__init__(conn=conn) @@ -938,7 +941,7 @@ class KeystoneLDAPHandler(LDAPHandler): if attrlist is None: attrlist_utf8 = None else: - attrlist_utf8 = map(utf8_encode, attrlist) + attrlist_utf8 = list(map(utf8_encode, attrlist)) ldap_result = self.conn.search_s(base_utf8, scope, filterstr_utf8, attrlist_utf8, attrsonly) @@ -989,7 +992,7 @@ class KeystoneLDAPHandler(LDAPHandler): attrlist_utf8 = None else: attrlist = [attr for attr in attrlist if attr is not None] - attrlist_utf8 = map(utf8_encode, attrlist) + attrlist_utf8 = list(map(utf8_encode, attrlist)) msgid = self.conn.search_ext(base_utf8, scope, filterstr_utf8, @@ -1083,7 +1086,7 @@ def register_handler(prefix, handler): def _get_connection(conn_url, use_pool=False, use_auth_pool=False): - for prefix, handler in six.iteritems(_HANDLERS): + for prefix, handler in _HANDLERS.items(): if conn_url.startswith(prefix): return handler() @@ -1109,7 +1112,6 @@ def filter_entity(entity_ref): class BaseLdap(object): - DEFAULT_SUFFIX = "dc=example,dc=com" DEFAULT_OU = None DEFAULT_STRUCTURAL_CLASSES = None DEFAULT_ID_ATTR = 'cn' @@ -1156,8 +1158,6 @@ class BaseLdap(object): if self.options_name is not None: self.suffix = conf.ldap.suffix - if self.suffix is None: - self.suffix = self.DEFAULT_SUFFIX dn = '%s_tree_dn' % self.options_name self.tree_dn = (getattr(conf.ldap, dn) or '%s,%s' % (self.DEFAULT_OU, self.suffix)) @@ -1169,7 +1169,7 @@ class BaseLdap(object): self.object_class = (getattr(conf.ldap, objclass) or self.DEFAULT_OBJECTCLASS) - for k, v in six.iteritems(self.attribute_options_names): + for k, v in self.attribute_options_names.items(): v = '%s_%s_attribute' % (self.options_name, v) self.attribute_mapping[k] = getattr(conf.ldap, v) @@ -1318,7 +1318,7 @@ class BaseLdap(object): # in a case-insensitive way. We use the case specified in the # mapping for the model to ensure we have a predictable way of # retrieving values later. - lower_res = {k.lower(): v for k, v in six.iteritems(res[1])} + lower_res = {k.lower(): v for k, v in res[1].items()} id_attrs = lower_res.get(self.id_attr.lower()) if not id_attrs: @@ -1404,7 +1404,7 @@ class BaseLdap(object): self.affirm_unique(values) object_classes = self.structural_classes + [self.object_class] attrs = [('objectClass', object_classes)] - for k, v in six.iteritems(values): + for k, v in values.items(): if k in self.attribute_ignore: continue if k == 'id': @@ -1416,7 +1416,7 @@ class BaseLdap(object): if attr_type is not None: attrs.append((attr_type, [v])) extra_attrs = [attr for attr, name - in six.iteritems(self.extra_attr_mapping) + in self.extra_attr_mapping.items() if name == k] for attr in extra_attrs: attrs.append((attr, [v])) @@ -1439,8 +1439,8 @@ class BaseLdap(object): with self.get_connection() as conn: try: attrs = list(set(([self.id_attr] + - self.attribute_mapping.values() + - self.extra_attr_mapping.keys()))) + list(self.attribute_mapping.values()) + + list(self.extra_attr_mapping.keys())))) res = conn.search_s(self.tree_dn, self.LDAP_SCOPE, query, @@ -1453,14 +1453,15 @@ class BaseLdap(object): return None def _ldap_get_all(self, ldap_filter=None): - query = u'(&%s(objectClass=%s))' % (ldap_filter or - self.ldap_filter or - '', self.object_class) + query = u'(&%s(objectClass=%s)(%s=*))' % ( + ldap_filter or self.ldap_filter or '', + self.object_class, + self.id_attr) with self.get_connection() as conn: try: attrs = list(set(([self.id_attr] + - self.attribute_mapping.values() + - self.extra_attr_mapping.keys()))) + list(self.attribute_mapping.values()) + + list(self.extra_attr_mapping.keys())))) return conn.search_s(self.tree_dn, self.LDAP_SCOPE, query, @@ -1479,7 +1480,7 @@ class BaseLdap(object): query = (u'(&%s%s)' % (query, ''.join([calc_filter(k, v) for k, v in - six.iteritems(query_params)]))) + query_params.items()]))) with self.get_connection() as conn: return conn.search_s(search_base, scope, query, attrlist) @@ -1509,7 +1510,7 @@ class BaseLdap(object): old_obj = self.get(object_id) modlist = [] - for k, v in six.iteritems(values): + for k, v in values.items(): if k == 'id': # id can't be modified. continue @@ -1648,7 +1649,7 @@ class BaseLdap(object): (query, ''.join(['(%s=%s)' % (k, ldap.filter.escape_filter_chars(v)) for k, v in - six.iteritems(query_params)]))) + query_params.items()]))) not_deleted_nodes = [] with self.get_connection() as conn: try: @@ -1738,6 +1739,11 @@ class BaseLdap(object): return query_term + if query is None: + # make sure query is a string so the ldap filter is properly + # constructed from filter_list later + query = '' + if hints is None: return query @@ -1799,25 +1805,24 @@ class EnabledEmuMixIn(BaseLdap): utf8_decode(naming_rdn[1])) self.enabled_emulation_naming_attr = naming_attr - def _get_enabled(self, object_id): + def _get_enabled(self, object_id, conn): dn = self._id_to_dn(object_id) query = '(member=%s)' % dn - with self.get_connection() as conn: - try: - enabled_value = conn.search_s(self.enabled_emulation_dn, - ldap.SCOPE_BASE, - query, ['cn']) - except ldap.NO_SUCH_OBJECT: - return False - else: - return bool(enabled_value) + try: + enabled_value = conn.search_s(self.enabled_emulation_dn, + ldap.SCOPE_BASE, + query, attrlist=DN_ONLY) + except ldap.NO_SUCH_OBJECT: + return False + else: + return bool(enabled_value) def _add_enabled(self, object_id): - if not self._get_enabled(object_id): - modlist = [(ldap.MOD_ADD, - 'member', - [self._id_to_dn(object_id)])] - with self.get_connection() as conn: + with self.get_connection() as conn: + if not self._get_enabled(object_id, conn): + modlist = [(ldap.MOD_ADD, + 'member', + [self._id_to_dn(object_id)])] try: conn.modify_s(self.enabled_emulation_dn, modlist) except ldap.NO_SUCH_OBJECT: @@ -1851,10 +1856,12 @@ class EnabledEmuMixIn(BaseLdap): return super(EnabledEmuMixIn, self).create(values) def get(self, object_id, ldap_filter=None): - ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter) - if 'enabled' not in self.attribute_ignore and self.enabled_emulation: - ref['enabled'] = self._get_enabled(object_id) - return ref + with self.get_connection() as conn: + ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter) + if ('enabled' not in self.attribute_ignore and + self.enabled_emulation): + ref['enabled'] = self._get_enabled(object_id, conn) + return ref def get_all(self, ldap_filter=None): if 'enabled' not in self.attribute_ignore and self.enabled_emulation: @@ -1862,8 +1869,10 @@ class EnabledEmuMixIn(BaseLdap): tenant_list = [self._ldap_res_to_model(x) for x in self._ldap_get_all(ldap_filter) if x[0] != self.enabled_emulation_dn] - for tenant_ref in tenant_list: - tenant_ref['enabled'] = self._get_enabled(tenant_ref['id']) + with self.get_connection() as conn: + for tenant_ref in tenant_list: + tenant_ref['enabled'] = self._get_enabled( + tenant_ref['id'], conn) return tenant_list else: return super(EnabledEmuMixIn, self).get_all(ldap_filter) diff --git a/keystone-moon/keystone/common/manager.py b/keystone-moon/keystone/common/manager.py index 28bf2efb..7150fbf3 100644 --- a/keystone-moon/keystone/common/manager.py +++ b/keystone-moon/keystone/common/manager.py @@ -14,7 +14,13 @@ import functools +from oslo_log import log +from oslo_log import versionutils from oslo_utils import importutils +import stevedore + + +LOG = log.getLogger(__name__) def response_truncated(f): @@ -53,6 +59,28 @@ def response_truncated(f): return wrapper +def load_driver(namespace, driver_name, *args): + try: + driver_manager = stevedore.DriverManager(namespace, + driver_name, + invoke_on_load=True, + invoke_args=args) + return driver_manager.driver + except RuntimeError as e: + LOG.debug('Failed to load %r using stevedore: %s', driver_name, e) + # Ignore failure and continue on. + + @versionutils.deprecated(as_of=versionutils.deprecated.LIBERTY, + in_favor_of='entrypoints', + what='direct import of driver') + def _load_using_import(driver_name, *args): + return importutils.import_object(driver_name, *args) + + # For backwards-compatibility, an unregistered class reference can + # still be used. + return _load_using_import(driver_name, *args) + + class Manager(object): """Base class for intermediary request layer. @@ -66,8 +94,10 @@ class Manager(object): """ + driver_namespace = None + def __init__(self, driver_name): - self.driver = importutils.import_object(driver_name) + self.driver = load_driver(self.driver_namespace, driver_name) def __getattr__(self, name): """Forward calls to the underlying driver.""" diff --git a/keystone-moon/keystone/common/models.py b/keystone-moon/keystone/common/models.py index 3b3aabe1..0bb37319 100644 --- a/keystone-moon/keystone/common/models.py +++ b/keystone-moon/keystone/common/models.py @@ -130,11 +130,12 @@ class Project(Model): Optional Keys: description enabled (bool, default True) + is_domain (bool, default False) """ required_keys = ('id', 'name', 'domain_id') - optional_keys = ('description', 'enabled') + optional_keys = ('description', 'enabled', 'is_domain') class Role(Model): diff --git a/keystone-moon/keystone/common/openssl.py b/keystone-moon/keystone/common/openssl.py index 4eb7d1d1..be56b9cc 100644 --- a/keystone-moon/keystone/common/openssl.py +++ b/keystone-moon/keystone/common/openssl.py @@ -20,7 +20,7 @@ from oslo_log import log from keystone.common import environment from keystone.common import utils -from keystone.i18n import _LI, _LE +from keystone.i18n import _LI, _LE, _LW LOG = log.getLogger(__name__) CONF = cfg.CONF @@ -70,8 +70,8 @@ class BaseCertificateConfigure(object): if "OpenSSL 0." in openssl_ver: self.ssl_dictionary['default_md'] = 'sha1' except OSError: - LOG.warn('Failed to invoke ``openssl version``, ' - 'assuming is v1.0 or newer') + LOG.warn(_LW('Failed to invoke ``openssl version``, ' + 'assuming is v1.0 or newer')) self.ssl_dictionary.update(kwargs) def exec_command(self, command): diff --git a/keystone-moon/keystone/common/sql/core.py b/keystone-moon/keystone/common/sql/core.py index bf168701..ebd61bb7 100644 --- a/keystone-moon/keystone/common/sql/core.py +++ b/keystone-moon/keystone/common/sql/core.py @@ -239,6 +239,39 @@ def truncated(f): return wrapper +class _WontMatch(Exception): + """Raised to indicate that the filter won't match. + + This is raised to short-circuit the computation of the filter as soon as + it's discovered that the filter requested isn't going to match anything. + + A filter isn't going to match anything if the value is too long for the + field, for example. + + """ + + @classmethod + def check(cls, value, col_attr): + """Check if the value can match given the column attributes. + + Raises this class if the value provided can't match any value in the + column in the table given the column's attributes. For example, if the + column is a string and the value is longer than the column then it + won't match any value in the column in the table. + + """ + col = col_attr.property.columns[0] + if isinstance(col.type, sql.types.Boolean): + # The column is a Boolean, we should have already validated input. + return + if not col.type.length: + # The column doesn't have a length so can't validate anymore. + return + if len(value) > col.type.length: + raise cls() + # Otherwise the value could match a value in the column. + + def _filter(model, query, hints): """Applies filtering to a query. @@ -251,16 +284,14 @@ def _filter(model, query, hints): :returns query: query, updated with any filters satisfied """ - def inexact_filter(model, query, filter_, satisfied_filters, hints): + def inexact_filter(model, query, filter_, satisfied_filters): """Applies an inexact filter to a query. :param model: the table model in question :param query: query to apply filters to - :param filter_: the dict that describes this filter - :param satisfied_filters: a cumulative list of satisfied filters, to - which filter_ will be added if it is - satisfied. - :param hints: contains the list of filters yet to be satisfied. + :param dict filter_: describes this filter + :param list satisfied_filters: filter_ will be added if it is + satisfied. :returns query: query updated to add any inexact filters we could satisfy @@ -278,10 +309,13 @@ def _filter(model, query, hints): return query if filter_['comparator'] == 'contains': + _WontMatch.check(filter_['value'], column_attr) query_term = column_attr.ilike('%%%s%%' % filter_['value']) elif filter_['comparator'] == 'startswith': + _WontMatch.check(filter_['value'], column_attr) query_term = column_attr.ilike('%s%%' % filter_['value']) elif filter_['comparator'] == 'endswith': + _WontMatch.check(filter_['value'], column_attr) query_term = column_attr.ilike('%%%s' % filter_['value']) else: # It's a filter we don't understand, so let the caller @@ -291,53 +325,50 @@ def _filter(model, query, hints): satisfied_filters.append(filter_) return query.filter(query_term) - def exact_filter( - model, filter_, satisfied_filters, cumulative_filter_dict, hints): + def exact_filter(model, filter_, cumulative_filter_dict): """Applies an exact filter to a query. :param model: the table model in question - :param filter_: the dict that describes this filter - :param satisfied_filters: a cumulative list of satisfied filters, to - which filter_ will be added if it is - satisfied. - :param cumulative_filter_dict: a dict that describes the set of - exact filters built up so far - :param hints: contains the list of filters yet to be satisfied. - - :returns: updated cumulative dict + :param dict filter_: describes this filter + :param dict cumulative_filter_dict: describes the set of exact filters + built up so far """ key = filter_['name'] - if isinstance(getattr(model, key).property.columns[0].type, - sql.types.Boolean): + + col = getattr(model, key) + if isinstance(col.property.columns[0].type, sql.types.Boolean): cumulative_filter_dict[key] = ( utils.attr_as_boolean(filter_['value'])) else: + _WontMatch.check(filter_['value'], col) cumulative_filter_dict[key] = filter_['value'] - satisfied_filters.append(filter_) - return cumulative_filter_dict - - filter_dict = {} - satisfied_filters = [] - for filter_ in hints.filters: - if filter_['name'] not in model.attributes: - continue - if filter_['comparator'] == 'equals': - filter_dict = exact_filter( - model, filter_, satisfied_filters, filter_dict, hints) - else: - query = inexact_filter( - model, query, filter_, satisfied_filters, hints) - # Apply any exact filters we built up - if filter_dict: - query = query.filter_by(**filter_dict) + try: + filter_dict = {} + satisfied_filters = [] + for filter_ in hints.filters: + if filter_['name'] not in model.attributes: + continue + if filter_['comparator'] == 'equals': + exact_filter(model, filter_, filter_dict) + satisfied_filters.append(filter_) + else: + query = inexact_filter(model, query, filter_, + satisfied_filters) + + # Apply any exact filters we built up + if filter_dict: + query = query.filter_by(**filter_dict) + + # Remove satisfied filters, then the caller will know remaining filters + for filter_ in satisfied_filters: + hints.filters.remove(filter_) - # Remove satisfied filters, then the caller will know remaining filters - for filter_ in satisfied_filters: - hints.filters.remove(filter_) - - return query + return query + except _WontMatch: + hints.cannot_match = True + return def _limit(query, hints): @@ -378,6 +409,10 @@ def filter_limit_query(model, query, hints): # First try and satisfy any filters query = _filter(model, query, hints) + if hints.cannot_match: + # Nothing's going to match, so don't bother with the query. + return [] + # NOTE(henry-nash): Any unsatisfied filters will have been left in # the hints list for the controller to handle. We can only try and # limit here if all the filters are already satisfied since, if not, diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py index b6f40719..2a98fb90 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py @@ -19,7 +19,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py index b6f40719..2a98fb90 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py @@ -19,7 +19,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py index b6f40719..2a98fb90 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py @@ -19,7 +19,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py index b6f40719..2a98fb90 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py @@ -19,7 +19,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py index b6f40719..2a98fb90 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py @@ -19,7 +19,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py index 535a0944..c4b41580 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py @@ -27,7 +27,8 @@ def upgrade(migrate_engine): # names, depending on version of MySQL used. We shoud make this naming # consistent, by reverting index name to a consistent condition. if any(i for i in endpoint.indexes if - i.columns.keys() == ['service_id'] and i.name != 'service_id'): + list(i.columns.keys()) == ['service_id'] + and i.name != 'service_id'): # NOTE(i159): by this action will be made re-creation of an index # with the new name. This can be considered as renaming under the # MySQL rules. @@ -37,13 +38,6 @@ def upgrade(migrate_engine): meta, autoload=True) if any(i for i in user_group_membership.indexes if - i.columns.keys() == ['group_id'] and i.name != 'group_id'): + list(i.columns.keys()) == ['group_id'] + and i.name != 'group_id'): sa.Index('group_id', user_group_membership.c.group_id).create() - - -def downgrade(migrate_engine): - # NOTE(i159): index exists only in MySQL schemas, and got an inconsistent - # name only when MySQL 5.5 renamed it after re-creation - # (during migrations). So we just fixed inconsistency, there is no - # necessity to revert it. - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py index 074fbb63..59720f6e 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py @@ -39,11 +39,3 @@ def upgrade(migrate_engine): mysql_engine='InnoDB', mysql_charset='utf8') mapping_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - assignment = sql.Table(MAPPING_TABLE, meta, autoload=True) - assignment.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py index 9f1fd9f0..86302a8f 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py @@ -14,6 +14,7 @@ import sqlalchemy as sql + _REGION_TABLE_NAME = 'region' @@ -24,11 +25,3 @@ def upgrade(migrate_engine): region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) url_column = sql.Column('url', sql.String(255), nullable=True) region_table.create_column(url_column) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) - region_table.drop_column('url') diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py index 6dc0004f..c2be48f4 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. - """Migrated the endpoint 'region' column to 'region_id. In addition to the rename, the new column is made a foreign key to the @@ -36,25 +35,9 @@ b. For each endpoint ii. Assign the id to the region_id column c. Remove the column region - -To Downgrade: - -Endpoint Table - -a. Add back in the region column -b. For each endpoint - i. Copy the region_id column to the region column -c. Remove the column region_id - -Region Table - -Decrease the size of the id column in the region table, making sure that -we don't get classing primary keys. - """ import migrate -import six import sqlalchemy as sql from sqlalchemy.orm import sessionmaker @@ -90,39 +73,6 @@ def _migrate_to_region_id(migrate_engine, region_table, endpoint_table): name='fk_endpoint_region_id').create() -def _migrate_to_region(migrate_engine, region_table, endpoint_table): - endpoints = list(endpoint_table.select().execute()) - - for endpoint in endpoints: - new_values = {'region': endpoint.region_id} - f = endpoint_table.c.id == endpoint.id - update = endpoint_table.update().where(f).values(new_values) - migrate_engine.execute(update) - - if 'sqlite' != migrate_engine.name: - migrate.ForeignKeyConstraint( - columns=[endpoint_table.c.region_id], - refcolumns=[region_table.c.id], - name='fk_endpoint_region_id').drop() - endpoint_table.c.region_id.drop() - - -def _prepare_regions_for_id_truncation(migrate_engine, region_table): - """Ensure there are no IDs that are bigger than 64 chars. - - The size of the id and parent_id fields where increased from 64 to 255 - during the upgrade. On downgrade we have to make sure that the ids can - fit in the new column size. For rows with ids greater than this, we have - no choice but to dump them. - - """ - for region in list(region_table.select().execute()): - if (len(six.text_type(region.id)) > 64 or - len(six.text_type(region.parent_region_id)) > 64): - delete = region_table.delete(region_table.c.id == region.id) - migrate_engine.execute(delete) - - def upgrade(migrate_engine): meta = sql.MetaData() meta.bind = migrate_engine @@ -138,19 +88,3 @@ def upgrade(migrate_engine): _migrate_to_region_id(migrate_engine, region_table, endpoint_table) endpoint_table.c.region.drop() - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - region_table = sql.Table('region', meta, autoload=True) - endpoint_table = sql.Table('endpoint', meta, autoload=True) - region_column = sql.Column('region', sql.String(length=255)) - region_column.create(endpoint_table) - - _migrate_to_region(migrate_engine, region_table, endpoint_table) - _prepare_regions_for_id_truncation(migrate_engine, region_table) - - region_table.c.id.alter(type=sql.String(length=64)) - region_table.c.parent_region_id.alter(type=sql.String(length=64)) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py index 33b13b7d..caf4d66f 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py @@ -14,6 +14,7 @@ import sqlalchemy as sql + ASSIGNMENT_TABLE = 'assignment' @@ -24,12 +25,3 @@ def upgrade(migrate_engine): assignment = sql.Table(ASSIGNMENT_TABLE, meta, autoload=True) idx = sql.Index('ix_actor_id', assignment.c.actor_id) idx.create(migrate_engine) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - assignment = sql.Table(ASSIGNMENT_TABLE, meta, autoload=True) - idx = sql.Index('ix_actor_id', assignment.c.actor_id) - idx.drop(migrate_engine) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py index 1cfddd3f..a7f327ea 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py @@ -23,13 +23,3 @@ def upgrade(migrate_engine): sql.Index('ix_token_user_id', token.c.user_id).create() sql.Index('ix_token_trust_id', token.c.trust_id).create() - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - token = sql.Table('token', meta, autoload=True) - - sql.Index('ix_token_user_id', token.c.user_id).drop() - sql.Index('ix_token_trust_id', token.c.trust_id).drop() diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py index 5f82254f..8bb40490 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py @@ -16,7 +16,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py index 5f82254f..8bb40490 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py @@ -16,7 +16,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py index 5f82254f..8bb40490 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py @@ -16,7 +16,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py index 5f82254f..8bb40490 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py @@ -16,7 +16,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py index 5f82254f..8bb40490 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py @@ -16,7 +16,3 @@ def upgrade(migrate_engine): pass - - -def downgrade(migration_engine): - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py index bb8ef9f6..ca9b3ce2 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py @@ -14,6 +14,7 @@ import sqlalchemy as sql from keystone.common.sql import migration_helpers + _PROJECT_TABLE_NAME = 'project' _PARENT_ID_COLUMN_NAME = 'parent_id' @@ -38,17 +39,3 @@ def upgrade(migrate_engine): if migrate_engine.name == 'sqlite': return migration_helpers.add_constraints(list_constraints(project_table)) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - project_table = sql.Table(_PROJECT_TABLE_NAME, meta, autoload=True) - - # SQLite does not support constraints, and querying the constraints - # raises an exception - if migrate_engine.name != 'sqlite': - migration_helpers.remove_constraints(list_constraints(project_table)) - - project_table.drop_column(_PARENT_ID_COLUMN_NAME) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py index 5a33486c..f7a69bb6 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py @@ -33,9 +33,3 @@ def upgrade(migrate_engine): if migrate_engine.name == 'sqlite': return migration_helpers.remove_constraints(list_constraints(migrate_engine)) - - -def downgrade(migrate_engine): - if migrate_engine.name == 'sqlite': - return - migration_helpers.add_constraints(list_constraints(migrate_engine)) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py index 109a8412..e45133ab 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py @@ -12,6 +12,7 @@ import sqlalchemy as sql + _REGION_TABLE_NAME = 'region' @@ -21,12 +22,3 @@ def upgrade(migrate_engine): region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) region_table.drop_column('url') - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) - url_column = sql.Column('url', sql.String(255), nullable=True) - region_table.create_column(url_column) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py index bca00902..637f2151 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py @@ -37,9 +37,3 @@ def upgrade(migrate_engine): if migrate_engine.name == 'sqlite': return migration_helpers.remove_constraints(list_constraints(migrate_engine)) - - -def downgrade(migrate_engine): - if migrate_engine.name == 'sqlite': - return - migration_helpers.add_constraints(list_constraints(migrate_engine)) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py index fd8717d2..63a86c11 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py @@ -14,6 +14,7 @@ import sqlalchemy as sql from keystone.common import sql as ks_sql + WHITELIST_TABLE = 'whitelisted_config' SENSITIVE_TABLE = 'sensitive_config' @@ -43,13 +44,3 @@ def upgrade(migrate_engine): mysql_engine='InnoDB', mysql_charset='utf8') sensitive_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - table = sql.Table(WHITELIST_TABLE, meta, autoload=True) - table.drop(migrate_engine, checkfirst=True) - table = sql.Table(SENSITIVE_TABLE, meta, autoload=True) - table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py index 3feadc53..fe0cee88 100644 --- a/keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py @@ -22,7 +22,11 @@ def upgrade(migrate_engine): services = list(service_table.select().execute()) for service in services: - extra_dict = jsonutils.loads(service.extra) + if service.extra is not None: + extra_dict = jsonutils.loads(service.extra) + else: + extra_dict = {} + # Skip records where service is not null if extra_dict.get('name') is not None: continue @@ -34,10 +38,3 @@ def upgrade(migrate_engine): f = service_table.c.id == service.id update = service_table.update().where(f).values(new_values) migrate_engine.execute(update) - - -def downgrade(migration_engine): - # The upgrade fixes the data inconsistency for the service name, - # it defaults the value to empty string. There is no necessity - # to revert it. - pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/067_drop_redundant_mysql_index.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/067_drop_redundant_mysql_index.py new file mode 100644 index 00000000..b9df1a55 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/067_drop_redundant_mysql_index.py @@ -0,0 +1,25 @@ +# 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 sqlalchemy + + +def upgrade(migrate_engine): + # NOTE(viktors): Migration 062 removed FK from `assignment` table, but + # MySQL silently creates indexes on FK constraints, so we should remove + # this index manually. + if migrate_engine.name == 'mysql': + meta = sqlalchemy.MetaData(bind=migrate_engine) + table = sqlalchemy.Table('assignment', meta, autoload=True) + for index in table.indexes: + if [c.name for c in index.columns] == ['role_id']: + index.drop(migrate_engine) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/068_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/068_placeholder.py new file mode 100644 index 00000000..111df9d4 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/068_placeholder.py @@ -0,0 +1,18 @@ +# 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. + +# This is a placeholder for Kilo backports. Do not use this number for new +# Liberty work. New Liberty work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/069_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/069_placeholder.py new file mode 100644 index 00000000..111df9d4 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/069_placeholder.py @@ -0,0 +1,18 @@ +# 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. + +# This is a placeholder for Kilo backports. Do not use this number for new +# Liberty work. New Liberty work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/070_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/070_placeholder.py new file mode 100644 index 00000000..111df9d4 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/070_placeholder.py @@ -0,0 +1,18 @@ +# 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. + +# This is a placeholder for Kilo backports. Do not use this number for new +# Liberty work. New Liberty work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/071_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/071_placeholder.py new file mode 100644 index 00000000..111df9d4 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/071_placeholder.py @@ -0,0 +1,18 @@ +# 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. + +# This is a placeholder for Kilo backports. Do not use this number for new +# Liberty work. New Liberty work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/072_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/072_placeholder.py new file mode 100644 index 00000000..111df9d4 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/072_placeholder.py @@ -0,0 +1,18 @@ +# 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. + +# This is a placeholder for Kilo backports. Do not use this number for new +# Liberty work. New Liberty work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/073_insert_assignment_inherited_pk.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/073_insert_assignment_inherited_pk.py new file mode 100644 index 00000000..ffa210c4 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/073_insert_assignment_inherited_pk.py @@ -0,0 +1,114 @@ +# 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 +from sqlalchemy.orm import sessionmaker + +from keystone.assignment.backends import sql as assignment_sql + + +def upgrade(migrate_engine): + """Inserts inherited column to assignment table PK contraints. + + For non-SQLite databases, it changes the constraint in the existing table. + + For SQLite, since changing constraints is not supported, it recreates the + assignment table with the new PK constraint and migrates the existing data. + + """ + + ASSIGNMENT_TABLE_NAME = 'assignment' + + metadata = sql.MetaData() + metadata.bind = migrate_engine + + # Retrieve the existing assignment table + assignment_table = sql.Table(ASSIGNMENT_TABLE_NAME, metadata, + autoload=True) + + if migrate_engine.name == 'sqlite': + ACTOR_ID_INDEX_NAME = 'ix_actor_id' + TMP_ASSIGNMENT_TABLE_NAME = 'tmp_assignment' + + # Define the new assignment table with a temporary name + new_assignment_table = sql.Table( + TMP_ASSIGNMENT_TABLE_NAME, metadata, + sql.Column('type', sql.Enum( + assignment_sql.AssignmentType.USER_PROJECT, + assignment_sql.AssignmentType.GROUP_PROJECT, + assignment_sql.AssignmentType.USER_DOMAIN, + assignment_sql.AssignmentType.GROUP_DOMAIN, + name='type'), + nullable=False), + sql.Column('actor_id', sql.String(64), nullable=False), + sql.Column('target_id', sql.String(64), nullable=False), + sql.Column('role_id', sql.String(64), sql.ForeignKey('role.id'), + nullable=False), + sql.Column('inherited', sql.Boolean, default=False, + nullable=False), + sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', + 'role_id', 'inherited'), + mysql_engine='InnoDB', + mysql_charset='utf8') + + # Create the new assignment table + new_assignment_table.create(migrate_engine, checkfirst=True) + + # Change the index from the existing assignment table to the new one + sql.Index(ACTOR_ID_INDEX_NAME, assignment_table.c.actor_id).drop() + sql.Index(ACTOR_ID_INDEX_NAME, + new_assignment_table.c.actor_id).create() + + # Instantiate session + maker = sessionmaker(bind=migrate_engine) + session = maker() + + # Migrate existing data + insert = new_assignment_table.insert().from_select( + assignment_table.c, select=session.query(assignment_table)) + session.execute(insert) + session.commit() + + # Drop the existing assignment table, in favor of the new one + assignment_table.deregister() + assignment_table.drop() + + # Finally, rename the new table to the original assignment table name + new_assignment_table.rename(ASSIGNMENT_TABLE_NAME) + elif migrate_engine.name == 'ibm_db_sa': + # Recreate the existing constraint, marking the inherited column as PK + # for DB2. + + # This is a workaround to the general case in the else statement below. + # Due to a bug in the DB2 sqlalchemy dialect, Column.alter() actually + # creates a primary key over only the "inherited" column. This is wrong + # because the primary key for the table actually covers other columns + # too, not just the "inherited" column. Since the primary key already + # exists for the table after the Column.alter() call, it causes the + # next line to fail with an error that the primary key already exists. + + # The workaround here skips doing the Column.alter(). This causes a + # warning message since the metadata is out of sync. We can remove this + # workaround once the DB2 sqlalchemy dialect is fixed. + # DB2 Issue: https://code.google.com/p/ibm-db/issues/detail?id=173 + + migrate.PrimaryKeyConstraint(table=assignment_table).drop() + migrate.PrimaryKeyConstraint( + assignment_table.c.type, assignment_table.c.actor_id, + assignment_table.c.target_id, assignment_table.c.role_id, + assignment_table.c.inherited).create() + else: + # Recreate the existing constraint, marking the inherited column as PK + migrate.PrimaryKeyConstraint(table=assignment_table).drop() + assignment_table.c.inherited.alter(primary_key=True) + migrate.PrimaryKeyConstraint(table=assignment_table).create() diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/074_add_is_domain_project.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/074_add_is_domain_project.py new file mode 100644 index 00000000..dcb89b07 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/074_add_is_domain_project.py @@ -0,0 +1,27 @@ +# 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 sqlalchemy as sql + + +_PROJECT_TABLE_NAME = 'project' +_IS_DOMAIN_COLUMN_NAME = 'is_domain' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + project_table = sql.Table(_PROJECT_TABLE_NAME, meta, autoload=True) + is_domain = sql.Column(_IS_DOMAIN_COLUMN_NAME, sql.Boolean, nullable=False, + server_default='0', default=False) + project_table.create_column(is_domain) diff --git a/keystone-moon/keystone/common/sql/migration_helpers.py b/keystone-moon/keystone/common/sql/migration_helpers.py index 86932995..aaa59f70 100644 --- a/keystone-moon/keystone/common/sql/migration_helpers.py +++ b/keystone-moon/keystone/common/sql/migration_helpers.py @@ -143,37 +143,21 @@ def _sync_common_repo(version): abs_path = find_migrate_repo() init_version = migrate_repo.DB_INIT_VERSION engine = sql.get_engine() + _assert_not_schema_downgrade(version=version) migration.db_sync(engine, abs_path, version=version, - init_version=init_version) + init_version=init_version, sanity_check=False) -def _fix_federation_tables(engine): - """Fix the identity_provider, federation_protocol and mapping tables - to be InnoDB and Charset UTF8. - - This function is to work around bug #1426334. This has occurred because - the original migration did not specify InnoDB and charset utf8. Due - to the sanity_check, a deployer can get wedged here and require manual - database changes to fix. - """ - # NOTE(marco-fargetta) This is a workaround to "fix" that tables only - # if we're under MySQL - if engine.name == 'mysql': - # * Disable any check for the foreign keys because they prevent the - # alter table to execute - engine.execute("SET foreign_key_checks = 0") - # * Make the tables using InnoDB engine - engine.execute("ALTER TABLE identity_provider Engine=InnoDB") - engine.execute("ALTER TABLE federation_protocol Engine=InnoDB") - engine.execute("ALTER TABLE mapping Engine=InnoDB") - # * Make the tables using utf8 encoding - engine.execute("ALTER TABLE identity_provider " - "CONVERT TO CHARACTER SET utf8") - engine.execute("ALTER TABLE federation_protocol " - "CONVERT TO CHARACTER SET utf8") - engine.execute("ALTER TABLE mapping CONVERT TO CHARACTER SET utf8") - # * Revert the foreign keys check back - engine.execute("SET foreign_key_checks = 1") +def _assert_not_schema_downgrade(extension=None, version=None): + if version is not None: + try: + current_ver = int(six.text_type(get_db_version(extension))) + if int(version) < current_ver: + raise migration.exception.DbMigrationError() + except exceptions.DatabaseNotControlledError: + # NOTE(morganfainberg): The database is not controlled, this action + # cannot be a downgrade. + pass def _sync_extension_repo(extension, version): @@ -198,27 +182,11 @@ def _sync_extension_repo(extension, version): except exception.MigrationNotProvided as e: print(e) sys.exit(1) - try: - migration.db_sync(engine, abs_path, version=version, - init_version=init_version) - except ValueError: - # NOTE(marco-fargetta): ValueError is raised from the sanity check ( - # verifies that tables are utf8 under mysql). The federation_protocol, - # identity_provider and mapping tables were not initially built with - # InnoDB and utf8 as part of the table arguments when the migration - # was initially created. Bug #1426334 is a scenario where the deployer - # can get wedged, unable to upgrade or downgrade. - # This is a workaround to "fix" those tables if we're under MySQL and - # the version is before the 6 because before the tables were introduced - # before and patched when migration 5 was available - if engine.name == 'mysql' and \ - int(six.text_type(get_db_version(extension))) < 6: - _fix_federation_tables(engine) - # The migration is applied again after the fix - migration.db_sync(engine, abs_path, version=version, - init_version=init_version) - else: - raise + + _assert_not_schema_downgrade(extension=extension, version=version) + + migration.db_sync(engine, abs_path, version=version, + init_version=init_version, sanity_check=False) def sync_database_to_version(extension=None, version=None): diff --git a/keystone-moon/keystone/common/utils.py b/keystone-moon/keystone/common/utils.py index a4b03ffd..48336af7 100644 --- a/keystone-moon/keystone/common/utils.py +++ b/keystone-moon/keystone/common/utils.py @@ -27,10 +27,12 @@ from oslo_config import cfg from oslo_log import log from oslo_serialization import jsonutils from oslo_utils import strutils +from oslo_utils import timeutils import passlib.hash import six from six import moves +from keystone.common import authorization from keystone import exception from keystone.i18n import _, _LE, _LW @@ -51,7 +53,7 @@ def flatten_dict(d, parent_key=''): for k, v in d.items(): new_key = parent_key + '.' + k if parent_key else k if isinstance(v, collections.MutableMapping): - items.extend(flatten_dict(v, new_key).items()) + items.extend(list(flatten_dict(v, new_key).items())) else: items.append((new_key, v)) return dict(items) @@ -244,7 +246,7 @@ def setup_remote_pydev_debug(): def get_unix_user(user=None): - '''Get the uid and user name. + """Get the uid and user name. This is a convenience utility which accepts a variety of input which might represent a unix user. If successful it returns the uid @@ -257,7 +259,7 @@ def get_unix_user(user=None): lookup as a uid. int - An integer is interpretted as a uid. + An integer is interpreted as a uid. None None is interpreted to mean use the current process's @@ -270,7 +272,8 @@ def get_unix_user(user=None): lookup. :return: tuple of (uid, name) - ''' + + """ if isinstance(user, six.string_types): try: @@ -299,7 +302,7 @@ def get_unix_user(user=None): def get_unix_group(group=None): - '''Get the gid and group name. + """Get the gid and group name. This is a convenience utility which accepts a variety of input which might represent a unix group. If successful it returns the gid @@ -312,7 +315,7 @@ def get_unix_group(group=None): lookup as a gid. int - An integer is interpretted as a gid. + An integer is interpreted as a gid. None None is interpreted to mean use the current process's @@ -326,7 +329,8 @@ def get_unix_group(group=None): lookup. :return: tuple of (gid, name) - ''' + + """ if isinstance(group, six.string_types): try: @@ -357,7 +361,7 @@ def get_unix_group(group=None): def set_permissions(path, mode=None, user=None, group=None, log=None): - '''Set the ownership and permissions on the pathname. + """Set the ownership and permissions on the pathname. Each of the mode, user and group are optional, if None then that aspect is not modified. @@ -374,7 +378,8 @@ def set_permissions(path, mode=None, user=None, group=None, log=None): if None do not set. :param logger log: logging.logger object, used to emit log messages, if None no logging is performed. - ''' + + """ if user is None: user_uid, user_name = None, None @@ -420,7 +425,7 @@ def set_permissions(path, mode=None, user=None, group=None, log=None): def make_dirs(path, mode=None, user=None, group=None, log=None): - '''Assure directory exists, set ownership and permissions. + """Assure directory exists, set ownership and permissions. Assure the directory exists and optionally set its ownership and permissions. @@ -440,7 +445,8 @@ def make_dirs(path, mode=None, user=None, group=None, log=None): if None do not set. :param logger log: logging.logger object, used to emit log messages, if None no logging is performed. - ''' + + """ if log: if mode is None: @@ -469,3 +475,54 @@ class WhiteListedItemFilter(object): if name not in self._whitelist: raise KeyError return self._data[name] + + +_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' +_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' + + +def isotime(at=None, subsecond=False): + """Stringify time in ISO 8601 format.""" + + # Python provides a similar instance method for datetime.datetime objects + # called isoformat(). The format of the strings generated by isoformat() + # have a couple of problems: + # 1) The strings generated by isotime are used in tokens and other public + # APIs that we can't change without a deprecation period. The strings + # generated by isoformat are not the same format, so we can't just + # change to it. + # 2) The strings generated by isoformat do not include the microseconds if + # the value happens to be 0. This will likely show up as random failures + # as parsers may be written to always expect microseconds, and it will + # parse correctly most of the time. + + if not at: + at = timeutils.utcnow() + st = at.strftime(_ISO8601_TIME_FORMAT + if not subsecond + else _ISO8601_TIME_FORMAT_SUBSECOND) + tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' + st += ('Z' if tz == 'UTC' else tz) + return st + + +def strtime(): + at = timeutils.utcnow() + return at.strftime(timeutils.PERFECT_TIME_FORMAT) + + +def get_token_ref(context): + """Retrieves KeystoneToken object from the auth context and returns it. + + :param dict context: The request context. + :raises: exception.Unauthorized if auth context cannot be found. + :returns: The KeystoneToken object. + """ + try: + # Retrieve the auth context that was prepared by AuthContextMiddleware. + auth_context = (context['environment'] + [authorization.AUTH_CONTEXT_ENV]) + return auth_context['token'] + except KeyError: + LOG.warning(_LW("Couldn't find the auth context.")) + raise exception.Unauthorized() diff --git a/keystone-moon/keystone/common/validation/__init__.py b/keystone-moon/keystone/common/validation/__init__.py index f9c58eaf..1e5cc6a5 100644 --- a/keystone-moon/keystone/common/validation/__init__.py +++ b/keystone-moon/keystone/common/validation/__init__.py @@ -12,8 +12,11 @@ """Request body validating middleware for OpenStack Identity resources.""" import functools +import inspect from keystone.common.validation import validators +from keystone import exception +from keystone.i18n import _ def validated(request_body_schema, resource_to_validate): @@ -24,15 +27,47 @@ def validated(request_body_schema, resource_to_validate): :param request_body_schema: a schema to validate the resource reference :param resource_to_validate: the reference to validate + :raises keystone.exception.ValidationError: if `resource_to_validate` is + not passed by or passed with an empty value (see wrapper method + below). + :raises TypeError: at decoration time when the expected resource to + validate isn't found in the decorated method's + signature """ schema_validator = validators.SchemaValidator(request_body_schema) def add_validator(func): + argspec = inspect.getargspec(func) + try: + arg_index = argspec.args.index(resource_to_validate) + except ValueError: + raise TypeError(_('validated expected to find %(param_name)r in ' + 'function signature for %(func_name)r.') % + {'param_name': resource_to_validate, + 'func_name': func.__name__}) + @functools.wraps(func) def wrapper(*args, **kwargs): - if resource_to_validate in kwargs: + if kwargs.get(resource_to_validate): schema_validator.validate(kwargs[resource_to_validate]) + else: + try: + resource = args[arg_index] + # If resource to be validated is empty, no need to do + # validation since the message given by jsonschema doesn't + # help in this case. + if resource: + schema_validator.validate(resource) + else: + raise exception.ValidationError( + attribute=resource_to_validate, + target='request body') + # We cannot find the resource neither from kwargs nor args. + except IndexError: + raise exception.ValidationError( + attribute=resource_to_validate, + target='request body') return func(*args, **kwargs) return wrapper return add_validator diff --git a/keystone-moon/keystone/common/validation/parameter_types.py b/keystone-moon/keystone/common/validation/parameter_types.py index c5908836..1bc81383 100644 --- a/keystone-moon/keystone/common/validation/parameter_types.py +++ b/keystone-moon/keystone/common/validation/parameter_types.py @@ -28,6 +28,12 @@ name = { 'maxLength': 255 } +external_id_string = { + 'type': 'string', + 'minLength': 1, + 'maxLength': 64 +} + id_string = { 'type': 'string', 'minLength': 1, diff --git a/keystone-moon/keystone/common/wsgi.py b/keystone-moon/keystone/common/wsgi.py index 6ee8150d..0dee954b 100644 --- a/keystone-moon/keystone/common/wsgi.py +++ b/keystone-moon/keystone/common/wsgi.py @@ -20,7 +20,7 @@ import copy import itertools -import urllib +import wsgiref.util from oslo_config import cfg import oslo_i18n @@ -49,10 +49,12 @@ LOG = log.getLogger(__name__) # Environment variable used to pass the request context CONTEXT_ENV = 'openstack.context' - # Environment variable used to pass the request params PARAMS_ENV = 'openstack.params' +JSON_ENCODE_CONTENT_TYPES = set(['application/json', + 'application/json-home']) + def validate_token_bind(context, token_ref): bind_mode = CONF.token.enforce_token_bind @@ -84,7 +86,7 @@ def validate_token_bind(context, token_ref): LOG.info(_LI("Named bind mode %s not in bind information"), name) raise exception.Unauthorized() - for bind_type, identifier in six.iteritems(bind): + for bind_type, identifier in bind.items(): if bind_type == 'kerberos': if not (context['environment'].get('AUTH_TYPE', '').lower() == 'negotiate'): @@ -195,8 +197,16 @@ class Application(BaseApplication): # allow middleware up the stack to provide context, params and headers. context = req.environ.get(CONTEXT_ENV, {}) - context['query_string'] = dict(six.iteritems(req.params)) - context['headers'] = dict(six.iteritems(req.headers)) + + try: + context['query_string'] = dict(req.params.items()) + except UnicodeDecodeError as e: + # The webob package throws UnicodeError when a request cannot be + # decoded. Raise ValidationError instead to avoid an UnknownError. + msg = _('Query string is not UTF-8 encoded') + raise exception.ValidationError(msg) + + context['headers'] = dict(req.headers.items()) context['path'] = req.environ['PATH_INFO'] scheme = (None if not CONF.secure_proxy_ssl_header else req.environ.get(CONF.secure_proxy_ssl_header)) @@ -211,8 +221,8 @@ class Application(BaseApplication): context['host_url'] = req.host_url params = req.environ.get(PARAMS_ENV, {}) # authentication and authorization attributes are set as environment - # values by the container and processed by the pipeline. the complete - # set is not yet know. + # values by the container and processed by the pipeline. The complete + # set is not yet known. context['environment'] = req.environ context['accept_header'] = req.accept req.environ = None @@ -227,11 +237,10 @@ class Application(BaseApplication): # NOTE(morganfainberg): use the request method to normalize the # response code between GET and HEAD requests. The HTTP status should # be the same. - req_method = req.environ['REQUEST_METHOD'].upper() - LOG.info('%(req_method)s %(path)s?%(params)s', { - 'req_method': req_method, - 'path': context['path'], - 'params': urllib.urlencode(req.params)}) + LOG.info('%(req_method)s %(uri)s', { + 'req_method': req.environ['REQUEST_METHOD'].upper(), + 'uri': wsgiref.util.request_uri(req.environ), + }) params = self._normalize_dict(params) @@ -270,7 +279,7 @@ class Application(BaseApplication): response_code = self._get_response_code(req) return render_response(body=result, status=response_code, - method=req_method) + method=req.environ['REQUEST_METHOD']) def _get_response_code(self, req): req_method = req.environ['REQUEST_METHOD'] @@ -284,17 +293,21 @@ class Application(BaseApplication): return arg.replace(':', '_').replace('-', '_') def _normalize_dict(self, d): - return {self._normalize_arg(k): v for (k, v) in six.iteritems(d)} + return {self._normalize_arg(k): v for (k, v) in d.items()} def assert_admin(self, context): + """Ensure the user is an admin. + + :raises keystone.exception.Unauthorized: if a token could not be + found/authorized, a user is invalid, or a tenant is + invalid/not scoped. + :raises keystone.exception.Forbidden: if the user is not an admin and + does not have the admin role + + """ + if not context['is_admin']: - try: - user_token_ref = token_model.KeystoneToken( - token_id=context['token_id'], - token_data=self.token_provider_api.validate_token( - context['token_id'])) - except exception.TokenNotFound as e: - raise exception.Unauthorized(e) + user_token_ref = utils.get_token_ref(context) validate_token_bind(context, user_token_ref) creds = copy.deepcopy(user_token_ref.metadata) @@ -353,16 +366,7 @@ class Application(BaseApplication): LOG.debug(('will not lookup trust as the request auth token is ' 'either absent or it is the system admin token')) return None - - try: - token_data = self.token_provider_api.validate_token( - context['token_id']) - except exception.TokenNotFound: - LOG.warning(_LW('Invalid token in _get_trust_id_for_request')) - raise exception.Unauthorized() - - token_ref = token_model.KeystoneToken(token_id=context['token_id'], - token_data=token_data) + token_ref = utils.get_token_ref(context) return token_ref.trust_id @classmethod @@ -371,8 +375,7 @@ class Application(BaseApplication): if url: substitutions = dict( - itertools.chain(six.iteritems(CONF), - six.iteritems(CONF.eventlet_server))) + itertools.chain(CONF.items(), CONF.eventlet_server.items())) url = url % substitutions else: @@ -491,7 +494,7 @@ class Debug(Middleware): resp = req.get_response(self.application) if not hasattr(LOG, 'isEnabledFor') or LOG.isEnabledFor(LOG.debug): LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE HEADERS', ('*' * 20)) - for (key, value) in six.iteritems(resp.headers): + for (key, value) in resp.headers.items(): LOG.debug('%s = %s', key, value) LOG.debug('') @@ -603,7 +606,7 @@ class ExtensionRouter(Router): mapper = routes.Mapper() self.application = application self.add_routes(mapper) - mapper.connect('{path_info:.*}', controller=self.application) + mapper.connect('/{path_info:.*}', controller=self.application) super(ExtensionRouter, self).__init__(mapper) def add_routes(self, mapper): @@ -657,7 +660,7 @@ class RoutersBase(object): get_action=None, head_action=None, get_head_action=None, put_action=None, post_action=None, patch_action=None, delete_action=None, get_post_action=None, - path_vars=None, status=None): + path_vars=None, status=json_home.Status.STABLE): if get_head_action: getattr(controller, get_head_action) # ensure the attribute exists mapper.connect(path, controller=controller, action=get_head_action, @@ -699,13 +702,7 @@ class RoutersBase(object): else: resource_data['href'] = path - if status: - if not json_home.Status.is_supported(status): - raise exception.Error(message=_( - 'Unexpected status requested for JSON Home response, %s') % - status) - resource_data.setdefault('hints', {}) - resource_data['hints']['status'] = status + json_home.Status.update_resource_data(resource_data, status) self.v3_resources.append((rel, resource_data)) @@ -762,8 +759,6 @@ def render_response(body=None, status=None, headers=None, method=None): else: content_type = None - JSON_ENCODE_CONTENT_TYPES = ('application/json', - 'application/json-home',) if content_type is None or content_type in JSON_ENCODE_CONTENT_TYPES: body = jsonutils.dumps(body, cls=utils.SmarterEncoder) if content_type is None: @@ -774,7 +769,7 @@ def render_response(body=None, status=None, headers=None, method=None): status='%s %s' % status, headerlist=headers) - if method == 'HEAD': + if method and method.upper() == 'HEAD': # NOTE(morganfainberg): HEAD requests should return the same status # as a GET request and same headers (including content-type and # content-length). The webob.Response object automatically changes @@ -785,7 +780,7 @@ def render_response(body=None, status=None, headers=None, method=None): # both py2x and py3x. stored_headers = resp.headers.copy() resp.body = b'' - for header, value in six.iteritems(stored_headers): + for header, value in stored_headers.items(): resp.headers[header] = value return resp @@ -820,8 +815,7 @@ def render_exception(error, context=None, request=None, user_locale=None): url = 'http://localhost:%d' % CONF.eventlet_server.public_port else: substitutions = dict( - itertools.chain(six.iteritems(CONF), - six.iteritems(CONF.eventlet_server))) + itertools.chain(CONF.items(), CONF.eventlet_server.items())) url = url % substitutions headers.append(('WWW-Authenticate', 'Keystone uri="%s"' % url)) diff --git a/keystone-moon/keystone/config.py b/keystone-moon/keystone/config.py index 3d9a29fd..3967cee0 100644 --- a/keystone-moon/keystone/config.py +++ b/keystone-moon/keystone/config.py @@ -47,7 +47,8 @@ def set_default_for_default_log_levels(): ] log.register_options(CONF) - CONF.default_log_levels.extend(extra_log_level_defaults) + CONF.set_default('default_log_levels', + CONF.default_log_levels + extra_log_level_defaults) def setup_logging(): diff --git a/keystone-moon/keystone/contrib/ec2/controllers.py b/keystone-moon/keystone/contrib/ec2/controllers.py index 6e6d3268..78172ec9 100644 --- a/keystone-moon/keystone/contrib/ec2/controllers.py +++ b/keystone-moon/keystone/contrib/ec2/controllers.py @@ -46,7 +46,6 @@ from keystone.common import utils from keystone.common import wsgi from keystone import exception from keystone.i18n import _ -from keystone.models import token_model @dependency.requires('assignment_api', 'catalog_api', 'credential_api', @@ -57,16 +56,30 @@ class Ec2ControllerCommon(object): def check_signature(self, creds_ref, credentials): signer = ec2_utils.Ec2Signer(creds_ref['secret']) signature = signer.generate(credentials) - if utils.auth_str_equal(credentials['signature'], signature): - return - # NOTE(vish): Some libraries don't use the port when signing - # requests, so try again without port. - elif ':' in credentials['signature']: - hostname, _port = credentials['host'].split(':') - credentials['host'] = hostname - signature = signer.generate(credentials) - if not utils.auth_str_equal(credentials.signature, signature): - raise exception.Unauthorized(message='Invalid EC2 signature.') + # NOTE(davechen): credentials.get('signature') is not guaranteed to + # exist, we need check it explicitly. + if credentials.get('signature'): + if utils.auth_str_equal(credentials['signature'], signature): + return True + # NOTE(vish): Some client libraries don't use the port when signing + # requests, so try again without port. + elif ':' in credentials['host']: + hostname, _port = credentials['host'].split(':') + credentials['host'] = hostname + # NOTE(davechen): we need reinitialize 'signer' to avoid + # contaminated status of signature, this is similar with + # other programming language libraries, JAVA for example. + signer = ec2_utils.Ec2Signer(creds_ref['secret']) + signature = signer.generate(credentials) + if utils.auth_str_equal(credentials['signature'], + signature): + return True + raise exception.Unauthorized( + message='Invalid EC2 signature.') + else: + raise exception.Unauthorized( + message='EC2 signature not supplied.') + # Raise the exception when credentials.get('signature') is None else: raise exception.Unauthorized(message='EC2 signature not supplied.') @@ -305,14 +318,7 @@ class Ec2Controller(Ec2ControllerCommon, controller.V2Controller): :raises exception.Forbidden: when token is invalid """ - try: - token_data = self.token_provider_api.validate_token( - context['token_id']) - except exception.TokenNotFound as e: - raise exception.Unauthorized(e) - - token_ref = token_model.KeystoneToken(token_id=context['token_id'], - token_data=token_data) + token_ref = utils.get_token_ref(context) if token_ref.user_id != user_id: raise exception.Forbidden(_('Token belongs to another user')) @@ -329,7 +335,7 @@ class Ec2Controller(Ec2ControllerCommon, controller.V2Controller): # to properly perform policy enforcement. self.assert_admin(context) return True - except exception.Forbidden: + except (exception.Forbidden, exception.Unauthorized): return False def _assert_owner(self, user_id, credential_id): @@ -349,11 +355,11 @@ class Ec2Controller(Ec2ControllerCommon, controller.V2Controller): @dependency.requires('policy_api', 'token_provider_api') class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller): - member_name = 'project' + collection_name = 'credentials' + member_name = 'credential' def __init__(self): super(Ec2ControllerV3, self).__init__() - self.get_member_from_driver = self.credential_api.get_credential def _check_credential_owner_and_user_id_match(self, context, prep_info, user_id, credential_id): @@ -385,23 +391,35 @@ class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller): @controller.protected(callback=_check_credential_owner_and_user_id_match) def ec2_get_credential(self, context, user_id, credential_id): - return super(Ec2ControllerV3, self).get_credential(user_id, - credential_id) + ref = super(Ec2ControllerV3, self).get_credential(user_id, + credential_id) + return Ec2ControllerV3.wrap_member(context, ref['credential']) @controller.protected() def ec2_list_credentials(self, context, user_id): - return super(Ec2ControllerV3, self).get_credentials(user_id) + refs = super(Ec2ControllerV3, self).get_credentials(user_id) + return Ec2ControllerV3.wrap_collection(context, refs['credentials']) @controller.protected() def ec2_create_credential(self, context, user_id, tenant_id): - return super(Ec2ControllerV3, self).create_credential(context, user_id, - tenant_id) + ref = super(Ec2ControllerV3, self).create_credential(context, user_id, + tenant_id) + return Ec2ControllerV3.wrap_member(context, ref['credential']) @controller.protected(callback=_check_credential_owner_and_user_id_match) def ec2_delete_credential(self, context, user_id, credential_id): return super(Ec2ControllerV3, self).delete_credential(user_id, credential_id) + @classmethod + def _add_self_referential_link(cls, context, ref): + path = '/users/%(user_id)s/credentials/OS-EC2/%(credential_id)s' + url = cls.base_url(context, path) % { + 'user_id': ref['user_id'], + 'credential_id': ref['access']} + ref.setdefault('links', {}) + ref['links']['self'] = url + def render_token_data_response(token_id, token_data): """Render token data HTTP response. diff --git a/keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py b/keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py index 6ac3c1ca..22d5796a 100644 --- a/keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py +++ b/keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py @@ -13,20 +13,20 @@ # under the License. from oslo_config import cfg -import six from keystone.catalog.backends import sql from keystone.catalog import core as catalog_core from keystone.common import dependency from keystone import exception + CONF = cfg.CONF @dependency.requires('endpoint_filter_api') class EndpointFilterCatalog(sql.Catalog): def get_v3_catalog(self, user_id, project_id): - substitutions = dict(six.iteritems(CONF)) + substitutions = dict(CONF.items()) substitutions.update({'tenant_id': project_id, 'user_id': user_id}) services = {} @@ -66,7 +66,7 @@ class EndpointFilterCatalog(sql.Catalog): # format catalog catalog = [] - for service_id, service in six.iteritems(services): + for service_id, service in services.items(): formatted_service = {} formatted_service['id'] = service['id'] formatted_service['type'] = service['type'] diff --git a/keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py b/keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py index a998423f..53d511e5 100644 --- a/keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py +++ b/keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py @@ -13,6 +13,7 @@ # under the License. from keystone.common import sql +from keystone.contrib import endpoint_filter from keystone import exception from keystone.i18n import _ @@ -52,7 +53,7 @@ class ProjectEndpointGroupMembership(sql.ModelBase, sql.ModelDictMixin): 'project_id'), {}) -class EndpointFilter(object): +class EndpointFilter(endpoint_filter.Driver): @sql.handle_conflicts(conflict_type='project_endpoint') def add_endpoint_to_project(self, endpoint_id, project_id): @@ -150,9 +151,9 @@ class EndpointFilter(object): endpoint_group_ref = self._get_endpoint_group(session, endpoint_group_id) with session.begin(): - session.delete(endpoint_group_ref) self._delete_endpoint_group_association_by_endpoint_group( session, endpoint_group_id) + session.delete(endpoint_group_ref) def get_endpoint_group_in_project(self, endpoint_group_id, project_id): session = sql.get_session() diff --git a/keystone-moon/keystone/contrib/endpoint_filter/controllers.py b/keystone-moon/keystone/contrib/endpoint_filter/controllers.py index dc4ef7a3..eb627c6b 100644 --- a/keystone-moon/keystone/contrib/endpoint_filter/controllers.py +++ b/keystone-moon/keystone/contrib/endpoint_filter/controllers.py @@ -49,7 +49,7 @@ class _ControllerBase(controller.V3Controller): for endpoint in endpoints: is_candidate = True - for key, value in six.iteritems(filters): + for key, value in filters.items(): if endpoint[key] != value: is_candidate = False break diff --git a/keystone-moon/keystone/contrib/endpoint_filter/core.py b/keystone-moon/keystone/contrib/endpoint_filter/core.py index 972b65dd..1cb35b1f 100644 --- a/keystone-moon/keystone/contrib/endpoint_filter/core.py +++ b/keystone-moon/keystone/contrib/endpoint_filter/core.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +"""Main entry point into the Endpoint Filter service.""" + import abc from oslo_config import cfg @@ -56,6 +58,8 @@ class Manager(manager.Manager): """ + driver_namespace = 'keystone.endpoint_filter' + def __init__(self): super(Manager, self).__init__(CONF.endpoint_filter.driver) diff --git a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py index 090e7f47..2aa93a86 100644 --- a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py +++ b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py @@ -36,12 +36,3 @@ def upgrade(migrate_engine): nullable=False)) endpoint_filtering_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 ['project_endpoint']: - table = sql.Table(table_name, meta, autoload=True) - table.drop() diff --git a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py index 5f80160a..2c218b0d 100644 --- a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py +++ b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py @@ -39,13 +39,3 @@ def upgrade(migrate_engine): sql.PrimaryKeyConstraint('endpoint_group_id', 'project_id')) project_endpoint_group_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 ['project_endpoint_group', - 'endpoint_group']: - table = sql.Table(table_name, meta, autoload=True) - table.drop() diff --git a/keystone-moon/keystone/contrib/endpoint_filter/routers.py b/keystone-moon/keystone/contrib/endpoint_filter/routers.py index 00c8cd72..285b9df2 100644 --- a/keystone-moon/keystone/contrib/endpoint_filter/routers.py +++ b/keystone-moon/keystone/contrib/endpoint_filter/routers.py @@ -36,28 +36,32 @@ class EndpointFilterExtension(wsgi.V3ExtensionRouter): The API looks like:: - PUT /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id - GET /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id - HEAD /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id - DELETE /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id - GET /OS-EP-FILTER/endpoints/$endpoint_id/projects - GET /OS-EP-FILTER/projects/$project_id/endpoints + PUT /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + GET /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + HEAD /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + DELETE /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + GET /OS-EP-FILTER/endpoints/{endpoint_id}/projects + GET /OS-EP-FILTER/projects/{project_id}/endpoints + GET /OS-EP-FILTER/projects/{project_id}/endpoint_groups GET /OS-EP-FILTER/endpoint_groups POST /OS-EP-FILTER/endpoint_groups - GET /OS-EP-FILTER/endpoint_groups/$endpoint_group_id - HEAD /OS-EP-FILTER/endpoint_groups/$endpoint_group_id - PATCH /OS-EP-FILTER/endpoint_groups/$endpoint_group_id - DELETE /OS-EP-FILTER/endpoint_groups/$endpoint_group_id - - GET /OS-EP-FILTER/endpoint_groups/$endpoint_group_id/projects - GET /OS-EP-FILTER/endpoint_groups/$endpoint_group_id/endpoints - - PUT /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/$project_id - GET /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/$project_id - HEAD /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/$project_id - DELETE /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/ - $project_id + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + PATCH /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + DELETE /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/projects + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/endpoints + + PUT /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} + HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} + DELETE /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} """ PATH_PREFIX = '/OS-EP-FILTER' @@ -101,6 +105,15 @@ class EndpointFilterExtension(wsgi.V3ExtensionRouter): }) self._add_resource( mapper, endpoint_group_controller, + path=self.PATH_PREFIX + '/projects/{project_id}/endpoint_groups', + get_action='list_endpoint_groups_for_project', + rel=build_resource_relation( + resource_name='project_endpoint_groups'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + }) + self._add_resource( + mapper, endpoint_group_controller, path=self.PATH_PREFIX + '/endpoint_groups', get_action='list_endpoint_groups', post_action='create_endpoint_group', diff --git a/keystone-moon/keystone/contrib/endpoint_policy/__init__.py b/keystone-moon/keystone/contrib/endpoint_policy/__init__.py index 12722dc5..e69de29b 100644 --- a/keystone-moon/keystone/contrib/endpoint_policy/__init__.py +++ b/keystone-moon/keystone/contrib/endpoint_policy/__init__.py @@ -1,15 +0,0 @@ -# Copyright 2014 IBM Corp. -# -# 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.contrib.endpoint_policy.core import * # noqa diff --git a/keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py b/keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py index 484444f1..54792f30 100644 --- a/keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py +++ b/keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py @@ -1,5 +1,3 @@ -# Copyright 2014 IBM Corp. -# # 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 @@ -12,129 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - -import sqlalchemy - -from keystone.common import sql -from keystone import exception - - -class PolicyAssociation(sql.ModelBase, sql.ModelDictMixin): - __tablename__ = 'policy_association' - attributes = ['policy_id', 'endpoint_id', 'region_id', 'service_id'] - # The id column is never exposed outside this module. It only exists to - # provide a primary key, given that the real columns we would like to use - # (endpoint_id, service_id, region_id) can be null - id = sql.Column(sql.String(64), primary_key=True) - policy_id = sql.Column(sql.String(64), nullable=False) - endpoint_id = sql.Column(sql.String(64), nullable=True) - service_id = sql.Column(sql.String(64), nullable=True) - region_id = sql.Column(sql.String(64), nullable=True) - __table_args__ = (sql.UniqueConstraint('endpoint_id', 'service_id', - 'region_id'), {}) - - def to_dict(self): - """Returns the model's attributes as a dictionary. - - We override the standard method in order to hide the id column, - since this only exists to provide the table with a primary key. - - """ - d = {} - for attr in self.__class__.attributes: - d[attr] = getattr(self, attr) - return d - - -class EndpointPolicy(object): - - def create_policy_association(self, policy_id, endpoint_id=None, - service_id=None, region_id=None): - with sql.transaction() as session: - try: - # See if there is already a row for this association, and if - # so, update it with the new policy_id - query = session.query(PolicyAssociation) - query = query.filter_by(endpoint_id=endpoint_id) - query = query.filter_by(service_id=service_id) - query = query.filter_by(region_id=region_id) - association = query.one() - association.policy_id = policy_id - except sql.NotFound: - association = PolicyAssociation(id=uuid.uuid4().hex, - policy_id=policy_id, - endpoint_id=endpoint_id, - service_id=service_id, - region_id=region_id) - session.add(association) - - def check_policy_association(self, policy_id, endpoint_id=None, - service_id=None, region_id=None): - sql_constraints = sqlalchemy.and_( - PolicyAssociation.policy_id == policy_id, - PolicyAssociation.endpoint_id == endpoint_id, - PolicyAssociation.service_id == service_id, - PolicyAssociation.region_id == region_id) - - # NOTE(henry-nash): Getting a single value to save object - # management overhead. - with sql.transaction() as session: - if session.query(PolicyAssociation.id).filter( - sql_constraints).distinct().count() == 0: - raise exception.PolicyAssociationNotFound() - - def delete_policy_association(self, policy_id, endpoint_id=None, - service_id=None, region_id=None): - with sql.transaction() as session: - query = session.query(PolicyAssociation) - query = query.filter_by(policy_id=policy_id) - query = query.filter_by(endpoint_id=endpoint_id) - query = query.filter_by(service_id=service_id) - query = query.filter_by(region_id=region_id) - query.delete() +import logging - def get_policy_association(self, endpoint_id=None, - service_id=None, region_id=None): - sql_constraints = sqlalchemy.and_( - PolicyAssociation.endpoint_id == endpoint_id, - PolicyAssociation.service_id == service_id, - PolicyAssociation.region_id == region_id) +from oslo_log import versionutils - try: - with sql.transaction() as session: - policy_id = session.query(PolicyAssociation.policy_id).filter( - sql_constraints).distinct().one() - return {'policy_id': policy_id} - except sql.NotFound: - raise exception.PolicyAssociationNotFound() +from keystone.endpoint_policy.backends import sql - def list_associations_for_policy(self, policy_id): - with sql.transaction() as session: - query = session.query(PolicyAssociation) - query = query.filter_by(policy_id=policy_id) - return [ref.to_dict() for ref in query.all()] +LOG = logging.getLogger(__name__) - def delete_association_by_endpoint(self, endpoint_id): - with sql.transaction() as session: - query = session.query(PolicyAssociation) - query = query.filter_by(endpoint_id=endpoint_id) - query.delete() +_OLD = 'keystone.contrib.endpoint_policy.backends.sql.EndpointPolicy' +_NEW = 'keystone.endpoint_policy.backends.sql.EndpointPolicy' - def delete_association_by_service(self, service_id): - with sql.transaction() as session: - query = session.query(PolicyAssociation) - query = query.filter_by(service_id=service_id) - query.delete() - def delete_association_by_region(self, region_id): - with sql.transaction() as session: - query = session.query(PolicyAssociation) - query = query.filter_by(region_id=region_id) - query.delete() +class EndpointPolicy(sql.EndpointPolicy): - def delete_association_by_policy(self, policy_id): - with sql.transaction() as session: - query = session.query(PolicyAssociation) - query = query.filter_by(policy_id=policy_id) - query.delete() + @versionutils.deprecated(versionutils.deprecated.LIBERTY, + in_favor_of=_NEW, + remove_in=1, + what=_OLD) + def __init__(self, *args, **kwargs): + super(EndpointPolicy, self).__init__(*args, **kwargs) diff --git a/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py index c77e4380..5c22f169 100644 --- a/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py +++ b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py @@ -38,11 +38,3 @@ def upgrade(migrate_engine): mysql_charset='utf8') endpoint_policy_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. - table = sql.Table('policy_association', meta, autoload=True) - table.drop() diff --git a/keystone-moon/keystone/contrib/endpoint_policy/routers.py b/keystone-moon/keystone/contrib/endpoint_policy/routers.py index 999d1eed..714d1663 100644 --- a/keystone-moon/keystone/contrib/endpoint_policy/routers.py +++ b/keystone-moon/keystone/contrib/endpoint_policy/routers.py @@ -1,5 +1,3 @@ -# Copyright 2014 IBM Corp. -# # 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 @@ -12,74 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. -import functools - -from keystone.common import json_home -from keystone.common import wsgi -from keystone.contrib.endpoint_policy import controllers +import logging +from oslo_log import versionutils -build_resource_relation = functools.partial( - json_home.build_v3_extension_resource_relation, - extension_name='OS-ENDPOINT-POLICY', extension_version='1.0') +from keystone.common import wsgi +LOG = logging.getLogger(__name__) -class EndpointPolicyExtension(wsgi.V3ExtensionRouter): +_OLD = 'keystone.contrib.endpoint_policy.routers.EndpointPolicyExtension' +_NEW = 'keystone.endpoint_policy.routers.Routers' - PATH_PREFIX = '/OS-ENDPOINT-POLICY' - def add_routes(self, mapper): - endpoint_policy_controller = controllers.EndpointPolicyV3Controller() +class EndpointPolicyExtension(wsgi.Middleware): - self._add_resource( - mapper, endpoint_policy_controller, - path='/endpoints/{endpoint_id}' + self.PATH_PREFIX + '/policy', - get_head_action='get_policy_for_endpoint', - rel=build_resource_relation(resource_name='endpoint_policy'), - path_vars={'endpoint_id': json_home.Parameters.ENDPOINT_ID}) - self._add_resource( - mapper, endpoint_policy_controller, - path='/policies/{policy_id}' + self.PATH_PREFIX + '/endpoints', - get_action='list_endpoints_for_policy', - rel=build_resource_relation(resource_name='policy_endpoints'), - path_vars={'policy_id': json_home.Parameters.POLICY_ID}) - self._add_resource( - mapper, endpoint_policy_controller, - path=('/policies/{policy_id}' + self.PATH_PREFIX + - '/endpoints/{endpoint_id}'), - get_head_action='check_policy_association_for_endpoint', - put_action='create_policy_association_for_endpoint', - delete_action='delete_policy_association_for_endpoint', - rel=build_resource_relation( - resource_name='endpoint_policy_association'), - path_vars={ - 'policy_id': json_home.Parameters.POLICY_ID, - 'endpoint_id': json_home.Parameters.ENDPOINT_ID, - }) - self._add_resource( - mapper, endpoint_policy_controller, - path=('/policies/{policy_id}' + self.PATH_PREFIX + - '/services/{service_id}'), - get_head_action='check_policy_association_for_service', - put_action='create_policy_association_for_service', - delete_action='delete_policy_association_for_service', - rel=build_resource_relation( - resource_name='service_policy_association'), - path_vars={ - 'policy_id': json_home.Parameters.POLICY_ID, - 'service_id': json_home.Parameters.SERVICE_ID, - }) - self._add_resource( - mapper, endpoint_policy_controller, - path=('/policies/{policy_id}' + self.PATH_PREFIX + - '/services/{service_id}/regions/{region_id}'), - get_head_action='check_policy_association_for_region_and_service', - put_action='create_policy_association_for_region_and_service', - delete_action='delete_policy_association_for_region_and_service', - rel=build_resource_relation( - resource_name='region_and_service_policy_association'), - path_vars={ - 'policy_id': json_home.Parameters.POLICY_ID, - 'service_id': json_home.Parameters.SERVICE_ID, - 'region_id': json_home.Parameters.REGION_ID, - }) + @versionutils.deprecated(versionutils.deprecated.LIBERTY, + in_favor_of=_NEW, + remove_in=1, + what=_OLD) + def __init__(self, *args, **kwargs): + super(EndpointPolicyExtension, self).__init__(*args, **kwargs) diff --git a/keystone-moon/keystone/contrib/example/core.py b/keystone-moon/keystone/contrib/example/core.py index 6e85c7f7..e369dc4d 100644 --- a/keystone-moon/keystone/contrib/example/core.py +++ b/keystone-moon/keystone/contrib/example/core.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +"""Main entry point into this Example service.""" + from oslo_log import log from keystone.common import dependency @@ -24,15 +26,18 @@ from keystone import notifications LOG = log.getLogger(__name__) +@notifications.listener # NOTE(dstanek): only needed if using event_callbacks @dependency.provider('example_api') class ExampleManager(manager.Manager): - """Example Manager. + """Default pivot point for this Example backend. See :mod:`keystone.common.manager.Manager` for more details on how this dynamically calls the backend. """ + driver_namespace = 'keystone.example' + def __init__(self): # The following is an example of event callbacks. In this setup, # ExampleManager's data model is depended on project's data model. @@ -45,8 +50,8 @@ class ExampleManager(manager.Manager): # project_created_callback will be invoked whenever a new project is # created. - # This information is used when the @dependency.provider decorator acts - # on the class. + # This information is used when the @notifications.listener decorator + # acts on the class. self.event_callbacks = { notifications.ACTIONS.deleted: { 'project': [self.project_deleted_callback], diff --git a/keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py b/keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py index 10b7ccc7..35061780 100644 --- a/keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py +++ b/keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py @@ -30,14 +30,3 @@ def upgrade(migrate_engine): sql.Column('type', sql.String(255)), sql.Column('extra', sql.Text())) service_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - meta = sql.MetaData() - meta.bind = migrate_engine - - tables = ['example'] - for t in tables: - table = sql.Table(t, meta, autoload=True) - table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/contrib/federation/backends/sql.py b/keystone-moon/keystone/contrib/federation/backends/sql.py index f2c124d0..ed07c08f 100644 --- a/keystone-moon/keystone/contrib/federation/backends/sql.py +++ b/keystone-moon/keystone/contrib/federation/backends/sql.py @@ -17,6 +17,7 @@ from oslo_serialization import jsonutils from keystone.common import sql from keystone.contrib.federation import core from keystone import exception +from sqlalchemy import orm class FederationProtocolModel(sql.ModelBase, sql.DictBase): @@ -44,13 +45,53 @@ class FederationProtocolModel(sql.ModelBase, sql.DictBase): class IdentityProviderModel(sql.ModelBase, sql.DictBase): __tablename__ = 'identity_provider' - attributes = ['id', 'remote_id', 'enabled', 'description'] - mutable_attributes = frozenset(['description', 'enabled', 'remote_id']) + attributes = ['id', 'enabled', 'description', 'remote_ids'] + mutable_attributes = frozenset(['description', 'enabled', 'remote_ids']) id = sql.Column(sql.String(64), primary_key=True) - remote_id = sql.Column(sql.String(256), nullable=True) enabled = sql.Column(sql.Boolean, nullable=False) description = sql.Column(sql.Text(), nullable=True) + remote_ids = orm.relationship('IdPRemoteIdsModel', + order_by='IdPRemoteIdsModel.remote_id', + cascade='all, delete-orphan') + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + remote_ids_list = new_dictionary.pop('remote_ids', None) + if not remote_ids_list: + remote_ids_list = [] + identity_provider = cls(**new_dictionary) + remote_ids = [] + # NOTE(fmarco76): the remote_ids_list contains only remote ids + # associated with the IdP because of the "relationship" established in + # sqlalchemy and corresponding to the FK in the idp_remote_ids table + for remote in remote_ids_list: + remote_ids.append(IdPRemoteIdsModel(remote_id=remote)) + identity_provider.remote_ids = remote_ids + return identity_provider + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + d['remote_ids'] = [] + for remote in self.remote_ids: + d['remote_ids'].append(remote.remote_id) + return d + + +class IdPRemoteIdsModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'idp_remote_ids' + attributes = ['idp_id', 'remote_id'] + mutable_attributes = frozenset(['idp_id', 'remote_id']) + + idp_id = sql.Column(sql.String(64), + sql.ForeignKey('identity_provider.id', + ondelete='CASCADE')) + remote_id = sql.Column(sql.String(255), + primary_key=True) @classmethod def from_dict(cls, dictionary): @@ -75,6 +116,7 @@ class MappingModel(sql.ModelBase, sql.DictBase): @classmethod def from_dict(cls, dictionary): new_dictionary = dictionary.copy() + new_dictionary['rules'] = jsonutils.dumps(new_dictionary['rules']) return cls(**new_dictionary) def to_dict(self): @@ -82,20 +124,23 @@ class MappingModel(sql.ModelBase, sql.DictBase): d = dict() for attr in self.__class__.attributes: d[attr] = getattr(self, attr) + d['rules'] = jsonutils.loads(d['rules']) return d class ServiceProviderModel(sql.ModelBase, sql.DictBase): __tablename__ = 'service_provider' - attributes = ['auth_url', 'id', 'enabled', 'description', 'sp_url'] + attributes = ['auth_url', 'id', 'enabled', 'description', + 'relay_state_prefix', 'sp_url'] mutable_attributes = frozenset(['auth_url', 'description', 'enabled', - 'sp_url']) + 'relay_state_prefix', 'sp_url']) id = sql.Column(sql.String(64), primary_key=True) enabled = sql.Column(sql.Boolean, nullable=False) description = sql.Column(sql.Text(), nullable=True) auth_url = sql.Column(sql.String(256), nullable=False) sp_url = sql.Column(sql.String(256), nullable=False) + relay_state_prefix = sql.Column(sql.String(256), nullable=False) @classmethod def from_dict(cls, dictionary): @@ -123,6 +168,7 @@ class Federation(core.Driver): def delete_idp(self, idp_id): with sql.transaction() as session: + self._delete_assigned_protocols(session, idp_id) idp_ref = self._get_idp(session, idp_id) session.delete(idp_ref) @@ -133,7 +179,7 @@ class Federation(core.Driver): return idp_ref def _get_idp_from_remote_id(self, session, remote_id): - q = session.query(IdentityProviderModel) + q = session.query(IdPRemoteIdsModel) q = q.filter_by(remote_id=remote_id) try: return q.one() @@ -153,8 +199,8 @@ class Federation(core.Driver): def get_idp_from_remote_id(self, remote_id): with sql.transaction() as session: - idp_ref = self._get_idp_from_remote_id(session, remote_id) - return idp_ref.to_dict() + ref = self._get_idp_from_remote_id(session, remote_id) + return ref.to_dict() def update_idp(self, idp_id, idp): with sql.transaction() as session: @@ -214,6 +260,11 @@ class Federation(core.Driver): key_ref = self._get_protocol(session, idp_id, protocol_id) session.delete(key_ref) + def _delete_assigned_protocols(self, session, idp_id): + query = session.query(FederationProtocolModel) + query = query.filter_by(idp_id=idp_id) + query.delete() + # Mapping CRUD def _get_mapping(self, session, mapping_id): mapping_ref = session.query(MappingModel).get(mapping_id) @@ -225,7 +276,7 @@ class Federation(core.Driver): def create_mapping(self, mapping_id, mapping): ref = {} ref['id'] = mapping_id - ref['rules'] = jsonutils.dumps(mapping.get('rules')) + ref['rules'] = mapping.get('rules') with sql.transaction() as session: mapping_ref = MappingModel.from_dict(ref) session.add(mapping_ref) @@ -250,7 +301,7 @@ class Federation(core.Driver): def update_mapping(self, mapping_id, mapping): ref = {} ref['id'] = mapping_id - ref['rules'] = jsonutils.dumps(mapping.get('rules')) + ref['rules'] = mapping.get('rules') with sql.transaction() as session: mapping_ref = self._get_mapping(session, mapping_id) old_mapping = mapping_ref.to_dict() diff --git a/keystone-moon/keystone/contrib/federation/constants.py b/keystone-moon/keystone/contrib/federation/constants.py new file mode 100644 index 00000000..afb38494 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/constants.py @@ -0,0 +1,15 @@ +# 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. + +FEDERATION = 'OS-FEDERATION' +IDENTITY_PROVIDER = 'OS-FEDERATION:identity_provider' +PROTOCOL = 'OS-FEDERATION:protocol' diff --git a/keystone-moon/keystone/contrib/federation/controllers.py b/keystone-moon/keystone/contrib/federation/controllers.py index 6066a33f..912d45d5 100644 --- a/keystone-moon/keystone/contrib/federation/controllers.py +++ b/keystone-moon/keystone/contrib/federation/controllers.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Extensions supporting Federation.""" +"""Workflow logic for the Federation service.""" import string @@ -55,9 +55,9 @@ class IdentityProvider(_ControllerBase): collection_name = 'identity_providers' member_name = 'identity_provider' - _mutable_parameters = frozenset(['description', 'enabled', 'remote_id']) + _mutable_parameters = frozenset(['description', 'enabled', 'remote_ids']) _public_parameters = frozenset(['id', 'enabled', 'description', - 'remote_id', 'links' + 'remote_ids', 'links' ]) @classmethod @@ -247,6 +247,36 @@ class MappingController(_ControllerBase): @dependency.requires('federation_api') class Auth(auth_controllers.Auth): + def _get_sso_origin_host(self, context): + """Validate and return originating dashboard URL. + + Make sure the parameter is specified in the request's URL as well its + value belongs to a list of trusted dashboards. + + :param context: request's context + :raises: exception.ValidationError: ``origin`` query parameter was not + specified. The URL is deemed invalid. + :raises: exception.Unauthorized: URL specified in origin query + parameter does not exist in list of websso trusted dashboards. + :returns: URL with the originating dashboard + + """ + if 'origin' in context['query_string']: + origin = context['query_string'].get('origin') + host = urllib.parse.unquote_plus(origin) + else: + msg = _('Request must have an origin query parameter') + LOG.error(msg) + raise exception.ValidationError(msg) + + if host not in CONF.federation.trusted_dashboard: + msg = _('%(host)s is not a trusted dashboard host') + msg = msg % {'host': host} + LOG.error(msg) + raise exception.Unauthorized(msg) + + return host + def federated_authentication(self, context, identity_provider, protocol): """Authenticate from dedicated url endpoint. @@ -268,33 +298,23 @@ class Auth(auth_controllers.Auth): def federated_sso_auth(self, context, protocol_id): try: - remote_id_name = CONF.federation.remote_id_attribute + remote_id_name = utils.get_remote_id_parameter(protocol_id) remote_id = context['environment'][remote_id_name] except KeyError: msg = _('Missing entity ID from environment') LOG.error(msg) raise exception.Unauthorized(msg) - if 'origin' in context['query_string']: - origin = context['query_string'].get('origin') - host = urllib.parse.unquote_plus(origin) - else: - msg = _('Request must have an origin query parameter') - LOG.error(msg) - raise exception.ValidationError(msg) + host = self._get_sso_origin_host(context) - if host in CONF.federation.trusted_dashboard: - ref = self.federation_api.get_idp_from_remote_id(remote_id) - identity_provider = ref['id'] - res = self.federated_authentication(context, identity_provider, - protocol_id) - token_id = res.headers['X-Subject-Token'] - return self.render_html_response(host, token_id) - else: - msg = _('%(host)s is not a trusted dashboard host') - msg = msg % {'host': host} - LOG.error(msg) - raise exception.Unauthorized(msg) + ref = self.federation_api.get_idp_from_remote_id(remote_id) + # NOTE(stevemar): the returned object is a simple dict that + # contains the idp_id and remote_id. + identity_provider = ref['idp_id'] + res = self.federated_authentication(context, identity_provider, + protocol_id) + token_id = res.headers['X-Subject-Token'] + return self.render_html_response(host, token_id) def render_html_response(self, host, token_id): """Forms an HTML Form from a template with autosubmit.""" @@ -309,45 +329,77 @@ class Auth(auth_controllers.Auth): return webob.Response(body=body, status='200', headerlist=headers) - @validation.validated(schema.saml_create, 'auth') - def create_saml_assertion(self, context, auth): - """Exchange a scoped token for a SAML assertion. - - :param auth: Dictionary that contains a token and service provider id - :returns: SAML Assertion based on properties from the token - """ - + def _create_base_saml_assertion(self, context, auth): issuer = CONF.saml.idp_entity_id sp_id = auth['scope']['service_provider']['id'] service_provider = self.federation_api.get_sp(sp_id) utils.assert_enabled_service_provider_object(service_provider) - sp_url = service_provider.get('sp_url') - auth_url = service_provider.get('auth_url') token_id = auth['identity']['token']['id'] token_data = self.token_provider_api.validate_token(token_id) token_ref = token_model.KeystoneToken(token_id, token_data) - subject = token_ref.user_name - roles = token_ref.role_names if not token_ref.project_scoped: action = _('Use a project scoped token when attempting to create ' 'a SAML assertion') raise exception.ForbiddenAction(action=action) + subject = token_ref.user_name + roles = token_ref.role_names project = token_ref.project_name + # NOTE(rodrigods): the domain name is necessary in order to distinguish + # between projects and users with the same name in different domains. + project_domain_name = token_ref.project_domain_name + subject_domain_name = token_ref.user_domain_name + generator = keystone_idp.SAMLGenerator() - response = generator.samlize_token(issuer, sp_url, subject, roles, - project) + response = generator.samlize_token( + issuer, sp_url, subject, subject_domain_name, + roles, project, project_domain_name) + return (response, service_provider) + + def _build_response_headers(self, service_provider): + return [('Content-Type', 'text/xml'), + ('X-sp-url', six.binary_type(service_provider['sp_url'])), + ('X-auth-url', six.binary_type(service_provider['auth_url']))] + + @validation.validated(schema.saml_create, 'auth') + def create_saml_assertion(self, context, auth): + """Exchange a scoped token for a SAML assertion. + + :param auth: Dictionary that contains a token and service provider ID + :returns: SAML Assertion based on properties from the token + """ + t = self._create_base_saml_assertion(context, auth) + (response, service_provider) = t + + headers = self._build_response_headers(service_provider) return wsgi.render_response(body=response.to_string(), status=('200', 'OK'), - headers=[('Content-Type', 'text/xml'), - ('X-sp-url', - six.binary_type(sp_url)), - ('X-auth-url', - six.binary_type(auth_url))]) + headers=headers) + + @validation.validated(schema.saml_create, 'auth') + def create_ecp_assertion(self, context, auth): + """Exchange a scoped token for an ECP assertion. + + :param auth: Dictionary that contains a token and service provider ID + :returns: ECP Assertion based on properties from the token + """ + + t = self._create_base_saml_assertion(context, auth) + (saml_assertion, service_provider) = t + relay_state_prefix = service_provider.get('relay_state_prefix') + + generator = keystone_idp.ECPGenerator() + ecp_assertion = generator.generate_ecp(saml_assertion, + relay_state_prefix) + + headers = self._build_response_headers(service_provider) + return wsgi.render_response(body=ecp_assertion.to_string(), + status=('200', 'OK'), + headers=headers) @dependency.requires('assignment_api', 'resource_api') @@ -404,15 +456,17 @@ class ServiceProvider(_ControllerBase): member_name = 'service_provider' _mutable_parameters = frozenset(['auth_url', 'description', 'enabled', - 'sp_url']) + 'relay_state_prefix', 'sp_url']) _public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description', - 'links', 'sp_url']) + 'links', 'relay_state_prefix', 'sp_url']) @controller.protected() @validation.validated(schema.service_provider_create, 'service_provider') def create_service_provider(self, context, sp_id, service_provider): service_provider = self._normalize_dict(service_provider) service_provider.setdefault('enabled', False) + service_provider.setdefault('relay_state_prefix', + CONF.saml.relay_state_prefix) ServiceProvider.check_immutable_params(service_provider) sp_ref = self.federation_api.create_sp(sp_id, service_provider) response = ServiceProvider.wrap_member(context, sp_ref) diff --git a/keystone-moon/keystone/contrib/federation/core.py b/keystone-moon/keystone/contrib/federation/core.py index b596cff7..2ab75ecb 100644 --- a/keystone-moon/keystone/contrib/federation/core.py +++ b/keystone-moon/keystone/contrib/federation/core.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Extension supporting Federation.""" +"""Main entry point into the Federation service.""" import abc @@ -21,6 +21,7 @@ import six from keystone.common import dependency from keystone.common import extension from keystone.common import manager +from keystone.contrib.federation import utils from keystone import exception @@ -41,11 +42,6 @@ EXTENSION_DATA = { extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) -FEDERATION = 'OS-FEDERATION' -IDENTITY_PROVIDER = 'OS-FEDERATION:identity_provider' -PROTOCOL = 'OS-FEDERATION:protocol' -FEDERATED_DOMAIN_KEYWORD = 'Federated' - @dependency.provider('federation_api') class Manager(manager.Manager): @@ -55,6 +51,9 @@ class Manager(manager.Manager): dynamically calls the backend. """ + + driver_namespace = 'keystone.federation' + def __init__(self): super(Manager, self).__init__(CONF.federation.driver) @@ -84,6 +83,13 @@ class Manager(manager.Manager): service_providers = self.driver.get_enabled_service_providers() return [normalize(sp) for sp in service_providers] + def evaluate(self, idp_id, protocol_id, assertion_data): + mapping = self.get_mapping_from_idp_and_protocol(idp_id, protocol_id) + rules = mapping['rules'] + rule_processor = utils.RuleProcessor(rules) + mapped_properties = rule_processor.process(assertion_data) + return mapped_properties, mapping['id'] + @six.add_metaclass(abc.ABCMeta) class Driver(object): diff --git a/keystone-moon/keystone/contrib/federation/idp.py b/keystone-moon/keystone/contrib/federation/idp.py index bf400135..739fc01a 100644 --- a/keystone-moon/keystone/contrib/federation/idp.py +++ b/keystone-moon/keystone/contrib/federation/idp.py @@ -17,17 +17,24 @@ import uuid from oslo_config import cfg from oslo_log import log +from oslo_utils import fileutils +from oslo_utils import importutils from oslo_utils import timeutils import saml2 +from saml2 import client_base from saml2 import md +from saml2.profile import ecp from saml2 import saml from saml2 import samlp +from saml2.schema import soapenv from saml2 import sigver -import xmldsig +xmldsig = importutils.try_import("saml2.xmldsig") +if not xmldsig: + xmldsig = importutils.try_import("xmldsig") +from keystone.common import utils from keystone import exception from keystone.i18n import _, _LE -from keystone.openstack.common import fileutils LOG = log.getLogger(__name__) @@ -40,8 +47,8 @@ class SAMLGenerator(object): def __init__(self): self.assertion_id = uuid.uuid4().hex - def samlize_token(self, issuer, recipient, user, roles, project, - expires_in=None): + def samlize_token(self, issuer, recipient, user, user_domain_name, roles, + project, project_domain_name, expires_in=None): """Convert Keystone attributes to a SAML assertion. :param issuer: URL of the issuing party @@ -50,10 +57,14 @@ class SAMLGenerator(object): :type recipient: string :param user: User name :type user: string + :param user_domain_name: User Domain name + :type user_domain_name: string :param roles: List of role names :type roles: list :param project: Project name :type project: string + :param project_domain_name: Project Domain name + :type project_domain_name: string :param expires_in: Sets how long the assertion is valid for, in seconds :type expires_in: int @@ -64,8 +75,8 @@ class SAMLGenerator(object): status = self._create_status() saml_issuer = self._create_issuer(issuer) subject = self._create_subject(user, expiration_time, recipient) - attribute_statement = self._create_attribute_statement(user, roles, - project) + attribute_statement = self._create_attribute_statement( + user, user_domain_name, roles, project, project_domain_name) authn_statement = self._create_authn_statement(issuer, expiration_time) signature = self._create_signature() @@ -84,7 +95,7 @@ class SAMLGenerator(object): expires_in = CONF.saml.assertion_expiration_time now = timeutils.utcnow() future = now + datetime.timedelta(seconds=expires_in) - return timeutils.isotime(future, subsecond=True) + return utils.isotime(future, subsecond=True) def _create_status(self): """Create an object that represents a SAML Status. @@ -150,58 +161,64 @@ class SAMLGenerator(object): subject.name_id = name_id return subject - def _create_attribute_statement(self, user, roles, project): + def _create_attribute_statement(self, user, user_domain_name, roles, + project, project_domain_name): """Create an object that represents a SAML AttributeStatement. - <ns0:AttributeStatement - xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <ns0:AttributeStatement> <ns0:Attribute Name="openstack_user"> <ns0:AttributeValue xsi:type="xs:string">test_user</ns0:AttributeValue> </ns0:Attribute> + <ns0:Attribute Name="openstack_user_domain"> + <ns0:AttributeValue + xsi:type="xs:string">Default</ns0:AttributeValue> + </ns0:Attribute> <ns0:Attribute Name="openstack_roles"> <ns0:AttributeValue xsi:type="xs:string">admin</ns0:AttributeValue> <ns0:AttributeValue xsi:type="xs:string">member</ns0:AttributeValue> </ns0:Attribute> - <ns0:Attribute Name="openstack_projects"> + <ns0:Attribute Name="openstack_project"> <ns0:AttributeValue xsi:type="xs:string">development</ns0:AttributeValue> </ns0:Attribute> + <ns0:Attribute Name="openstack_project_domain"> + <ns0:AttributeValue + xsi:type="xs:string">Default</ns0:AttributeValue> + </ns0:Attribute> </ns0:AttributeStatement> :return: XML <AttributeStatement> object """ - openstack_user = 'openstack_user' - user_attribute = saml.Attribute() - user_attribute.name = openstack_user - user_value = saml.AttributeValue() - user_value.set_text(user) - user_attribute.attribute_value = user_value - - openstack_roles = 'openstack_roles' - roles_attribute = saml.Attribute() - roles_attribute.name = openstack_roles - - for role in roles: - role_value = saml.AttributeValue() - role_value.set_text(role) - roles_attribute.attribute_value.append(role_value) - - openstack_project = 'openstack_project' - project_attribute = saml.Attribute() - project_attribute.name = openstack_project - project_value = saml.AttributeValue() - project_value.set_text(project) - project_attribute.attribute_value = project_value + + def _build_attribute(attribute_name, attribute_values): + attribute = saml.Attribute() + attribute.name = attribute_name + + for value in attribute_values: + attribute_value = saml.AttributeValue() + attribute_value.set_text(value) + attribute.attribute_value.append(attribute_value) + + return attribute + + user_attribute = _build_attribute('openstack_user', [user]) + roles_attribute = _build_attribute('openstack_roles', roles) + project_attribute = _build_attribute('openstack_project', [project]) + project_domain_attribute = _build_attribute( + 'openstack_project_domain', [project_domain_name]) + user_domain_attribute = _build_attribute( + 'openstack_user_domain', [user_domain_name]) attribute_statement = saml.AttributeStatement() attribute_statement.attribute.append(user_attribute) attribute_statement.attribute.append(roles_attribute) attribute_statement.attribute.append(project_attribute) + attribute_statement.attribute.append(project_domain_attribute) + attribute_statement.attribute.append(user_domain_attribute) return attribute_statement def _create_authn_statement(self, issuer, expiration_time): @@ -224,7 +241,7 @@ class SAMLGenerator(object): """ authn_statement = saml.AuthnStatement() - authn_statement.authn_instant = timeutils.isotime() + authn_statement.authn_instant = utils.isotime() authn_statement.session_index = uuid.uuid4().hex authn_statement.session_not_on_or_after = expiration_time @@ -261,7 +278,7 @@ class SAMLGenerator(object): """ assertion = saml.Assertion() assertion.id = self.assertion_id - assertion.issue_instant = timeutils.isotime() + assertion.issue_instant = utils.isotime() assertion.version = '2.0' assertion.issuer = issuer assertion.signature = signature @@ -289,7 +306,7 @@ class SAMLGenerator(object): response = samlp.Response() response.id = uuid.uuid4().hex response.destination = recipient - response.issue_instant = timeutils.isotime() + response.issue_instant = utils.isotime() response.version = '2.0' response.issuer = issuer response.status = status @@ -397,6 +414,7 @@ def _sign_assertion(assertion): command_list = [xmlsec_binary, '--sign', '--privkey-pem', certificates, '--id-attr:ID', 'Assertion'] + file_path = None try: # NOTE(gyee): need to make the namespace prefixes explicit so # they won't get reassigned when we wrap the assertion into @@ -405,15 +423,19 @@ def _sign_assertion(assertion): nspair={'saml': saml2.NAMESPACE, 'xmldsig': xmldsig.NAMESPACE})) command_list.append(file_path) - stdout = subprocess.check_output(command_list) + stdout = subprocess.check_output(command_list, + stderr=subprocess.STDOUT) except Exception as e: msg = _LE('Error when signing assertion, reason: %(reason)s') msg = msg % {'reason': e} + if hasattr(e, 'output'): + msg += ' output: %(output)s' % {'output': e.output} LOG.error(msg) raise exception.SAMLSigningError(reason=e) finally: try: - os.remove(file_path) + if file_path: + os.remove(file_path) except OSError: pass @@ -556,3 +578,31 @@ class MetadataGenerator(object): if value is None: return False return True + + +class ECPGenerator(object): + """A class for generating an ECP assertion.""" + + @staticmethod + def generate_ecp(saml_assertion, relay_state_prefix): + ecp_generator = ECPGenerator() + header = ecp_generator._create_header(relay_state_prefix) + body = ecp_generator._create_body(saml_assertion) + envelope = soapenv.Envelope(header=header, body=body) + return envelope + + def _create_header(self, relay_state_prefix): + relay_state_text = relay_state_prefix + uuid.uuid4().hex + relay_state = ecp.RelayState(actor=client_base.ACTOR, + must_understand='1', + text=relay_state_text) + header = soapenv.Header() + header.extension_elements = ( + [saml2.element_to_extension_element(relay_state)]) + return header + + def _create_body(self, saml_assertion): + body = soapenv.Body() + body.extension_elements = ( + [saml2.element_to_extension_element(saml_assertion)]) + return body diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py index cfb6f2c4..9a4d574b 100644 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py @@ -40,12 +40,3 @@ def upgrade(migrate_engine): mysql_charset='utf8') federation_protocol_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - tables = ['federation_protocol', 'identity_provider'] - for table_name in tables: - table = sql.Table(table_name, meta, autoload=True) - table.drop() diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py index f827f9a9..9a155f5c 100644 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py @@ -25,13 +25,3 @@ def upgrade(migrate_engine): mysql_engine='InnoDB', mysql_charset='utf8') mapping_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - # Drop previously created tables - tables = ['mapping'] - for table_name in tables: - table = sql.Table(table_name, meta, autoload=True) - table.drop() diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py index eb8b2378..1731b0d3 100644 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py @@ -27,9 +27,3 @@ def upgrade(migrate_engine): values(mapping_id='')) migrate_engine.execute(stmt) federation_protocol.c.mapping_id.alter(nullable=False) - - -def downgrade(migrate_engine): - meta = sa.MetaData(bind=migrate_engine) - federation_protocol = sa.Table('federation_protocol', meta, autoload=True) - federation_protocol.c.mapping_id.alter(nullable=True) diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py index dbe5d1f1..2e0aaf93 100644 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py @@ -21,10 +21,3 @@ def upgrade(migrate_engine): idp_table = utils.get_table(migrate_engine, 'identity_provider') remote_id = sql.Column('remote_id', sql.String(256), nullable=True) idp_table.create_column(remote_id) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - idp_table = utils.get_table(migrate_engine, 'identity_provider') - idp_table.drop_column('remote_id') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py index bff6a252..1594f893 100644 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py @@ -29,10 +29,3 @@ def upgrade(migrate_engine): mysql_charset='utf8') sp_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - table = sql.Table('service_provider', meta, autoload=True) - table.drop() diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py index 8a42ce3a..dc18f548 100644 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py @@ -38,11 +38,3 @@ def upgrade(migrate_engine): sp_table.c.auth_url.alter(nullable=False) sp_table.c.sp_url.alter(nullable=False) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - sp_table = sql.Table(_SP_TABLE_NAME, meta, autoload=True) - sp_table.c.auth_url.alter(nullable=True) - sp_table.c.sp_url.alter(nullable=True) diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py new file mode 100644 index 00000000..cd571245 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py @@ -0,0 +1,41 @@ +# 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 sqlalchemy as orm + + +def upgrade(migrate_engine): + meta = orm.MetaData() + meta.bind = migrate_engine + idp_table = orm.Table('identity_provider', meta, autoload=True) + remote_id_table = orm.Table( + 'idp_remote_ids', + meta, + orm.Column('idp_id', + orm.String(64), + orm.ForeignKey('identity_provider.id', + ondelete='CASCADE')), + orm.Column('remote_id', + orm.String(255), + primary_key=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + + remote_id_table.create(migrate_engine, checkfirst=True) + + select = orm.sql.select([idp_table.c.id, idp_table.c.remote_id]) + for identity in migrate_engine.execute(select): + remote_idp_entry = {'idp_id': identity.id, + 'remote_id': identity.remote_id} + remote_id_table.insert(remote_idp_entry).execute() + + idp_table.drop_column('remote_id') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py new file mode 100644 index 00000000..150dcfed --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py @@ -0,0 +1,39 @@ +# 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 oslo_config import cfg +from oslo_db.sqlalchemy import utils +import sqlalchemy as sql + + +CONF = cfg.CONF +_SP_TABLE_NAME = 'service_provider' +_RELAY_STATE_PREFIX = 'relay_state_prefix' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + idp_table = utils.get_table(migrate_engine, _SP_TABLE_NAME) + relay_state_prefix_default = CONF.saml.relay_state_prefix + relay_state_prefix = sql.Column(_RELAY_STATE_PREFIX, sql.String(256), + nullable=False, + server_default=relay_state_prefix_default) + idp_table.create_column(relay_state_prefix) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + idp_table = utils.get_table(migrate_engine, _SP_TABLE_NAME) + idp_table.drop_column(_RELAY_STATE_PREFIX) diff --git a/keystone-moon/keystone/contrib/federation/routers.py b/keystone-moon/keystone/contrib/federation/routers.py index 9a6224b7..d8fa8175 100644 --- a/keystone-moon/keystone/contrib/federation/routers.py +++ b/keystone-moon/keystone/contrib/federation/routers.py @@ -36,44 +36,45 @@ class FederationExtension(wsgi.V3ExtensionRouter): The API looks like:: - PUT /OS-FEDERATION/identity_providers/$identity_provider + PUT /OS-FEDERATION/identity_providers/{idp_id} GET /OS-FEDERATION/identity_providers - GET /OS-FEDERATION/identity_providers/$identity_provider - DELETE /OS-FEDERATION/identity_providers/$identity_provider - PATCH /OS-FEDERATION/identity_providers/$identity_provider + GET /OS-FEDERATION/identity_providers/{idp_id} + DELETE /OS-FEDERATION/identity_providers/{idp_id} + PATCH /OS-FEDERATION/identity_providers/{idp_id} PUT /OS-FEDERATION/identity_providers/ - $identity_provider/protocols/$protocol + {idp_id}/protocols/{protocol_id} GET /OS-FEDERATION/identity_providers/ - $identity_provider/protocols + {idp_id}/protocols GET /OS-FEDERATION/identity_providers/ - $identity_provider/protocols/$protocol + {idp_id}/protocols/{protocol_id} PATCH /OS-FEDERATION/identity_providers/ - $identity_provider/protocols/$protocol + {idp_id}/protocols/{protocol_id} DELETE /OS-FEDERATION/identity_providers/ - $identity_provider/protocols/$protocol + {idp_id}/protocols/{protocol_id} PUT /OS-FEDERATION/mappings GET /OS-FEDERATION/mappings - PATCH /OS-FEDERATION/mappings/$mapping_id - GET /OS-FEDERATION/mappings/$mapping_id - DELETE /OS-FEDERATION/mappings/$mapping_id + PATCH /OS-FEDERATION/mappings/{mapping_id} + GET /OS-FEDERATION/mappings/{mapping_id} + DELETE /OS-FEDERATION/mappings/{mapping_id} GET /OS-FEDERATION/projects GET /OS-FEDERATION/domains - PUT /OS-FEDERATION/service_providers/$service_provider + PUT /OS-FEDERATION/service_providers/{sp_id} GET /OS-FEDERATION/service_providers - GET /OS-FEDERATION/service_providers/$service_provider - DELETE /OS-FEDERATION/service_providers/$service_provider - PATCH /OS-FEDERATION/service_providers/$service_provider + GET /OS-FEDERATION/service_providers/{sp_id} + DELETE /OS-FEDERATION/service_providers/{sp_id} + PATCH /OS-FEDERATION/service_providers/{sp_id} - GET /OS-FEDERATION/identity_providers/$identity_provider/ - protocols/$protocol/auth - POST /OS-FEDERATION/identity_providers/$identity_provider/ - protocols/$protocol/auth + GET /OS-FEDERATION/identity_providers/{identity_provider}/ + protocols/{protocol}/auth + POST /OS-FEDERATION/identity_providers/{identity_provider}/ + protocols/{protocol}/auth POST /auth/OS-FEDERATION/saml2 + POST /auth/OS-FEDERATION/saml2/ecp GET /OS-FEDERATION/saml2/metadata GET /auth/OS-FEDERATION/websso/{protocol_id} @@ -191,6 +192,8 @@ class FederationExtension(wsgi.V3ExtensionRouter): path=self._construct_url('projects'), get_action='list_projects_for_groups', rel=build_resource_relation(resource_name='projects')) + + # Auth operations self._add_resource( mapper, auth_controller, path=self._construct_url('identity_providers/{identity_provider}/' @@ -202,8 +205,6 @@ class FederationExtension(wsgi.V3ExtensionRouter): 'identity_provider': IDP_ID_PARAMETER_RELATION, 'protocol': PROTOCOL_ID_PARAMETER_RELATION, }) - - # Auth operations self._add_resource( mapper, auth_controller, path='/auth' + self._construct_url('saml2'), @@ -211,6 +212,11 @@ class FederationExtension(wsgi.V3ExtensionRouter): rel=build_resource_relation(resource_name='saml2')) self._add_resource( mapper, auth_controller, + path='/auth' + self._construct_url('saml2/ecp'), + post_action='create_ecp_assertion', + rel=build_resource_relation(resource_name='ecp')) + self._add_resource( + mapper, auth_controller, path='/auth' + self._construct_url('websso/{protocol_id}'), get_post_action='federated_sso_auth', rel=build_resource_relation(resource_name='websso'), diff --git a/keystone-moon/keystone/contrib/federation/schema.py b/keystone-moon/keystone/contrib/federation/schema.py index 645e1129..17818a98 100644 --- a/keystone-moon/keystone/contrib/federation/schema.py +++ b/keystone-moon/keystone/contrib/federation/schema.py @@ -58,7 +58,8 @@ _service_provider_properties = { 'auth_url': parameter_types.url, 'sp_url': parameter_types.url, 'description': validation.nullable(parameter_types.description), - 'enabled': parameter_types.boolean + 'enabled': parameter_types.boolean, + 'relay_state_prefix': validation.nullable(parameter_types.description) } service_provider_create = { diff --git a/keystone-moon/keystone/contrib/federation/utils.py b/keystone-moon/keystone/contrib/federation/utils.py index 939fe9a0..b0db3cdd 100644 --- a/keystone-moon/keystone/contrib/federation/utils.py +++ b/keystone-moon/keystone/contrib/federation/utils.py @@ -21,7 +21,6 @@ from oslo_log import log from oslo_utils import timeutils import six -from keystone.contrib import federation from keystone import exception from keystone.i18n import _, _LW @@ -191,14 +190,37 @@ def validate_groups_cardinality(group_ids, mapping_id): raise exception.MissingGroups(mapping_id=mapping_id) -def validate_idp(idp, assertion): - """Check if the IdP providing the assertion is the one registered for - the mapping +def get_remote_id_parameter(protocol): + # NOTE(marco-fargetta): Since we support any protocol ID, we attempt to + # retrieve the remote_id_attribute of the protocol ID. If it's not + # registered in the config, then register the option and try again. + # This allows the user to register protocols other than oidc and saml2. + remote_id_parameter = None + try: + remote_id_parameter = CONF[protocol]['remote_id_attribute'] + except AttributeError: + CONF.register_opt(cfg.StrOpt('remote_id_attribute'), + group=protocol) + try: + remote_id_parameter = CONF[protocol]['remote_id_attribute'] + except AttributeError: + pass + if not remote_id_parameter: + LOG.debug('Cannot find "remote_id_attribute" in configuration ' + 'group %s. Trying default location in ' + 'group federation.', protocol) + remote_id_parameter = CONF.federation.remote_id_attribute + + return remote_id_parameter + + +def validate_idp(idp, protocol, assertion): + """Validate the IdP providing the assertion is registered for the mapping. """ - remote_id_parameter = CONF.federation.remote_id_attribute - if not remote_id_parameter or not idp['remote_id']: - LOG.warning(_LW('Impossible to identify the IdP %s '), - idp['id']) + + remote_id_parameter = get_remote_id_parameter(protocol) + if not remote_id_parameter or not idp['remote_ids']: + LOG.debug('Impossible to identify the IdP %s ', idp['id']) # If nothing is defined, the administrator may want to # allow the mapping of every IdP return @@ -206,10 +228,9 @@ def validate_idp(idp, assertion): idp_remote_identifier = assertion[remote_id_parameter] except KeyError: msg = _('Could not find Identity Provider identifier in ' - 'environment, check [federation] remote_id_attribute ' - 'for details.') + 'environment') raise exception.ValidationError(msg) - if idp_remote_identifier != idp['remote_id']: + if idp_remote_identifier not in idp['remote_ids']: msg = _('Incoming identity provider identifier not included ' 'among the accepted identifiers.') raise exception.Forbidden(msg) @@ -265,7 +286,7 @@ def validate_groups(group_ids, mapping_id, identity_api): # TODO(marek-denis): Optimize this function, so the number of calls to the # backend are minimized. def transform_to_group_ids(group_names, mapping_id, - identity_api, assignment_api): + identity_api, resource_api): """Transform groups identitified by name/domain to their ids Function accepts list of groups identified by a name and domain giving @@ -296,7 +317,7 @@ def transform_to_group_ids(group_names, mapping_id, :type mapping_id: str :param identity_api: identity_api object - :param assignment_api: assignment_api object + :param resource_api: resource manager object :returns: generator object with group ids @@ -317,7 +338,7 @@ def transform_to_group_ids(group_names, mapping_id, """ domain_id = (domain.get('id') or - assignment_api.get_domain_by_name( + resource_api.get_domain_by_name( domain.get('name')).get('id')) return domain_id @@ -334,7 +355,7 @@ def transform_to_group_ids(group_names, mapping_id, def get_assertion_params_from_env(context): LOG.debug('Environment variables: %s', context['environment']) prefix = CONF.federation.assertion_prefix - for k, v in context['environment'].items(): + for k, v in list(context['environment'].items()): if k.startswith(prefix): yield (k, v) @@ -487,8 +508,8 @@ class RuleProcessor(object): """ def extract_groups(groups_by_domain): - for groups in groups_by_domain.values(): - for group in {g['name']: g for g in groups}.values(): + for groups in list(groups_by_domain.values()): + for group in list({g['name']: g for g in groups}.values()): yield group def normalize_user(user): @@ -506,8 +527,7 @@ class RuleProcessor(object): if user_type == UserType.EPHEMERAL: user['domain'] = { - 'id': (CONF.federation.federated_domain_name or - federation.FEDERATED_DOMAIN_KEYWORD) + 'id': CONF.federation.federated_domain_name } # initialize the group_ids as a set to eliminate duplicates @@ -586,7 +606,7 @@ class RuleProcessor(object): LOG.debug('direct_maps: %s', direct_maps) LOG.debug('local: %s', local) new = {} - for k, v in six.iteritems(local): + for k, v in local.items(): if isinstance(v, dict): new_value = self._update_local_mapping(v, direct_maps) else: @@ -644,7 +664,7 @@ class RuleProcessor(object): } :returns: identity values used to update local - :rtype: keystone.contrib.federation.utils.DirectMaps + :rtype: keystone.contrib.federation.utils.DirectMaps or None """ @@ -686,10 +706,10 @@ class RuleProcessor(object): # If a blacklist or whitelist is used, we want to map to the # whole list instead of just its values separately. - if blacklisted_values: + if blacklisted_values is not None: direct_map_values = [v for v in direct_map_values if v not in blacklisted_values] - elif whitelisted_values: + elif whitelisted_values is not None: direct_map_values = [v for v in direct_map_values if v in whitelisted_values] diff --git a/keystone-moon/keystone/contrib/moon/core.py b/keystone-moon/keystone/contrib/moon/core.py index a1255fe2..4a68cdaa 100644 --- a/keystone-moon/keystone/contrib/moon/core.py +++ b/keystone-moon/keystone/contrib/moon/core.py @@ -258,6 +258,8 @@ def enforce(action_names, object_name, **extra): @dependency.requires('moonlog_api', 'admin_api', 'tenant_api', 'root_api') class ConfigurationManager(manager.Manager): + driver_namespace = 'keystone.moon.configuration' + def __init__(self): super(ConfigurationManager, self).__init__(CONF.moon.configuration_driver) @@ -326,6 +328,8 @@ class ConfigurationManager(manager.Manager): @dependency.requires('moonlog_api', 'admin_api', 'configuration_api', 'root_api', 'resource_api') class TenantManager(manager.Manager): + driver_namespace = 'keystone.moon.tenant' + def __init__(self): super(TenantManager, self).__init__(CONF.moon.tenant_driver) @@ -452,6 +456,8 @@ class TenantManager(manager.Manager): @dependency.requires('identity_api', 'tenant_api', 'configuration_api', 'authz_api', 'admin_api', 'moonlog_api', 'root_api') class IntraExtensionManager(manager.Manager): + driver_namespace = 'keystone.moon.intraextension' + def __init__(self): driver = CONF.moon.intraextension_driver super(IntraExtensionManager, self).__init__(driver) @@ -2065,6 +2071,8 @@ class IntraExtensionRootManager(IntraExtensionManager): @dependency.requires('identity_api', 'tenant_api', 'configuration_api', 'authz_api', 'admin_api', 'root_api') class LogManager(manager.Manager): + driver_namespace = 'keystone.moon.log' + def __init__(self): driver = CONF.moon.log_driver super(LogManager, self).__init__(driver) diff --git a/keystone-moon/keystone/contrib/moon/routers.py b/keystone-moon/keystone/contrib/moon/routers.py index 4da3b991..63915092 100644 --- a/keystone-moon/keystone/contrib/moon/routers.py +++ b/keystone-moon/keystone/contrib/moon/routers.py @@ -9,7 +9,7 @@ from keystone.contrib.moon import controllers from keystone.common import wsgi -class Routers(wsgi.RoutersBase): +class Routers(wsgi.V3ExtensionRouter): """API Endpoints for the Moon extension. """ diff --git a/keystone-moon/keystone/contrib/oauth1/backends/sql.py b/keystone-moon/keystone/contrib/oauth1/backends/sql.py index c6ab6e5a..a7876756 100644 --- a/keystone-moon/keystone/contrib/oauth1/backends/sql.py +++ b/keystone-moon/keystone/contrib/oauth1/backends/sql.py @@ -18,9 +18,9 @@ import uuid from oslo_serialization import jsonutils from oslo_utils import timeutils -import six from keystone.common import sql +from keystone.common import utils from keystone.contrib.oauth1 import core from keystone import exception from keystone.i18n import _ @@ -58,7 +58,7 @@ class RequestToken(sql.ModelBase, sql.DictBase): return cls(**user_dict) def to_dict(self): - return dict(six.iteritems(self)) + return dict(self.items()) class AccessToken(sql.ModelBase, sql.DictBase): @@ -81,7 +81,7 @@ class AccessToken(sql.ModelBase, sql.DictBase): return cls(**user_dict) def to_dict(self): - return dict(six.iteritems(self)) + return dict(self.items()) class OAuth1(object): @@ -163,7 +163,7 @@ class OAuth1(object): if token_duration: now = timeutils.utcnow() future = now + datetime.timedelta(seconds=token_duration) - expiry_date = timeutils.isotime(future, subsecond=True) + expiry_date = utils.isotime(future, subsecond=True) ref = {} ref['id'] = request_token_id @@ -225,7 +225,7 @@ class OAuth1(object): if token_duration: now = timeutils.utcnow() future = now + datetime.timedelta(seconds=token_duration) - expiry_date = timeutils.isotime(future, subsecond=True) + expiry_date = utils.isotime(future, subsecond=True) # add Access Token ref = {} diff --git a/keystone-moon/keystone/contrib/oauth1/controllers.py b/keystone-moon/keystone/contrib/oauth1/controllers.py index fb5d0bc2..d12fc96b 100644 --- a/keystone-moon/keystone/contrib/oauth1/controllers.py +++ b/keystone-moon/keystone/contrib/oauth1/controllers.py @@ -20,12 +20,12 @@ from oslo_utils import timeutils from keystone.common import controller from keystone.common import dependency +from keystone.common import utils from keystone.common import wsgi from keystone.contrib.oauth1 import core as oauth1 from keystone.contrib.oauth1 import validator from keystone import exception from keystone.i18n import _ -from keystone.models import token_model from keystone import notifications @@ -84,10 +84,7 @@ class ConsumerCrudV3(controller.V3Controller): @controller.protected() def delete_consumer(self, context, consumer_id): - user_token_ref = token_model.KeystoneToken( - token_id=context['token_id'], - token_data=self.token_provider_api.validate_token( - context['token_id'])) + user_token_ref = utils.get_token_ref(context) payload = {'user_id': user_token_ref.user_id, 'consumer_id': consumer_id} _emit_user_oauth_consumer_token_invalidate(payload) @@ -382,10 +379,7 @@ class OAuthControllerV3(controller.V3Controller): authed_roles.add(role['id']) # verify the authorizing user has the roles - user_token = token_model.KeystoneToken( - token_id=context['token_id'], - token_data=self.token_provider_api.validate_token( - context['token_id'])) + user_token = utils.get_token_ref(context) user_id = user_token.user_id project_id = req_token['requested_project_id'] user_roles = self.assignment_api.get_roles_for_user_and_project( diff --git a/keystone-moon/keystone/contrib/oauth1/core.py b/keystone-moon/keystone/contrib/oauth1/core.py index eeb3e114..d7f64dc4 100644 --- a/keystone-moon/keystone/contrib/oauth1/core.py +++ b/keystone-moon/keystone/contrib/oauth1/core.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Extensions supporting OAuth1.""" +"""Main entry point into the OAuth1 service.""" from __future__ import absolute_import @@ -151,6 +151,9 @@ class Manager(manager.Manager): dynamically calls the backend. """ + + driver_namespace = 'keystone.oauth1' + _ACCESS_TOKEN = "OS-OAUTH1:access_token" _REQUEST_TOKEN = "OS-OAUTH1:request_token" _CONSUMER = "OS-OAUTH1:consumer" diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py index a4fbf155..e0305351 100644 --- a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py @@ -55,13 +55,3 @@ def upgrade(migrate_engine): sql.Column('consumer_id', sql.String(64), nullable=False), sql.Column('expires_at', sql.String(64), nullable=True)) access_token_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. - tables = ['consumer', 'request_token', 'access_token'] - for table_name in tables: - table = sql.Table(table_name, meta, autoload=True) - table.drop() diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py index d39df8d5..174120e8 100644 --- a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py @@ -35,20 +35,3 @@ def upgrade(migrate_engine): 'ref_column': consumer_table.c.id}] if meta.bind != 'sqlite': migration_helpers.add_constraints(constraints) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - consumer_table = sql.Table('consumer', meta, autoload=True) - request_token_table = sql.Table('request_token', meta, autoload=True) - access_token_table = sql.Table('access_token', meta, autoload=True) - - constraints = [{'table': request_token_table, - 'fk_column': 'consumer_id', - 'ref_column': consumer_table.c.id}, - {'table': access_token_table, - 'fk_column': 'consumer_id', - 'ref_column': consumer_table.c.id}] - if migrate_engine.name != 'sqlite': - migration_helpers.remove_constraints(constraints) diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py index e1cf8843..cf6ffb7c 100644 --- a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py @@ -20,10 +20,3 @@ def upgrade(migrate_engine): meta.bind = migrate_engine user_table = sql.Table('consumer', meta, autoload=True) user_table.c.description.alter(nullable=True) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - user_table = sql.Table('consumer', meta, autoload=True) - user_table.c.description.alter(nullable=False) diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py index 6f1e2e81..6934eb6f 100644 --- a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py @@ -23,13 +23,3 @@ def upgrade(migrate_engine): request_token_table.c.requested_roles.alter(name="role_ids") access_token_table = sql.Table('access_token', meta, autoload=True) access_token_table.c.requested_roles.alter(name="role_ids") - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - request_token_table = sql.Table('request_token', meta, autoload=True) - request_token_table.c.role_ids.alter(nullable=False) - request_token_table.c.role_ids.alter(name="requested_roles") - access_token_table = sql.Table('access_token', meta, autoload=True) - access_token_table.c.role_ids.alter(name="requested_roles") diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py index 428971f8..0627d21c 100644 --- a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py @@ -26,17 +26,10 @@ def upgrade(migrate_engine): # indexes create automatically. That those indexes will have different # names, depending on version of MySQL used. We shoud make this naming # consistent, by reverting index name to a consistent condition. - if any(i for i in table.indexes if i.columns.keys() == ['consumer_id'] + if any(i for i in table.indexes if + list(i.columns.keys()) == ['consumer_id'] and i.name != 'consumer_id'): # NOTE(i159): by this action will be made re-creation of an index # with the new name. This can be considered as renaming under the # MySQL rules. sa.Index('consumer_id', table.c.consumer_id).create() - - -def downgrade(migrate_engine): - # NOTE(i159): index exists only in MySQL schemas, and got an inconsistent - # name only when MySQL 5.5 renamed it after re-creation - # (during migrations). So we just fixed inconsistency, there is no - # necessity to revert it. - pass diff --git a/keystone-moon/keystone/contrib/oauth1/routers.py b/keystone-moon/keystone/contrib/oauth1/routers.py index 35619ede..4b772eb5 100644 --- a/keystone-moon/keystone/contrib/oauth1/routers.py +++ b/keystone-moon/keystone/contrib/oauth1/routers.py @@ -44,17 +44,17 @@ class OAuth1Extension(wsgi.V3ExtensionRouter): # Basic admin-only consumer crud POST /OS-OAUTH1/consumers GET /OS-OAUTH1/consumers - PATCH /OS-OAUTH1/consumers/$consumer_id - GET /OS-OAUTH1/consumers/$consumer_id - DELETE /OS-OAUTH1/consumers/$consumer_id + PATCH /OS-OAUTH1/consumers/{consumer_id} + GET /OS-OAUTH1/consumers/{consumer_id} + DELETE /OS-OAUTH1/consumers/{consumer_id} # User access token crud - GET /users/$user_id/OS-OAUTH1/access_tokens - GET /users/$user_id/OS-OAUTH1/access_tokens/$access_token_id + GET /users/{user_id}/OS-OAUTH1/access_tokens + GET /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id} GET /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/roles GET /users/{user_id}/OS-OAUTH1/access_tokens /{access_token_id}/roles/{role_id} - DELETE /users/$user_id/OS-OAUTH1/access_tokens/$access_token_id + DELETE /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id} # OAuth interfaces POST /OS-OAUTH1/request_token # create a request token diff --git a/keystone-moon/keystone/contrib/revoke/backends/kvs.py b/keystone-moon/keystone/contrib/revoke/backends/kvs.py index cc41fbee..349ed6e3 100644 --- a/keystone-moon/keystone/contrib/revoke/backends/kvs.py +++ b/keystone-moon/keystone/contrib/revoke/backends/kvs.py @@ -13,12 +13,12 @@ import datetime from oslo_config import cfg +from oslo_log import versionutils from oslo_utils import timeutils from keystone.common import kvs from keystone.contrib import revoke from keystone import exception -from keystone.openstack.common import versionutils CONF = cfg.CONF @@ -45,29 +45,30 @@ class Revoke(revoke.Driver): except exception.NotFound: return [] - def _prune_expired_events_and_get(self, last_fetch=None, new_event=None): - pruned = [] + def list_events(self, last_fetch=None): results = [] + + with self._store.get_lock(_EVENT_KEY): + events = self._list_events() + + for event in events: + revoked_at = event.revoked_at + if last_fetch is None or revoked_at > last_fetch: + results.append(event) + return results + + def revoke(self, event): + pruned = [] expire_delta = datetime.timedelta(seconds=CONF.token.expiration) oldest = timeutils.utcnow() - expire_delta - # TODO(ayoung): Store the time of the oldest event so that the - # prune process can be skipped if none of the events have timed out. + with self._store.get_lock(_EVENT_KEY) as lock: events = self._list_events() - if new_event is not None: - events.append(new_event) + if event: + events.append(event) for event in events: revoked_at = event.revoked_at if revoked_at > oldest: pruned.append(event) - if last_fetch is None or revoked_at > last_fetch: - results.append(event) self._store.set(_EVENT_KEY, pruned, lock) - return results - - def list_events(self, last_fetch=None): - return self._prune_expired_events_and_get(last_fetch=last_fetch) - - def revoke(self, event): - self._prune_expired_events_and_get(new_event=event) diff --git a/keystone-moon/keystone/contrib/revoke/backends/sql.py b/keystone-moon/keystone/contrib/revoke/backends/sql.py index 1b0cde1e..dd7fdd19 100644 --- a/keystone-moon/keystone/contrib/revoke/backends/sql.py +++ b/keystone-moon/keystone/contrib/revoke/backends/sql.py @@ -33,7 +33,7 @@ class RevocationEvent(sql.ModelBase, sql.ModelDictMixin): access_token_id = sql.Column(sql.String(64)) issued_before = sql.Column(sql.DateTime(), nullable=False) expires_at = sql.Column(sql.DateTime()) - revoked_at = sql.Column(sql.DateTime(), nullable=False) + revoked_at = sql.Column(sql.DateTime(), nullable=False, index=True) audit_id = sql.Column(sql.String(32)) audit_chain_id = sql.Column(sql.String(32)) @@ -81,7 +81,6 @@ class Revoke(revoke.Driver): session.flush() def list_events(self, last_fetch=None): - self._prune_expired_events() session = sql.get_session() query = session.query(RevocationEvent).order_by( RevocationEvent.revoked_at) @@ -102,3 +101,4 @@ class Revoke(revoke.Driver): session = sql.get_session() with session.begin(): session.add(record) + self._prune_expired_events() diff --git a/keystone-moon/keystone/contrib/revoke/core.py b/keystone-moon/keystone/contrib/revoke/core.py index c7335690..e1ab87c8 100644 --- a/keystone-moon/keystone/contrib/revoke/core.py +++ b/keystone-moon/keystone/contrib/revoke/core.py @@ -10,11 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +"""Main entry point into the Revoke service.""" + import abc import datetime from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_utils import timeutils import six @@ -26,7 +29,6 @@ from keystone.contrib.revoke import model from keystone import exception from keystone.i18n import _ from keystone import notifications -from keystone.openstack.common import versionutils CONF = cfg.CONF @@ -64,12 +66,17 @@ def revoked_before_cutoff_time(): @dependency.provider('revoke_api') class Manager(manager.Manager): - """Revoke API Manager. + """Default pivot point for the Revoke backend. Performs common logic for recording revocations. + See :mod:`keystone.common.manager.Manager` for more details on + how this dynamically calls the backend. + """ + driver_namespace = 'keystone.revoke' + def __init__(self): super(Manager, self).__init__(CONF.revoke.driver) self._register_listeners() @@ -109,11 +116,12 @@ class Manager(manager.Manager): self.revoke( model.RevokeEvent(access_token_id=payload['resource_info'])) - def _group_callback(self, service, resource_type, operation, payload): - user_ids = (u['id'] for u in self.identity_api.list_users_in_group( - payload['resource_info'])) - for uid in user_ids: - self.revoke(model.RevokeEvent(user_id=uid)) + def _role_assignment_callback(self, service, resource_type, operation, + payload): + info = payload['resource_info'] + self.revoke_by_grant(role_id=info['role_id'], user_id=info['user_id'], + domain_id=info.get('domain_id'), + project_id=info.get('project_id')) def _register_listeners(self): callbacks = { @@ -124,6 +132,7 @@ class Manager(manager.Manager): ['role', self._role_callback], ['user', self._user_callback], ['project', self._project_callback], + ['role_assignment', self._role_assignment_callback] ], notifications.ACTIONS.disabled: [ ['user', self._user_callback], @@ -136,7 +145,7 @@ class Manager(manager.Manager): ] } - for event, cb_info in six.iteritems(callbacks): + for event, cb_info in callbacks.items(): for resource_type, callback_fns in cb_info: notifications.register_event_callback(event, resource_type, callback_fns) diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py index 7927ce0c..8b59010e 100644 --- a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py +++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py @@ -34,14 +34,3 @@ def upgrade(migrate_engine): sql.Column('expires_at', sql.DateTime()), sql.Column('revoked_at', sql.DateTime(), index=True, nullable=False)) service_table.create(migrate_engine, checkfirst=True) - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - meta = sql.MetaData() - meta.bind = migrate_engine - - tables = ['revocation_event'] - for t in tables: - table = sql.Table(t, meta, autoload=True) - table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py index bee6fb2a..b6d821d7 100644 --- a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py +++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py @@ -26,12 +26,3 @@ def upgrade(migrate_engine): nullable=True) event_table.create_column(audit_id_column) event_table.create_column(audit_chain_column) - - -def downgrade(migrate_engine): - meta = sql.MetaData() - meta.bind = migrate_engine - - event_table = sql.Table(_TABLE_NAME, meta, autoload=True) - event_table.drop_column('audit_id') - event_table.drop_column('audit_chain_id') diff --git a/keystone-moon/keystone/contrib/revoke/model.py b/keystone-moon/keystone/contrib/revoke/model.py index 5e92042d..1a23d57d 100644 --- a/keystone-moon/keystone/contrib/revoke/model.py +++ b/keystone-moon/keystone/contrib/revoke/model.py @@ -11,6 +11,9 @@ # under the License. from oslo_utils import timeutils +from six.moves import map + +from keystone.common import utils # The set of attributes common between the RevokeEvent @@ -43,6 +46,15 @@ _TOKEN_KEYS = ['identity_domain_id', 'trustor_id', 'trustee_id'] +# Alternative names to be checked in token for every field in +# revoke tree. +ALTERNATIVES = { + 'user_id': ['user_id', 'trustor_id', 'trustee_id'], + 'domain_id': ['identity_domain_id', 'assignment_domain_id'], + # For a domain-scoped token, the domain is in assignment_domain_id. + 'domain_scope_id': ['assignment_domain_id', ], +} + REVOKE_KEYS = _NAMES + _EVENT_ARGS @@ -100,10 +112,10 @@ class RevokeEvent(object): if self.consumer_id is not None: event['OS-OAUTH1:access_token_id'] = self.access_token_id if self.expires_at is not None: - event['expires_at'] = timeutils.isotime(self.expires_at) + event['expires_at'] = utils.isotime(self.expires_at) if self.issued_before is not None: - event['issued_before'] = timeutils.isotime(self.issued_before, - subsecond=True) + event['issued_before'] = utils.isotime(self.issued_before, + subsecond=True) return event def key_for_name(self, name): @@ -111,7 +123,7 @@ class RevokeEvent(object): def attr_keys(event): - return map(event.key_for_name, _EVENT_NAMES) + return list(map(event.key_for_name, _EVENT_NAMES)) class RevokeTree(object): @@ -176,7 +188,52 @@ class RevokeTree(object): del parent[key] def add_events(self, revoke_events): - return map(self.add_event, revoke_events or []) + return list(map(self.add_event, revoke_events or [])) + + @staticmethod + def _next_level_keys(name, token_data): + """Generate keys based on current field name and token data + + Generate all keys to look for in the next iteration of revocation + event tree traversal. + """ + yield '*' + if name == 'role_id': + # Roles are very special since a token has a list of them. + # If the revocation event matches any one of them, + # revoke the token. + for role_id in token_data.get('roles', []): + yield role_id + else: + # For other fields we try to get any branch that concur + # with any alternative field in the token. + for alt_name in ALTERNATIVES.get(name, [name]): + yield token_data[alt_name] + + def _search(self, revoke_map, names, token_data): + """Search for revocation event by token_data + + Traverse the revocation events tree looking for event matching token + data issued after the token. + """ + if not names: + # The last (leaf) level is checked in a special way because we + # verify issued_at field differently. + try: + return revoke_map['issued_before'] > token_data['issued_at'] + except KeyError: + return False + + name, remaining_names = names[0], names[1:] + + for key in self._next_level_keys(name, token_data): + subtree = revoke_map.get('%s=%s' % (name, key)) + if subtree and self._search(subtree, remaining_names, token_data): + return True + + # If we made it out of the loop then no element in revocation tree + # corresponds to our token and it is good. + return False def is_revoked(self, token_data): """Check if a token matches the revocation event @@ -195,58 +252,7 @@ class RevokeTree(object): 'consumer_id', 'access_token_id' """ - # Alternative names to be checked in token for every field in - # revoke tree. - alternatives = { - 'user_id': ['user_id', 'trustor_id', 'trustee_id'], - 'domain_id': ['identity_domain_id', 'assignment_domain_id'], - # For a domain-scoped token, the domain is in assignment_domain_id. - 'domain_scope_id': ['assignment_domain_id', ], - } - # Contains current forest (collection of trees) to be checked. - partial_matches = [self.revoke_map] - # We iterate over every layer of our revoke tree (except the last one). - for name in _EVENT_NAMES: - # bundle is the set of partial matches for the next level down - # the tree - bundle = [] - wildcard = '%s=*' % (name,) - # For every tree in current forest. - for tree in partial_matches: - # If there is wildcard node on current level we take it. - bundle.append(tree.get(wildcard)) - if name == 'role_id': - # Roles are very special since a token has a list of them. - # If the revocation event matches any one of them, - # revoke the token. - for role_id in token_data.get('roles', []): - bundle.append(tree.get('role_id=%s' % role_id)) - else: - # For other fields we try to get any branch that concur - # with any alternative field in the token. - for alt_name in alternatives.get(name, [name]): - bundle.append( - tree.get('%s=%s' % (name, token_data[alt_name]))) - # tree.get returns `None` if there is no match, so `bundle.append` - # adds a 'None' entry. This call remoes the `None` entries. - partial_matches = [x for x in bundle if x is not None] - if not partial_matches: - # If we end up with no branches to follow means that the token - # is definitely not in the revoke tree and all further - # iterations will be for nothing. - return False - - # The last (leaf) level is checked in a special way because we verify - # issued_at field differently. - for leaf in partial_matches: - try: - if leaf['issued_before'] > token_data['issued_at']: - return True - except KeyError: - pass - # If we made it out of the loop then no element in revocation tree - # corresponds to our token and it is good. - return False + return self._search(self.revoke_map, _EVENT_NAMES, token_data) def build_token_values_v2(access, default_domain_id): diff --git a/keystone-moon/keystone/contrib/s3/core.py b/keystone-moon/keystone/contrib/s3/core.py index 34095bf4..d3e06acc 100644 --- a/keystone-moon/keystone/contrib/s3/core.py +++ b/keystone-moon/keystone/contrib/s3/core.py @@ -25,6 +25,8 @@ import base64 import hashlib import hmac +import six + from keystone.common import extension from keystone.common import json_home from keystone.common import utils @@ -32,6 +34,7 @@ from keystone.common import wsgi from keystone.contrib.ec2 import controllers from keystone import exception + EXTENSION_DATA = { 'name': 'OpenStack S3 API', 'namespace': 'http://docs.openstack.org/identity/api/ext/' @@ -65,9 +68,15 @@ class S3Extension(wsgi.V3ExtensionRouter): class S3Controller(controllers.Ec2Controller): def check_signature(self, creds_ref, credentials): msg = base64.urlsafe_b64decode(str(credentials['token'])) - key = str(creds_ref['secret']) - signed = base64.encodestring( - hmac.new(key, msg, hashlib.sha1).digest()).strip() + key = str(creds_ref['secret']).encode('utf-8') + + if six.PY2: + b64_encode = base64.encodestring + else: + b64_encode = base64.encodebytes + + signed = b64_encode( + hmac.new(key, msg, hashlib.sha1).digest()).decode('utf-8').strip() if not utils.auth_str_equal(credentials['signature'], signed): raise exception.Unauthorized('Credential signature mismatch') diff --git a/keystone-moon/keystone/controllers.py b/keystone-moon/keystone/controllers.py index 12f13c77..085c1fb0 100644 --- a/keystone-moon/keystone/controllers.py +++ b/keystone-moon/keystone/controllers.py @@ -63,7 +63,7 @@ class Extensions(wsgi.Application): return None def get_extensions_info(self, context): - return {'extensions': {'values': self.extensions.values()}} + return {'extensions': {'values': list(self.extensions.values())}} def get_extension_info(self, context, extension_alias): try: @@ -146,9 +146,9 @@ class Version(wsgi.Application): if 'v3' in _VERSIONS: versions['v3'] = { - 'id': 'v3.0', + 'id': 'v3.4', 'status': 'stable', - 'updated': '2013-03-06T00:00:00Z', + 'updated': '2015-03-30T00:00:00Z', 'links': [ { 'rel': 'self', @@ -177,7 +177,7 @@ class Version(wsgi.Application): versions = self._get_versions_list(context) return wsgi.render_response(status=(300, 'Multiple Choices'), body={ 'versions': { - 'values': versions.values() + 'values': list(versions.values()) } }) diff --git a/keystone-moon/keystone/credential/core.py b/keystone-moon/keystone/credential/core.py index d3354ea3..2368439e 100644 --- a/keystone-moon/keystone/credential/core.py +++ b/keystone-moon/keystone/credential/core.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Main entry point into the Credentials service.""" +"""Main entry point into the Credential service.""" import abc @@ -40,6 +40,8 @@ class Manager(manager.Manager): """ + driver_namespace = 'keystone.credential' + def __init__(self): super(Manager, self).__init__(CONF.credential.driver) diff --git a/keystone-moon/keystone/endpoint_policy/__init__.py b/keystone-moon/keystone/endpoint_policy/__init__.py new file mode 100644 index 00000000..c8ae5e68 --- /dev/null +++ b/keystone-moon/keystone/endpoint_policy/__init__.py @@ -0,0 +1,14 @@ +# 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.endpoint_policy.core import * # noqa +from keystone.endpoint_policy import routers # noqa diff --git a/keystone-moon/keystone/endpoint_policy/backends/__init__.py b/keystone-moon/keystone/endpoint_policy/backends/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/endpoint_policy/backends/__init__.py diff --git a/keystone-moon/keystone/endpoint_policy/backends/sql.py b/keystone-moon/keystone/endpoint_policy/backends/sql.py new file mode 100644 index 00000000..484444f1 --- /dev/null +++ b/keystone-moon/keystone/endpoint_policy/backends/sql.py @@ -0,0 +1,140 @@ +# Copyright 2014 IBM Corp. +# +# 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 uuid + +import sqlalchemy + +from keystone.common import sql +from keystone import exception + + +class PolicyAssociation(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'policy_association' + attributes = ['policy_id', 'endpoint_id', 'region_id', 'service_id'] + # The id column is never exposed outside this module. It only exists to + # provide a primary key, given that the real columns we would like to use + # (endpoint_id, service_id, region_id) can be null + id = sql.Column(sql.String(64), primary_key=True) + policy_id = sql.Column(sql.String(64), nullable=False) + endpoint_id = sql.Column(sql.String(64), nullable=True) + service_id = sql.Column(sql.String(64), nullable=True) + region_id = sql.Column(sql.String(64), nullable=True) + __table_args__ = (sql.UniqueConstraint('endpoint_id', 'service_id', + 'region_id'), {}) + + def to_dict(self): + """Returns the model's attributes as a dictionary. + + We override the standard method in order to hide the id column, + since this only exists to provide the table with a primary key. + + """ + d = {} + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class EndpointPolicy(object): + + def create_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + with sql.transaction() as session: + try: + # See if there is already a row for this association, and if + # so, update it with the new policy_id + query = session.query(PolicyAssociation) + query = query.filter_by(endpoint_id=endpoint_id) + query = query.filter_by(service_id=service_id) + query = query.filter_by(region_id=region_id) + association = query.one() + association.policy_id = policy_id + except sql.NotFound: + association = PolicyAssociation(id=uuid.uuid4().hex, + policy_id=policy_id, + endpoint_id=endpoint_id, + service_id=service_id, + region_id=region_id) + session.add(association) + + def check_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + sql_constraints = sqlalchemy.and_( + PolicyAssociation.policy_id == policy_id, + PolicyAssociation.endpoint_id == endpoint_id, + PolicyAssociation.service_id == service_id, + PolicyAssociation.region_id == region_id) + + # NOTE(henry-nash): Getting a single value to save object + # management overhead. + with sql.transaction() as session: + if session.query(PolicyAssociation.id).filter( + sql_constraints).distinct().count() == 0: + raise exception.PolicyAssociationNotFound() + + def delete_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(policy_id=policy_id) + query = query.filter_by(endpoint_id=endpoint_id) + query = query.filter_by(service_id=service_id) + query = query.filter_by(region_id=region_id) + query.delete() + + def get_policy_association(self, endpoint_id=None, + service_id=None, region_id=None): + sql_constraints = sqlalchemy.and_( + PolicyAssociation.endpoint_id == endpoint_id, + PolicyAssociation.service_id == service_id, + PolicyAssociation.region_id == region_id) + + try: + with sql.transaction() as session: + policy_id = session.query(PolicyAssociation.policy_id).filter( + sql_constraints).distinct().one() + return {'policy_id': policy_id} + except sql.NotFound: + raise exception.PolicyAssociationNotFound() + + def list_associations_for_policy(self, policy_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(policy_id=policy_id) + return [ref.to_dict() for ref in query.all()] + + def delete_association_by_endpoint(self, endpoint_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(endpoint_id=endpoint_id) + query.delete() + + def delete_association_by_service(self, service_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(service_id=service_id) + query.delete() + + def delete_association_by_region(self, region_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(region_id=region_id) + query.delete() + + def delete_association_by_policy(self, policy_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(policy_id=policy_id) + query.delete() diff --git a/keystone-moon/keystone/endpoint_policy/controllers.py b/keystone-moon/keystone/endpoint_policy/controllers.py new file mode 100644 index 00000000..b96834dc --- /dev/null +++ b/keystone-moon/keystone/endpoint_policy/controllers.py @@ -0,0 +1,166 @@ +# Copyright 2014 IBM Corp. +# +# 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 controller +from keystone.common import dependency +from keystone import notifications + + +@dependency.requires('policy_api', 'catalog_api', 'endpoint_policy_api') +class EndpointPolicyV3Controller(controller.V3Controller): + collection_name = 'endpoints' + member_name = 'endpoint' + + def __init__(self): + super(EndpointPolicyV3Controller, self).__init__() + notifications.register_event_callback( + 'deleted', 'endpoint', self._on_endpoint_delete) + notifications.register_event_callback( + 'deleted', 'service', self._on_service_delete) + notifications.register_event_callback( + 'deleted', 'region', self._on_region_delete) + notifications.register_event_callback( + 'deleted', 'policy', self._on_policy_delete) + + def _on_endpoint_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_endpoint( + payload['resource_info']) + + def _on_service_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_service( + payload['resource_info']) + + def _on_region_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_region( + payload['resource_info']) + + def _on_policy_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_policy( + payload['resource_info']) + + @controller.protected() + def create_policy_association_for_endpoint(self, context, + policy_id, endpoint_id): + """Create an association between a policy and an endpoint.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_endpoint(endpoint_id) + self.endpoint_policy_api.create_policy_association( + policy_id, endpoint_id=endpoint_id) + + @controller.protected() + def check_policy_association_for_endpoint(self, context, + policy_id, endpoint_id): + """Check an association between a policy and an endpoint.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_endpoint(endpoint_id) + self.endpoint_policy_api.check_policy_association( + policy_id, endpoint_id=endpoint_id) + + @controller.protected() + def delete_policy_association_for_endpoint(self, context, + policy_id, endpoint_id): + """Delete an association between a policy and an endpoint.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_endpoint(endpoint_id) + self.endpoint_policy_api.delete_policy_association( + policy_id, endpoint_id=endpoint_id) + + @controller.protected() + def create_policy_association_for_service(self, context, + policy_id, service_id): + """Create an association between a policy and a service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.endpoint_policy_api.create_policy_association( + policy_id, service_id=service_id) + + @controller.protected() + def check_policy_association_for_service(self, context, + policy_id, service_id): + """Check an association between a policy and a service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.endpoint_policy_api.check_policy_association( + policy_id, service_id=service_id) + + @controller.protected() + def delete_policy_association_for_service(self, context, + policy_id, service_id): + """Delete an association between a policy and a service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.endpoint_policy_api.delete_policy_association( + policy_id, service_id=service_id) + + @controller.protected() + def create_policy_association_for_region_and_service( + self, context, policy_id, service_id, region_id): + """Create an association between a policy and region+service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.catalog_api.get_region(region_id) + self.endpoint_policy_api.create_policy_association( + policy_id, service_id=service_id, region_id=region_id) + + @controller.protected() + def check_policy_association_for_region_and_service( + self, context, policy_id, service_id, region_id): + """Check an association between a policy and region+service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.catalog_api.get_region(region_id) + self.endpoint_policy_api.check_policy_association( + policy_id, service_id=service_id, region_id=region_id) + + @controller.protected() + def delete_policy_association_for_region_and_service( + self, context, policy_id, service_id, region_id): + """Delete an association between a policy and region+service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.catalog_api.get_region(region_id) + self.endpoint_policy_api.delete_policy_association( + policy_id, service_id=service_id, region_id=region_id) + + @controller.protected() + def get_policy_for_endpoint(self, context, endpoint_id): + """Get the effective policy for an endpoint.""" + self.catalog_api.get_endpoint(endpoint_id) + ref = self.endpoint_policy_api.get_policy_for_endpoint(endpoint_id) + # NOTE(henry-nash): since the collection and member for this class is + # set to endpoints, we have to handle wrapping this policy entity + # ourselves. + self._add_self_referential_link(context, ref) + return {'policy': ref} + + # NOTE(henry-nash): As in the catalog controller, we must ensure that the + # legacy_endpoint_id does not escape. + + @classmethod + def filter_endpoint(cls, ref): + if 'legacy_endpoint_id' in ref: + ref.pop('legacy_endpoint_id') + return ref + + @classmethod + def wrap_member(cls, context, ref): + ref = cls.filter_endpoint(ref) + return super(EndpointPolicyV3Controller, cls).wrap_member(context, ref) + + @controller.protected() + def list_endpoints_for_policy(self, context, policy_id): + """List endpoints with the effective association to a policy.""" + self.policy_api.get_policy(policy_id) + refs = self.endpoint_policy_api.list_endpoints_for_policy(policy_id) + return EndpointPolicyV3Controller.wrap_collection(context, refs) diff --git a/keystone-moon/keystone/endpoint_policy/core.py b/keystone-moon/keystone/endpoint_policy/core.py new file mode 100644 index 00000000..3e8026e6 --- /dev/null +++ b/keystone-moon/keystone/endpoint_policy/core.py @@ -0,0 +1,433 @@ +# Copyright 2014 IBM Corp. +# +# 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 abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone.common import manager +from keystone import exception +from keystone.i18n import _, _LE, _LW + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@dependency.provider('endpoint_policy_api') +@dependency.requires('catalog_api', 'policy_api') +class Manager(manager.Manager): + """Default pivot point for the Endpoint Policy backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + driver_namespace = 'keystone.endpoint_policy' + + def __init__(self): + super(Manager, self).__init__(CONF.endpoint_policy.driver) + + def _assert_valid_association(self, endpoint_id, service_id, region_id): + """Assert that the association is supported. + + There are three types of association supported: + + - Endpoint (in which case service and region must be None) + - Service and region (in which endpoint must be None) + - Service (in which case endpoint and region must be None) + + """ + if (endpoint_id is not None and + service_id is None and region_id is None): + return + if (service_id is not None and region_id is not None and + endpoint_id is None): + return + if (service_id is not None and + endpoint_id is None and region_id is None): + return + + raise exception.InvalidPolicyAssociation(endpoint_id=endpoint_id, + service_id=service_id, + region_id=region_id) + + def create_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + self._assert_valid_association(endpoint_id, service_id, region_id) + self.driver.create_policy_association(policy_id, endpoint_id, + service_id, region_id) + + def check_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + self._assert_valid_association(endpoint_id, service_id, region_id) + self.driver.check_policy_association(policy_id, endpoint_id, + service_id, region_id) + + def delete_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + self._assert_valid_association(endpoint_id, service_id, region_id) + self.driver.delete_policy_association(policy_id, endpoint_id, + service_id, region_id) + + def list_endpoints_for_policy(self, policy_id): + + def _get_endpoint(endpoint_id, policy_id): + try: + return self.catalog_api.get_endpoint(endpoint_id) + except exception.EndpointNotFound: + msg = _LW('Endpoint %(endpoint_id)s referenced in ' + 'association for policy %(policy_id)s not found.') + LOG.warning(msg, {'policy_id': policy_id, + 'endpoint_id': endpoint_id}) + raise + + def _get_endpoints_for_service(service_id, endpoints): + # TODO(henry-nash): Consider optimizing this in the future by + # adding an explicit list_endpoints_for_service to the catalog API. + return [ep for ep in endpoints if ep['service_id'] == service_id] + + def _get_endpoints_for_service_and_region( + service_id, region_id, endpoints, regions): + # TODO(henry-nash): Consider optimizing this in the future. + # The lack of a two-way pointer in the region tree structure + # makes this somewhat inefficient. + + def _recursively_get_endpoints_for_region( + region_id, service_id, endpoint_list, region_list, + endpoints_found, regions_examined): + """Recursively search down a region tree for endpoints. + + :param region_id: the point in the tree to examine + :param service_id: the service we are interested in + :param endpoint_list: list of all endpoints + :param region_list: list of all regions + :param endpoints_found: list of matching endpoints found so + far - which will be updated if more are + found in this iteration + :param regions_examined: list of regions we have already looked + at - used to spot illegal circular + references in the tree to avoid never + completing search + :returns: list of endpoints that match + + """ + + if region_id in regions_examined: + msg = _LE('Circular reference or a repeated entry found ' + 'in region tree - %(region_id)s.') + LOG.error(msg, {'region_id': ref.region_id}) + return + + regions_examined.append(region_id) + endpoints_found += ( + [ep for ep in endpoint_list if + ep['service_id'] == service_id and + ep['region_id'] == region_id]) + + for region in region_list: + if region['parent_region_id'] == region_id: + _recursively_get_endpoints_for_region( + region['id'], service_id, endpoints, regions, + endpoints_found, regions_examined) + + endpoints_found = [] + regions_examined = [] + + # Now walk down the region tree + _recursively_get_endpoints_for_region( + region_id, service_id, endpoints, regions, + endpoints_found, regions_examined) + + return endpoints_found + + matching_endpoints = [] + endpoints = self.catalog_api.list_endpoints() + regions = self.catalog_api.list_regions() + for ref in self.driver.list_associations_for_policy(policy_id): + if ref.get('endpoint_id') is not None: + matching_endpoints.append( + _get_endpoint(ref['endpoint_id'], policy_id)) + continue + + if (ref.get('service_id') is not None and + ref.get('region_id') is None): + matching_endpoints += _get_endpoints_for_service( + ref['service_id'], endpoints) + continue + + if (ref.get('service_id') is not None and + ref.get('region_id') is not None): + matching_endpoints += ( + _get_endpoints_for_service_and_region( + ref['service_id'], ref['region_id'], + endpoints, regions)) + continue + + msg = _LW('Unsupported policy association found - ' + 'Policy %(policy_id)s, Endpoint %(endpoint_id)s, ' + 'Service %(service_id)s, Region %(region_id)s, ') + LOG.warning(msg, {'policy_id': policy_id, + 'endpoint_id': ref['endpoint_id'], + 'service_id': ref['service_id'], + 'region_id': ref['region_id']}) + + return matching_endpoints + + def get_policy_for_endpoint(self, endpoint_id): + + def _get_policy(policy_id, endpoint_id): + try: + return self.policy_api.get_policy(policy_id) + except exception.PolicyNotFound: + msg = _LW('Policy %(policy_id)s referenced in association ' + 'for endpoint %(endpoint_id)s not found.') + LOG.warning(msg, {'policy_id': policy_id, + 'endpoint_id': endpoint_id}) + raise + + def _look_for_policy_for_region_and_service(endpoint): + """Look in the region and its parents for a policy. + + Examine the region of the endpoint for a policy appropriate for + the service of the endpoint. If there isn't a match, then chase up + the region tree to find one. + + """ + region_id = endpoint['region_id'] + regions_examined = [] + while region_id is not None: + try: + ref = self.driver.get_policy_association( + service_id=endpoint['service_id'], + region_id=region_id) + return ref['policy_id'] + except exception.PolicyAssociationNotFound: + pass + + # There wasn't one for that region & service, let's + # chase up the region tree + regions_examined.append(region_id) + region = self.catalog_api.get_region(region_id) + region_id = None + if region.get('parent_region_id') is not None: + region_id = region['parent_region_id'] + if region_id in regions_examined: + msg = _LE('Circular reference or a repeated entry ' + 'found in region tree - %(region_id)s.') + LOG.error(msg, {'region_id': region_id}) + break + + # First let's see if there is a policy explicitly defined for + # this endpoint. + + try: + ref = self.driver.get_policy_association(endpoint_id=endpoint_id) + return _get_policy(ref['policy_id'], endpoint_id) + except exception.PolicyAssociationNotFound: + pass + + # There wasn't a policy explicitly defined for this endpoint, so + # now let's see if there is one for the Region & Service. + + endpoint = self.catalog_api.get_endpoint(endpoint_id) + policy_id = _look_for_policy_for_region_and_service(endpoint) + if policy_id is not None: + return _get_policy(policy_id, endpoint_id) + + # Finally, just check if there is one for the service. + try: + ref = self.driver.get_policy_association( + service_id=endpoint['service_id']) + return _get_policy(ref['policy_id'], endpoint_id) + except exception.PolicyAssociationNotFound: + pass + + msg = _('No policy is associated with endpoint ' + '%(endpoint_id)s.') % {'endpoint_id': endpoint_id} + raise exception.NotFound(msg) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface description for an Endpoint Policy driver.""" + + @abc.abstractmethod + def create_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + """Creates a policy association. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param service_id: identity of the service to associate + :type service_id: string + :param region_id: identity of the region to associate + :type region_id: string + :returns: None + + There are three types of association permitted: + + - Endpoint (in which case service and region must be None) + - Service and region (in which endpoint must be None) + - Service (in which case endpoint and region must be None) + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def check_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + """Checks existence a policy association. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param service_id: identity of the service to associate + :type service_id: string + :param region_id: identity of the region to associate + :type region_id: string + :raises: keystone.exception.PolicyAssociationNotFound if there is no + match for the specified association + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + """Deletes a policy association. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param service_id: identity of the service to associate + :type service_id: string + :param region_id: identity of the region to associate + :type region_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_policy_association(self, endpoint_id=None, + service_id=None, region_id=None): + """Gets the policy for an explicit association. + + This method is not exposed as a public API, but is used by + get_policy_for_endpoint(). + + :param endpoint_id: identity of endpoint + :type endpoint_id: string + :param service_id: identity of the service + :type service_id: string + :param region_id: identity of the region + :type region_id: string + :raises: keystone.exception.PolicyAssociationNotFound if there is no + match for the specified association + :returns: dict containing policy_id + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_associations_for_policy(self, policy_id): + """List the associations for a policy. + + This method is not exposed as a public API, but is used by + list_endpoints_for_policy(). + + :param policy_id: identity of policy + :type policy_id: string + :returns: List of association dicts + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoints_for_policy(self, policy_id): + """List all the endpoints using a given policy. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :returns: list of endpoints that have an effective association with + that policy + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_policy_for_endpoint(self, endpoint_id): + """Get the appropriate policy for a given endpoint. + + :param endpoint_id: identity of endpoint + :type endpoint_id: string + :returns: Policy entity for the endpoint + + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_endpoint(self, endpoint_id): + """Removes all the policy associations with the specific endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_service(self, service_id): + """Removes all the policy associations with the specific service. + + :param service_id: identity of endpoint to check + :type service_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_region(self, region_id): + """Removes all the policy associations with the specific region. + + :param region_id: identity of endpoint to check + :type region_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_policy(self, policy_id): + """Removes all the policy associations with the specific policy. + + :param policy_id: identity of endpoint to check + :type policy_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/endpoint_policy/routers.py b/keystone-moon/keystone/endpoint_policy/routers.py new file mode 100644 index 00000000..4846bb18 --- /dev/null +++ b/keystone-moon/keystone/endpoint_policy/routers.py @@ -0,0 +1,85 @@ +# Copyright 2014 IBM Corp. +# +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.endpoint_policy import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-ENDPOINT-POLICY', extension_version='1.0') + + +class Routers(wsgi.RoutersBase): + + PATH_PREFIX = '/OS-ENDPOINT-POLICY' + + def append_v3_routers(self, mapper, routers): + endpoint_policy_controller = controllers.EndpointPolicyV3Controller() + + self._add_resource( + mapper, endpoint_policy_controller, + path='/endpoints/{endpoint_id}' + self.PATH_PREFIX + '/policy', + get_head_action='get_policy_for_endpoint', + rel=build_resource_relation(resource_name='endpoint_policy'), + path_vars={'endpoint_id': json_home.Parameters.ENDPOINT_ID}) + self._add_resource( + mapper, endpoint_policy_controller, + path='/policies/{policy_id}' + self.PATH_PREFIX + '/endpoints', + get_action='list_endpoints_for_policy', + rel=build_resource_relation(resource_name='policy_endpoints'), + path_vars={'policy_id': json_home.Parameters.POLICY_ID}) + self._add_resource( + mapper, endpoint_policy_controller, + path=('/policies/{policy_id}' + self.PATH_PREFIX + + '/endpoints/{endpoint_id}'), + get_head_action='check_policy_association_for_endpoint', + put_action='create_policy_association_for_endpoint', + delete_action='delete_policy_association_for_endpoint', + rel=build_resource_relation( + resource_name='endpoint_policy_association'), + path_vars={ + 'policy_id': json_home.Parameters.POLICY_ID, + 'endpoint_id': json_home.Parameters.ENDPOINT_ID, + }) + self._add_resource( + mapper, endpoint_policy_controller, + path=('/policies/{policy_id}' + self.PATH_PREFIX + + '/services/{service_id}'), + get_head_action='check_policy_association_for_service', + put_action='create_policy_association_for_service', + delete_action='delete_policy_association_for_service', + rel=build_resource_relation( + resource_name='service_policy_association'), + path_vars={ + 'policy_id': json_home.Parameters.POLICY_ID, + 'service_id': json_home.Parameters.SERVICE_ID, + }) + self._add_resource( + mapper, endpoint_policy_controller, + path=('/policies/{policy_id}' + self.PATH_PREFIX + + '/services/{service_id}/regions/{region_id}'), + get_head_action='check_policy_association_for_region_and_service', + put_action='create_policy_association_for_region_and_service', + delete_action='delete_policy_association_for_region_and_service', + rel=build_resource_relation( + resource_name='region_and_service_policy_association'), + path_vars={ + 'policy_id': json_home.Parameters.POLICY_ID, + 'service_id': json_home.Parameters.SERVICE_ID, + 'region_id': json_home.Parameters.REGION_ID, + }) diff --git a/keystone-moon/keystone/exception.py b/keystone-moon/keystone/exception.py index 6749fdcd..8e573c4c 100644 --- a/keystone-moon/keystone/exception.py +++ b/keystone-moon/keystone/exception.py @@ -15,7 +15,6 @@ from oslo_config import cfg from oslo_log import log from oslo_utils import encodeutils -import six from keystone.i18n import _, _LW @@ -63,7 +62,7 @@ class Error(Exception): except UnicodeDecodeError: try: kwargs = {k: encodeutils.safe_decode(v) - for k, v in six.iteritems(kwargs)} + for k, v in kwargs.items()} except UnicodeDecodeError: # NOTE(jamielennox): This is the complete failure case # at least by showing the template we have some idea @@ -84,6 +83,11 @@ class ValidationError(Error): title = 'Bad Request' +class URLValidationError(ValidationError): + message_format = _("Cannot create an endpoint with an invalid URL:" + " %(url)s") + + class SchemaValidationError(ValidationError): # NOTE(lbragstad): For whole OpenStack message consistency, this error # message has been written in a format consistent with WSME. @@ -99,6 +103,15 @@ class ValidationTimeStampError(Error): title = 'Bad Request' +class ValidationExpirationError(Error): + message_format = _("The 'expires_at' must not be before now." + " The server could not comply with the request" + " since it is either malformed or otherwise" + " incorrect. The client is assumed to be in error.") + code = 400 + title = 'Bad Request' + + class StringLengthExceeded(ValidationError): message_format = _("String length exceeded.The length of" " string '%(string)s' exceeded the limit" @@ -448,9 +461,9 @@ class MigrationNotProvided(Exception): ) % {'mod_name': mod_name, 'path': path}) -class UnsupportedTokenVersionException(Exception): - """Token version is unrecognizable or unsupported.""" - pass +class UnsupportedTokenVersionException(UnexpectedError): + message_format = _('Token version is unrecognizable or ' + 'unsupported.') class SAMLSigningError(UnexpectedError): diff --git a/keystone-moon/keystone/identity/backends/ldap.py b/keystone-moon/keystone/identity/backends/ldap.py index 0f7ee450..7a3cb03b 100644 --- a/keystone-moon/keystone/identity/backends/ldap.py +++ b/keystone-moon/keystone/identity/backends/ldap.py @@ -14,13 +14,12 @@ from __future__ import absolute_import import uuid -import ldap import ldap.filter from oslo_config import cfg from oslo_log import log import six -from keystone import clean +from keystone.common import clean from keystone.common import driver_hints from keystone.common import ldap as common_ldap from keystone.common import models @@ -42,7 +41,7 @@ class Identity(identity.Driver): self.group = GroupApi(conf) def default_assignment_driver(self): - return "keystone.assignment.backends.ldap.Assignment" + return 'ldap' def is_domain_aware(self): return False @@ -352,20 +351,18 @@ class GroupApi(common_ldap.BaseLdap): """Return a list of groups for which the user is a member.""" user_dn_esc = ldap.filter.escape_filter_chars(user_dn) - query = '(&(objectClass=%s)(%s=%s)%s)' % (self.object_class, - self.member_attribute, - user_dn_esc, - self.ldap_filter or '') + query = '(%s=%s)%s' % (self.member_attribute, + user_dn_esc, + self.ldap_filter or '') return self.get_all(query) def list_user_groups_filtered(self, user_dn, hints): """Return a filtered list of groups for which the user is a member.""" user_dn_esc = ldap.filter.escape_filter_chars(user_dn) - query = '(&(objectClass=%s)(%s=%s)%s)' % (self.object_class, - self.member_attribute, - user_dn_esc, - self.ldap_filter or '') + query = '(%s=%s)%s' % (self.member_attribute, + user_dn_esc, + self.ldap_filter or '') return self.get_all_filtered(hints, query) def list_group_users(self, group_id): diff --git a/keystone-moon/keystone/identity/backends/sql.py b/keystone-moon/keystone/identity/backends/sql.py index 39868416..8bda9a1b 100644 --- a/keystone-moon/keystone/identity/backends/sql.py +++ b/keystone-moon/keystone/identity/backends/sql.py @@ -77,7 +77,7 @@ class Identity(identity.Driver): super(Identity, self).__init__() def default_assignment_driver(self): - return "keystone.assignment.backends.sql.Assignment" + return 'sql' @property def is_sql(self): @@ -211,28 +211,19 @@ class Identity(identity.Driver): session.delete(membership_ref) def list_groups_for_user(self, user_id, hints): - # TODO(henry-nash) We could implement full filtering here by enhancing - # the join below. However, since it is likely to be a fairly rare - # occurrence to filter on more than the user_id already being used - # here, this is left as future enhancement and until then we leave - # it for the controller to do for us. session = sql.get_session() self.get_user(user_id) query = session.query(Group).join(UserGroupMembership) query = query.filter(UserGroupMembership.user_id == user_id) + query = sql.filter_limit_query(Group, query, hints) return [g.to_dict() for g in query] def list_users_in_group(self, group_id, hints): - # TODO(henry-nash) We could implement full filtering here by enhancing - # the join below. However, since it is likely to be a fairly rare - # occurrence to filter on more than the group_id already being used - # here, this is left as future enhancement and until then we leave - # it for the controller to do for us. session = sql.get_session() self.get_group(group_id) query = session.query(User).join(UserGroupMembership) query = query.filter(UserGroupMembership.group_id == group_id) - + query = sql.filter_limit_query(User, query, hints) return [identity.filter_user(u.to_dict()) for u in query] def delete_user(self, user_id): diff --git a/keystone-moon/keystone/identity/controllers.py b/keystone-moon/keystone/identity/controllers.py index a2676c41..7a6a642a 100644 --- a/keystone-moon/keystone/identity/controllers.py +++ b/keystone-moon/keystone/identity/controllers.py @@ -19,8 +19,10 @@ from oslo_log import log from keystone.common import controller from keystone.common import dependency +from keystone.common import validation from keystone import exception from keystone.i18n import _, _LW +from keystone.identity import schema from keystone import notifications @@ -205,9 +207,8 @@ class UserV3(controller.V3Controller): self.check_protection(context, prep_info, ref) @controller.protected() + @validation.validated(schema.user_create, 'user') def create_user(self, context, user): - self._require_attribute(user, 'name') - # The manager layer will generate the unique ID for users ref = self._normalize_dict(user) ref = self._normalize_domain_id(context, ref) @@ -243,6 +244,7 @@ class UserV3(controller.V3Controller): return UserV3.wrap_member(context, ref) @controller.protected() + @validation.validated(schema.user_update, 'user') def update_user(self, context, user_id, user): return self._update_user(context, user_id, user) @@ -291,9 +293,8 @@ class GroupV3(controller.V3Controller): self.get_member_from_driver = self.identity_api.get_group @controller.protected() + @validation.validated(schema.group_create, 'group') def create_group(self, context, group): - self._require_attribute(group, 'name') - # The manager layer will generate the unique ID for groups ref = self._normalize_dict(group) ref = self._normalize_domain_id(context, ref) @@ -321,6 +322,7 @@ class GroupV3(controller.V3Controller): return GroupV3.wrap_member(context, ref) @controller.protected() + @validation.validated(schema.group_update, 'group') def update_group(self, context, group_id, group): self._require_matching_id(group_id, group) self._require_matching_domain_id( diff --git a/keystone-moon/keystone/identity/core.py b/keystone-moon/keystone/identity/core.py index 988df78b..612a1859 100644 --- a/keystone-moon/keystone/identity/core.py +++ b/keystone-moon/keystone/identity/core.py @@ -21,11 +21,10 @@ import uuid from oslo_config import cfg from oslo_log import log -from oslo_utils import importutils import six -from keystone import clean from keystone.common import cache +from keystone.common import clean from keystone.common import dependency from keystone.common import driver_hints from keystone.common import manager @@ -90,8 +89,9 @@ class DomainConfigs(dict): _any_sql = False def _load_driver(self, domain_config): - return importutils.import_object( - domain_config['cfg'].identity.driver, domain_config['cfg']) + return manager.load_driver(Manager.driver_namespace, + domain_config['cfg'].identity.driver, + domain_config['cfg']) def _assert_no_more_than_one_sql_driver(self, domain_id, new_config, config_file=None): @@ -111,7 +111,7 @@ class DomainConfigs(dict): if not config_file: config_file = _('Database at /domains/%s/config') % domain_id raise exception.MultipleSQLDriversInConfig(source=config_file) - self._any_sql = new_config['driver'].is_sql + self._any_sql = self._any_sql or new_config['driver'].is_sql def _load_config_from_file(self, resource_api, file_list, domain_name): @@ -176,6 +176,21 @@ class DomainConfigs(dict): fname) def _load_config_from_database(self, domain_id, specific_config): + + def _assert_not_sql_driver(domain_id, new_config): + """Ensure this is not an sql driver. + + Due to multi-threading safety concerns, we do not currently support + the setting of a specific identity driver to sql via the Identity + API. + + """ + if new_config['driver'].is_sql: + reason = _('Domain specific sql drivers are not supported via ' + 'the Identity API. One is specified in ' + '/domains/%s/config') % domain_id + raise exception.InvalidDomainConfig(reason=reason) + domain_config = {} domain_config['cfg'] = cfg.ConfigOpts() config.configure(conf=domain_config['cfg']) @@ -186,10 +201,12 @@ class DomainConfigs(dict): for group in specific_config: for option in specific_config[group]: domain_config['cfg'].set_override( - option, specific_config[group][option], group) + option, specific_config[group][option], + group, enforce_type=True) + domain_config['cfg_overrides'] = specific_config domain_config['driver'] = self._load_driver(domain_config) - self._assert_no_more_than_one_sql_driver(domain_id, domain_config) + _assert_not_sql_driver(domain_id, domain_config) self[domain_id] = domain_config def _setup_domain_drivers_from_database(self, standard_driver, @@ -226,10 +243,12 @@ class DomainConfigs(dict): resource_api) def get_domain_driver(self, domain_id): + self.check_config_and_reload_domain_driver_if_required(domain_id) if domain_id in self: return self[domain_id]['driver'] def get_domain_conf(self, domain_id): + self.check_config_and_reload_domain_driver_if_required(domain_id) if domain_id in self: return self[domain_id]['cfg'] else: @@ -249,6 +268,61 @@ class DomainConfigs(dict): # The standard driver self.driver = self.driver() + def check_config_and_reload_domain_driver_if_required(self, domain_id): + """Check for, and load, any new domain specific config for this domain. + + This is only supported for the database-stored domain specific + configuration. + + When the domain specific drivers were set up, we stored away the + specific config for this domain that was available at that time. So we + now read the current version and compare. While this might seem + somewhat inefficient, the sensitive config call is cached, so should be + light weight. More importantly, when the cache timeout is reached, we + will get any config that has been updated from any other keystone + process. + + This cache-timeout approach works for both multi-process and + multi-threaded keystone configurations. In multi-threaded + configurations, even though we might remove a driver object (that + could be in use by another thread), this won't actually be thrown away + until all references to it have been broken. When that other + thread is released back and is restarted with another command to + process, next time it accesses the driver it will pickup the new one. + + """ + if (not CONF.identity.domain_specific_drivers_enabled or + not CONF.identity.domain_configurations_from_database): + # If specific drivers are not enabled, then there is nothing to do. + # If we are not storing the configurations in the database, then + # we'll only re-read the domain specific config files on startup + # of keystone. + return + + latest_domain_config = ( + self.domain_config_api. + get_config_with_sensitive_info(domain_id)) + domain_config_in_use = domain_id in self + + if latest_domain_config: + if (not domain_config_in_use or + latest_domain_config != self[domain_id]['cfg_overrides']): + self._load_config_from_database(domain_id, + latest_domain_config) + elif domain_config_in_use: + # The domain specific config has been deleted, so should remove the + # specific driver for this domain. + try: + del self[domain_id] + except KeyError: + # Allow this error in case we are unlucky and in a + # multi-threaded situation, two threads happen to be running + # in lock step. + pass + # If we fall into the else condition, this means there is no domain + # config set, and there is none in use either, so we have nothing + # to do. + def domains_configured(f): """Wraps API calls to lazy load domain configs after init. @@ -291,6 +365,7 @@ def exception_translated(exception_type): return _exception_translated +@notifications.listener @dependency.provider('identity_api') @dependency.requires('assignment_api', 'credential_api', 'id_mapping_api', 'resource_api', 'revoke_api') @@ -332,6 +407,9 @@ class Manager(manager.Manager): mapping by default is a more prudent way to introduce this functionality. """ + + driver_namespace = 'keystone.identity' + _USER = 'user' _GROUP = 'group' @@ -521,10 +599,10 @@ class Manager(manager.Manager): if (not driver.is_domain_aware() and driver == self.driver and domain_id != CONF.identity.default_domain_id and domain_id is not None): - LOG.warning('Found multiple domains being mapped to a ' - 'driver that does not support that (e.g. ' - 'LDAP) - Domain ID: %(domain)s, ' - 'Default Driver: %(driver)s', + LOG.warning(_LW('Found multiple domains being mapped to a ' + 'driver that does not support that (e.g. ' + 'LDAP) - Domain ID: %(domain)s, ' + 'Default Driver: %(driver)s'), {'domain': domain_id, 'driver': (driver == self.driver)}) raise exception.DomainNotFound(domain_id=domain_id) @@ -765,7 +843,7 @@ class Manager(manager.Manager): # Get user details to invalidate the cache. user_old = self.get_user(user_id) driver.delete_user(entity_id) - self.assignment_api.delete_user(user_id) + self.assignment_api.delete_user_assignments(user_id) self.get_user.invalidate(self, user_id) self.get_user_by_name.invalidate(self, user_old['name'], user_old['domain_id']) @@ -837,7 +915,7 @@ class Manager(manager.Manager): driver.delete_group(entity_id) self.get_group.invalidate(self, group_id) self.id_mapping_api.delete_id_mapping(group_id) - self.assignment_api.delete_group(group_id) + self.assignment_api.delete_group_assignments(group_id) notifications.Audit.deleted(self._GROUP, group_id, initiator) @@ -895,6 +973,19 @@ class Manager(manager.Manager): """ pass + @notifications.internal( + notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE) + def emit_invalidate_grant_token_persistence(self, user_project): + """Emit a notification to the callback system to revoke grant tokens. + + This method and associated callback listener removes the need for + making a direct call to another manager to delete and revoke tokens. + + :param user_project: {'user_id': user_id, 'project_id': project_id} + :type user_project: dict + """ + pass + @manager.response_truncated @domains_configured @exception_translated('user') @@ -1193,6 +1284,8 @@ class Driver(object): class MappingManager(manager.Manager): """Default pivot point for the ID Mapping backend.""" + driver_namespace = 'keystone.identity.id_mapping' + def __init__(self): super(MappingManager, self).__init__(CONF.identity_mapping.driver) diff --git a/keystone-moon/keystone/identity/generator.py b/keystone-moon/keystone/identity/generator.py index d25426ce..05ad2df5 100644 --- a/keystone-moon/keystone/identity/generator.py +++ b/keystone-moon/keystone/identity/generator.py @@ -23,6 +23,7 @@ from keystone.common import dependency from keystone.common import manager from keystone import exception + CONF = cfg.CONF @@ -30,6 +31,8 @@ CONF = cfg.CONF class Manager(manager.Manager): """Default pivot point for the identifier generator backend.""" + driver_namespace = 'keystone.identity.id_generator' + def __init__(self): super(Manager, self).__init__(CONF.identity_mapping.generator) diff --git a/keystone-moon/keystone/identity/schema.py b/keystone-moon/keystone/identity/schema.py new file mode 100644 index 00000000..047fcf02 --- /dev/null +++ b/keystone-moon/keystone/identity/schema.py @@ -0,0 +1,67 @@ +# 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 validation +from keystone.common.validation import parameter_types + + +# NOTE(lhcheng): the max length is not applicable since it is specific +# to the SQL backend, LDAP does not have length limitation. +_identity_name = { + 'type': 'string', + 'minLength': 1 +} + +_user_properties = { + 'default_project_id': validation.nullable(parameter_types.id_string), + 'description': validation.nullable(parameter_types.description), + 'domain_id': parameter_types.id_string, + 'enabled': parameter_types.boolean, + 'name': _identity_name, + 'password': { + 'type': ['string', 'null'] + } +} + +user_create = { + 'type': 'object', + 'properties': _user_properties, + 'required': ['name'], + 'additionalProperties': True +} + +user_update = { + 'type': 'object', + 'properties': _user_properties, + 'minProperties': 1, + 'additionalProperties': True +} + +_group_properties = { + 'description': validation.nullable(parameter_types.description), + 'domain_id': parameter_types.id_string, + 'name': _identity_name +} + +group_create = { + 'type': 'object', + 'properties': _group_properties, + 'required': ['name'], + 'additionalProperties': True +} + +group_update = { + 'type': 'object', + 'properties': _group_properties, + 'minProperties': 1, + 'additionalProperties': True +} diff --git a/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po index 8e4b6773..0403952d 100644 --- a/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: German (http://www.transifex.com/projects/p/keystone/language/" +"Language-Team: German (http://www.transifex.com/openstack/keystone/language/" "de/)\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Vorlagendatei %s kann nicht geöffnet werden" diff --git a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po index d2f5ebe6..289fa43d 100644 --- a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: English (Australia) (http://www.transifex.com/projects/p/" +"Language-Team: English (Australia) (http://www.transifex.com/openstack/" "keystone/language/en_AU/)\n" "Language: en_AU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Unable to open template file %s" diff --git a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po index 977af694..65b59aa3 100644 --- a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po +++ b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po @@ -7,77 +7,47 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" -"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-06-26 17:13+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: English (Australia) (http://www.transifex.com/projects/p/" +"Language-Team: English (Australia) (http://www.transifex.com/openstack/" "keystone/language/en_AU/)\n" "Language: en_AU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: keystone/notifications.py:304 -msgid "Failed to construct notifier" +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format <host>:<port> and that a debugger processes is listening on " +"that port." msgstr "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format <host>:<port> and that a debugger processes is listening on " +"that port." -#: keystone/notifications.py:389 #, python-format msgid "Failed to send %(res_id)s %(event_type)s notification" msgstr "Failed to send %(res_id)s %(event_type)s notification" -#: keystone/notifications.py:606 -#, python-format -msgid "Failed to send %(action)s %(event_type)s notification" -msgstr "" - -#: keystone/catalog/core.py:62 -#, python-format -msgid "Malformed endpoint - %(url)r is not a string" -msgstr "" +msgid "Failed to validate token" +msgstr "Failed to validate token" -#: keystone/catalog/core.py:66 #, python-format msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" msgstr "Malformed endpoint %(url)s - unknown key %(keyerror)s" -#: keystone/catalog/core.py:71 -#, python-format -msgid "" -"Malformed endpoint '%(url)s'. The following type error occurred during " -"string substitution: %(typeerror)s" -msgstr "" - -#: keystone/catalog/core.py:77 #, python-format msgid "" "Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" msgstr "" "Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" -#: keystone/common/openssl.py:93 -#, python-format -msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" -msgstr "" - -#: keystone/common/openssl.py:121 -#, python-format -msgid "Failed to remove file %(file_path)r: %(error)s" -msgstr "" - -#: keystone/common/utils.py:239 -msgid "" -"Error setting up the debug environment. Verify that the option --debug-url " -"has the format <host>:<port> and that a debugger processes is listening on " -"that port." -msgstr "" -"Error setting up the debug environment. Verify that the option --debug-url " -"has the format <host>:<port> and that a debugger processes is listening on " -"that port." +msgid "Server error" +msgstr "Server error" -#: keystone/common/cache/core.py:100 #, python-format msgid "" "Unable to build cache config-key. Expected format \"<argname>:<value>\". " @@ -86,94 +56,9 @@ msgstr "" "Unable to build cache config-key. Expected format \"<argname>:<value>\". " "Skipping unknown format: %s" -#: keystone/common/environment/eventlet_server.py:99 -#, python-format -msgid "Could not bind to %(host)s:%(port)s" -msgstr "" - -#: keystone/common/environment/eventlet_server.py:185 -msgid "Server error" -msgstr "Server error" - -#: keystone/contrib/endpoint_policy/core.py:129 -#: keystone/contrib/endpoint_policy/core.py:228 -#, python-format -msgid "" -"Circular reference or a repeated entry found in region tree - %(region_id)s." -msgstr "" - -#: keystone/contrib/federation/idp.py:410 -#, python-format -msgid "Error when signing assertion, reason: %(reason)s" -msgstr "" - -#: keystone/contrib/oauth1/core.py:136 -msgid "Cannot retrieve Authorization headers" -msgstr "" - -#: keystone/openstack/common/loopingcall.py:95 -msgid "in fixed duration looping call" -msgstr "in fixed duration looping call" - -#: keystone/openstack/common/loopingcall.py:138 -msgid "in dynamic looping call" -msgstr "in dynamic looping call" - -#: keystone/openstack/common/service.py:268 -msgid "Unhandled exception" -msgstr "Unhandled exception" - -#: keystone/resource/core.py:477 -#, python-format -msgid "" -"Circular reference or a repeated entry found projects hierarchy - " -"%(project_id)s." -msgstr "" - -#: keystone/resource/core.py:939 -#, python-format -msgid "" -"Unexpected results in response for domain config - %(count)s responses, " -"first option is %(option)s, expected option %(expected)s" -msgstr "" - -#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 -#, python-format -msgid "" -"Circular reference or a repeated entry found in projects hierarchy - " -"%(project_id)s." -msgstr "" - -#: keystone/token/provider.py:292 -#, python-format -msgid "Unexpected error or malformed token determining token expiry: %s" -msgstr "Unexpected error or malformed token determining token expiry: %s" - -#: keystone/token/persistence/backends/kvs.py:226 -#, python-format -msgid "" -"Reinitializing revocation list due to error in loading revocation list from " -"backend. Expected `list` type got `%(type)s`. Old revocation list data: " -"%(list)r" -msgstr "" - -#: keystone/token/providers/common.py:611 -msgid "Failed to validate token" -msgstr "Failed to validate token" - -#: keystone/token/providers/pki.py:47 msgid "Unable to sign token" msgstr "Unable to sign token" -#: keystone/token/providers/fernet/utils.py:38 #, python-format -msgid "" -"Either [fernet_tokens] key_repository does not exist or Keystone does not " -"have sufficient permission to access it: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:79 -msgid "" -"Failed to create [fernet_tokens] key_repository: either it already exists or " -"you don't have sufficient permissions to create it" -msgstr "" +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "Unexpected error or malformed token determining token expiry: %s" diff --git a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po index e3dea47d..dca5aa9b 100644 --- a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po +++ b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po @@ -8,1535 +8,340 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-23 06:04+0000\n" -"PO-Revision-Date: 2015-03-21 23:03+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-08-04 18:01+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: English (Australia) " -"(http://www.transifex.com/projects/p/keystone/language/en_AU/)\n" +"Language-Team: English (Australia) (http://www.transifex.com/openstack/" +"keystone/language/en_AU/)\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" -#: keystone/clean.py:24 -#, python-format -msgid "%s cannot be empty." -msgstr "%s cannot be empty." - -#: keystone/clean.py:26 #, python-format msgid "%(property_name)s cannot be less than %(min_length)s characters." msgstr "%(property_name)s cannot be less than %(min_length)s characters." -#: keystone/clean.py:31 -#, python-format -msgid "%(property_name)s should not be greater than %(max_length)s characters." -msgstr "%(property_name)s should not be greater than %(max_length)s characters." - -#: keystone/clean.py:40 #, python-format msgid "%(property_name)s is not a %(display_expected_type)s" msgstr "%(property_name)s is not a %(display_expected_type)s" -#: keystone/cli.py:283 -msgid "At least one option must be provided" -msgstr "" - -#: keystone/cli.py:290 -msgid "--all option cannot be mixed with other options" -msgstr "" - -#: keystone/cli.py:301 -#, python-format -msgid "Unknown domain '%(name)s' specified by --domain-name" -msgstr "" - -#: keystone/cli.py:365 keystone/tests/unit/test_cli.py:213 -msgid "At least one option must be provided, use either --all or --domain-name" -msgstr "" - -#: keystone/cli.py:371 keystone/tests/unit/test_cli.py:229 -msgid "The --all option cannot be used with the --domain-name option" -msgstr "" - -#: keystone/cli.py:397 keystone/tests/unit/test_cli.py:246 -#, python-format -msgid "" -"Invalid domain name: %(domain)s found in config file name: %(file)s - " -"ignoring this file." -msgstr "" - -#: keystone/cli.py:405 keystone/tests/unit/test_cli.py:187 -#, python-format -msgid "" -"Domain: %(domain)s already has a configuration defined - ignoring file: " -"%(file)s." -msgstr "" - -#: keystone/cli.py:419 -#, python-format -msgid "Error parsing configuration file for domain: %(domain)s, file: %(file)s." -msgstr "" - -#: keystone/cli.py:452 -#, python-format -msgid "" -"To get a more detailed information on this error, re-run this command for" -" the specific domain, i.e.: keystone-manage domain_config_upload " -"--domain-name %s" -msgstr "" - -#: keystone/cli.py:470 -#, python-format -msgid "Unable to locate domain config directory: %s" -msgstr "Unable to locate domain config directory: %s" - -#: keystone/cli.py:503 -msgid "" -"Unable to access the keystone database, please check it is configured " -"correctly." -msgstr "" - -#: keystone/exception.py:79 -#, python-format -msgid "" -"Expecting to find %(attribute)s in %(target)s - the server could not " -"comply with the request since it is either malformed or otherwise " -"incorrect. The client is assumed to be in error." -msgstr "" - -#: keystone/exception.py:90 -#, python-format -msgid "%(detail)s" -msgstr "" - -#: keystone/exception.py:94 -msgid "" -"Timestamp not in expected format. The server could not comply with the " -"request since it is either malformed or otherwise incorrect. The client " -"is assumed to be in error." -msgstr "" -"Timestamp not in expected format. The server could not comply with the " -"request since it is either malformed or otherwise incorrect. The client " -"is assumed to be in error." - -#: keystone/exception.py:103 -#, python-format -msgid "" -"String length exceeded.The length of string '%(string)s' exceeded the " -"limit of column %(type)s(CHAR(%(length)d))." -msgstr "" -"String length exceeded.The length of string '%(string)s' exceeded the " -"limit of column %(type)s(CHAR(%(length)d))." - -#: keystone/exception.py:109 #, python-format -msgid "" -"Request attribute %(attribute)s must be less than or equal to %(size)i. " -"The server could not comply with the request because the attribute size " -"is invalid (too large). The client is assumed to be in error." -msgstr "" -"Request attribute %(attribute)s must be less than or equal to %(size)i. " -"The server could not comply with the request because the attribute size " -"is invalid (too large). The client is assumed to be in error." - -#: keystone/exception.py:119 -#, python-format -msgid "" -"The specified parent region %(parent_region_id)s would create a circular " -"region hierarchy." -msgstr "" - -#: keystone/exception.py:126 -#, python-format -msgid "" -"The password length must be less than or equal to %(size)i. The server " -"could not comply with the request because the password is invalid." -msgstr "" - -#: keystone/exception.py:134 -#, python-format -msgid "" -"Unable to delete region %(region_id)s because it or its child regions " -"have associated endpoints." -msgstr "" - -#: keystone/exception.py:141 -msgid "" -"The certificates you requested are not available. It is likely that this " -"server does not use PKI tokens otherwise this is the result of " -"misconfiguration." -msgstr "" - -#: keystone/exception.py:150 -msgid "(Disable debug mode to suppress these details.)" +msgid "%(property_name)s should not be greater than %(max_length)s characters." msgstr "" +"%(property_name)s should not be greater than %(max_length)s characters." -#: keystone/exception.py:155 #, python-format -msgid "%(message)s %(amendment)s" -msgstr "" - -#: keystone/exception.py:163 -msgid "The request you have made requires authentication." -msgstr "The request you have made requires authentication." - -#: keystone/exception.py:169 -msgid "Authentication plugin error." -msgstr "Authentication plugin error." +msgid "%s cannot be empty." +msgstr "%s cannot be empty." -#: keystone/exception.py:177 -#, python-format -msgid "Unable to find valid groups while using mapping %(mapping_id)s" -msgstr "" +msgid "Access token is expired" +msgstr "Access token is expired" -#: keystone/exception.py:182 -msgid "Attempted to authenticate with an unsupported method." -msgstr "Attempted to authenticate with an unsupported method." +msgid "Access token not found" +msgstr "Access token not found" -#: keystone/exception.py:190 msgid "Additional authentications steps required." msgstr "Additional authentications steps required." -#: keystone/exception.py:198 -msgid "You are not authorized to perform the requested action." -msgstr "You are not authorized to perform the requested action." - -#: keystone/exception.py:205 -#, python-format -msgid "You are not authorized to perform the requested action: %(action)s" -msgstr "" - -#: keystone/exception.py:210 -#, python-format -msgid "" -"Could not change immutable attribute(s) '%(attributes)s' in target " -"%(target)s" -msgstr "" - -#: keystone/exception.py:215 -#, python-format -msgid "" -"Group membership across backend boundaries is not allowed, group in " -"question is %(group_id)s, user is %(user_id)s" -msgstr "" - -#: keystone/exception.py:221 -#, python-format -msgid "" -"Invalid mix of entities for policy association - only Endpoint, Service " -"or Region+Service allowed. Request was - Endpoint: %(endpoint_id)s, " -"Service: %(service_id)s, Region: %(region_id)s" -msgstr "" - -#: keystone/exception.py:228 -#, python-format -msgid "Invalid domain specific configuration: %(reason)s" -msgstr "" - -#: keystone/exception.py:232 -#, python-format -msgid "Could not find: %(target)s" -msgstr "" - -#: keystone/exception.py:238 -#, python-format -msgid "Could not find endpoint: %(endpoint_id)s" -msgstr "" - -#: keystone/exception.py:245 msgid "An unhandled exception has occurred: Could not find metadata." msgstr "An unhandled exception has occurred: Could not find metadata." -#: keystone/exception.py:250 -#, python-format -msgid "Could not find policy: %(policy_id)s" -msgstr "" - -#: keystone/exception.py:254 -msgid "Could not find policy association" -msgstr "" - -#: keystone/exception.py:258 -#, python-format -msgid "Could not find role: %(role_id)s" -msgstr "" - -#: keystone/exception.py:262 -#, python-format -msgid "" -"Could not find role assignment with role: %(role_id)s, user or group: " -"%(actor_id)s, project or domain: %(target_id)s" -msgstr "" - -#: keystone/exception.py:268 -#, python-format -msgid "Could not find region: %(region_id)s" -msgstr "" - -#: keystone/exception.py:272 -#, python-format -msgid "Could not find service: %(service_id)s" -msgstr "" - -#: keystone/exception.py:276 -#, python-format -msgid "Could not find domain: %(domain_id)s" -msgstr "" - -#: keystone/exception.py:280 -#, python-format -msgid "Could not find project: %(project_id)s" -msgstr "" - -#: keystone/exception.py:284 -#, python-format -msgid "Cannot create project with parent: %(project_id)s" -msgstr "" - -#: keystone/exception.py:288 -#, python-format -msgid "Could not find token: %(token_id)s" -msgstr "" - -#: keystone/exception.py:292 -#, python-format -msgid "Could not find user: %(user_id)s" -msgstr "" - -#: keystone/exception.py:296 -#, python-format -msgid "Could not find group: %(group_id)s" -msgstr "" - -#: keystone/exception.py:300 -#, python-format -msgid "Could not find mapping: %(mapping_id)s" -msgstr "" - -#: keystone/exception.py:304 -#, python-format -msgid "Could not find trust: %(trust_id)s" -msgstr "" - -#: keystone/exception.py:308 -#, python-format -msgid "No remaining uses for trust: %(trust_id)s" -msgstr "" - -#: keystone/exception.py:312 -#, python-format -msgid "Could not find credential: %(credential_id)s" -msgstr "" - -#: keystone/exception.py:316 -#, python-format -msgid "Could not find version: %(version)s" -msgstr "" - -#: keystone/exception.py:320 -#, python-format -msgid "Could not find Endpoint Group: %(endpoint_group_id)s" -msgstr "" - -#: keystone/exception.py:324 -#, python-format -msgid "Could not find Identity Provider: %(idp_id)s" -msgstr "" - -#: keystone/exception.py:328 -#, python-format -msgid "Could not find Service Provider: %(sp_id)s" -msgstr "" - -#: keystone/exception.py:332 -#, python-format -msgid "" -"Could not find federated protocol %(protocol_id)s for Identity Provider: " -"%(idp_id)s" -msgstr "" - -#: keystone/exception.py:343 -#, python-format -msgid "" -"Could not find %(group_or_option)s in domain configuration for domain " -"%(domain_id)s" -msgstr "" - -#: keystone/exception.py:348 -#, python-format -msgid "Conflict occurred attempting to store %(type)s - %(details)s" -msgstr "" - -#: keystone/exception.py:356 -msgid "An unexpected error prevented the server from fulfilling your request." -msgstr "" - -#: keystone/exception.py:359 -#, python-format -msgid "" -"An unexpected error prevented the server from fulfilling your request: " -"%(exception)s" -msgstr "" - -#: keystone/exception.py:382 -#, python-format -msgid "Unable to consume trust %(trust_id)s, unable to acquire lock." -msgstr "" - -#: keystone/exception.py:387 -msgid "" -"Expected signing certificates are not available on the server. Please " -"check Keystone configuration." -msgstr "" - -#: keystone/exception.py:393 -#, python-format -msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." -msgstr "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." - -#: keystone/exception.py:398 -#, python-format -msgid "" -"Group %(group_id)s returned by mapping %(mapping_id)s was not found in " -"the backend." -msgstr "" - -#: keystone/exception.py:403 -#, python-format -msgid "Error while reading metadata file, %(reason)s" -msgstr "" - -#: keystone/exception.py:407 -#, python-format -msgid "" -"Unexpected combination of grant attributes - User: %(user_id)s, Group: " -"%(group_id)s, Project: %(project_id)s, Domain: %(domain_id)s" -msgstr "" - -#: keystone/exception.py:414 -msgid "The action you have requested has not been implemented." -msgstr "The action you have requested has not been implemented." - -#: keystone/exception.py:421 -msgid "The service you have requested is no longer available on this server." -msgstr "" - -#: keystone/exception.py:428 -#, python-format -msgid "The Keystone configuration file %(config_file)s could not be found." -msgstr "The Keystone configuration file %(config_file)s could not be found." - -#: keystone/exception.py:433 -msgid "" -"No encryption keys found; run keystone-manage fernet_setup to bootstrap " -"one." -msgstr "" - -#: keystone/exception.py:438 -#, python-format -msgid "" -"The Keystone domain-specific configuration has specified more than one " -"SQL driver (only one is permitted): %(source)s." -msgstr "" +msgid "Attempted to authenticate with an unsupported method." +msgstr "Attempted to authenticate with an unsupported method." -#: keystone/exception.py:445 -#, python-format -msgid "" -"%(mod_name)s doesn't provide database migrations. The migration " -"repository path at %(path)s doesn't exist or isn't a directory." -msgstr "" +msgid "Authentication plugin error." +msgstr "Authentication plugin error." -#: keystone/exception.py:457 #, python-format -msgid "" -"Unable to sign SAML assertion. It is likely that this server does not " -"have xmlsec1 installed, or this is the result of misconfiguration. Reason" -" %(reason)s" -msgstr "" - -#: keystone/exception.py:465 -msgid "" -"No Authorization headers found, cannot proceed with OAuth related calls, " -"if running under HTTPd or Apache, ensure WSGIPassAuthorization is set to " -"On." -msgstr "" +msgid "Cannot change %(option_name)s %(attr)s" +msgstr "Cannot change %(option_name)s %(attr)s" -#: keystone/notifications.py:250 -#, python-format -msgid "%(event)s is not a valid notification event, must be one of: %(actions)s" -msgstr "" +msgid "Cannot change consumer secret" +msgstr "Cannot change consumer secret" -#: keystone/notifications.py:259 #, python-format -msgid "Method not callable: %s" -msgstr "" - -#: keystone/assignment/controllers.py:107 keystone/identity/controllers.py:69 -#: keystone/resource/controllers.py:78 -msgid "Name field is required and cannot be empty" -msgstr "Name field is required and cannot be empty" - -#: keystone/assignment/controllers.py:330 -#: keystone/assignment/controllers.py:753 -msgid "Specify a domain or project, not both" -msgstr "Specify a domain or project, not both" - -#: keystone/assignment/controllers.py:333 -msgid "Specify one of domain or project" -msgstr "" - -#: keystone/assignment/controllers.py:338 -#: keystone/assignment/controllers.py:758 -msgid "Specify a user or group, not both" -msgstr "Specify a user or group, not both" - -#: keystone/assignment/controllers.py:341 -msgid "Specify one of user or group" -msgstr "" - -#: keystone/assignment/controllers.py:742 -msgid "Combining effective and group filter will always result in an empty list." -msgstr "" +msgid "Cannot remove role that has not been granted, %s" +msgstr "Cannot remove role that has not been granted, %s" -#: keystone/assignment/controllers.py:747 -msgid "" -"Combining effective, domain and inherited filters will always result in " -"an empty list." -msgstr "" +msgid "Consumer not found" +msgstr "Consumer not found" -#: keystone/assignment/core.py:228 -msgid "Must specify either domain or project" -msgstr "" +msgid "Could not find role" +msgstr "Could not find role" -#: keystone/assignment/core.py:493 -#, python-format -msgid "Project (%s)" -msgstr "Project (%s)" +msgid "Credential belongs to another user" +msgstr "Credential belongs to another user" -#: keystone/assignment/core.py:495 #, python-format msgid "Domain (%s)" msgstr "Domain (%s)" -#: keystone/assignment/core.py:497 -msgid "Unknown Target" -msgstr "Unknown Target" - -#: keystone/assignment/backends/ldap.py:92 -msgid "Domain metadata not supported by LDAP" -msgstr "" - -#: keystone/assignment/backends/ldap.py:381 -#, python-format -msgid "User %(user_id)s already has role %(role_id)s in tenant %(tenant_id)s" -msgstr "" - -#: keystone/assignment/backends/ldap.py:387 -#, python-format -msgid "Role %s not found" -msgstr "Role %s not found" - -#: keystone/assignment/backends/ldap.py:402 -#: keystone/assignment/backends/sql.py:335 #, python-format -msgid "Cannot remove role that has not been granted, %s" -msgstr "Cannot remove role that has not been granted, %s" +msgid "Domain is disabled: %s" +msgstr "Domain is disabled: %s" -#: keystone/assignment/backends/sql.py:356 -#, python-format -msgid "Unexpected assignment type encountered, %s" -msgstr "" +msgid "Domain scoped token is not supported" +msgstr "Domain scoped token is not supported" -#: keystone/assignment/role_backends/ldap.py:61 keystone/catalog/core.py:103 -#: keystone/common/ldap/core.py:1400 keystone/resource/backends/ldap.py:149 #, python-format msgid "Duplicate ID, %s." msgstr "Duplicate ID, %s." -#: keystone/assignment/role_backends/ldap.py:69 -#: keystone/common/ldap/core.py:1390 #, python-format msgid "Duplicate name, %s." msgstr "Duplicate name, %s." -#: keystone/assignment/role_backends/ldap.py:119 -#, python-format -msgid "Cannot duplicate name %s" -msgstr "" - -#: keystone/auth/controllers.py:60 -#, python-format -msgid "" -"Cannot load an auth-plugin by class-name without a \"method\" attribute " -"defined: %s" -msgstr "" - -#: keystone/auth/controllers.py:71 -#, python-format -msgid "" -"Auth plugin %(plugin)s is requesting previously registered method " -"%(method)s" -msgstr "" - -#: keystone/auth/controllers.py:115 -#, python-format -msgid "" -"Unable to reconcile identity attribute %(attribute)s as it has " -"conflicting values %(new)s and %(old)s" -msgstr "" - -#: keystone/auth/controllers.py:336 -msgid "Scoping to both domain and project is not allowed" -msgstr "Scoping to both domain and project is not allowed" - -#: keystone/auth/controllers.py:339 -msgid "Scoping to both domain and trust is not allowed" -msgstr "Scoping to both domain and trust is not allowed" - -#: keystone/auth/controllers.py:342 -msgid "Scoping to both project and trust is not allowed" -msgstr "Scoping to both project and trust is not allowed" - -#: keystone/auth/controllers.py:512 -msgid "User not found" -msgstr "User not found" - -#: keystone/auth/controllers.py:616 -msgid "A project-scoped token is required to produce a service catalog." -msgstr "" - -#: keystone/auth/plugins/external.py:46 -msgid "No authenticated user" -msgstr "No authenticated user" - -#: keystone/auth/plugins/external.py:56 -#, python-format -msgid "Unable to lookup user %s" -msgstr "Unable to lookup user %s" - -#: keystone/auth/plugins/external.py:107 -msgid "auth_type is not Negotiate" -msgstr "" - -#: keystone/auth/plugins/mapped.py:244 -msgid "Could not map user" -msgstr "" - -#: keystone/auth/plugins/oauth1.py:39 -#, python-format -msgid "%s not supported" -msgstr "" - -#: keystone/auth/plugins/oauth1.py:57 -msgid "Access token is expired" -msgstr "Access token is expired" - -#: keystone/auth/plugins/oauth1.py:71 -msgid "Could not validate the access token" -msgstr "" - -#: keystone/auth/plugins/password.py:46 -msgid "Invalid username or password" -msgstr "Invalid username or password" - -#: keystone/auth/plugins/token.py:72 keystone/token/controllers.py:160 -msgid "rescope a scoped token" -msgstr "" - -#: keystone/catalog/controllers.py:168 -#, python-format -msgid "Conflicting region IDs specified: \"%(url_id)s\" != \"%(ref_id)s\"" -msgstr "" - -#: keystone/common/authorization.py:47 keystone/common/wsgi.py:64 -#, python-format -msgid "token reference must be a KeystoneToken type, got: %s" -msgstr "" - -#: keystone/common/base64utils.py:66 -msgid "pad must be single character" -msgstr "pad must be single character" - -#: keystone/common/base64utils.py:215 -#, python-format -msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" -msgstr "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" +msgid "Enabled field must be a boolean" +msgstr "Enabled field must be a boolean" -#: keystone/common/base64utils.py:219 -#, python-format -msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" -msgstr "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" +msgid "Enabled field should be a boolean" +msgstr "Enabled field should be a boolean" -#: keystone/common/base64utils.py:225 #, python-format -msgid "text is not a multiple of 4, but contains pad \"%s\"" -msgstr "text is not a multiple of 4, but contains pad \"%s\"" - -#: keystone/common/base64utils.py:244 keystone/common/base64utils.py:265 -msgid "padded base64url text must be multiple of 4 characters" -msgstr "padded base64url text must be multiple of 4 characters" - -#: keystone/common/controller.py:237 keystone/token/providers/common.py:589 -msgid "Non-default domain is not supported" -msgstr "Non-default domain is not supported" +msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" +msgstr "Endpoint %(endpoint_id)s not found in project %(project_id)s" -#: keystone/common/controller.py:305 keystone/identity/core.py:428 -#: keystone/resource/core.py:761 keystone/resource/backends/ldap.py:61 #, python-format msgid "Expected dict or list: %s" msgstr "Expected dict or list: %s" -#: keystone/common/controller.py:318 -msgid "Marker could not be found" -msgstr "Marker could not be found" - -#: keystone/common/controller.py:329 -msgid "Invalid limit value" -msgstr "Invalid limit value" - -#: keystone/common/controller.py:637 -msgid "Cannot change Domain ID" -msgstr "" - -#: keystone/common/controller.py:666 -msgid "domain_id is required as part of entity" -msgstr "" - -#: keystone/common/controller.py:701 -msgid "A domain-scoped token must be used" -msgstr "" - -#: keystone/common/dependency.py:68 -#, python-format -msgid "Unregistered dependency: %(name)s for %(targets)s" -msgstr "" - -#: keystone/common/dependency.py:108 -msgid "event_callbacks must be a dict" -msgstr "" - -#: keystone/common/dependency.py:113 -#, python-format -msgid "event_callbacks[%s] must be a dict" -msgstr "" - -#: keystone/common/pemutils.py:223 -#, python-format -msgid "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" -msgstr "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" - -#: keystone/common/pemutils.py:242 -#, python-format -msgid "" -"unknown pem header \"%(pem_header)s\", valid headers are: " -"%(valid_pem_headers)s" -msgstr "" -"unknown pem header \"%(pem_header)s\", valid headers are: " -"%(valid_pem_headers)s" - -#: keystone/common/pemutils.py:298 -#, python-format -msgid "failed to find end matching \"%s\"" -msgstr "failed to find end matching \"%s\"" - -#: keystone/common/pemutils.py:302 -#, python-format -msgid "" -"beginning & end PEM headers do not match (%(begin_pem_header)s!= " -"%(end_pem_header)s)" -msgstr "" -"beginning & end PEM headers do not match (%(begin_pem_header)s!= " -"%(end_pem_header)s)" - -#: keystone/common/pemutils.py:377 -#, python-format -msgid "unknown pem_type: \"%s\"" -msgstr "unknown pem_type: \"%s\"" - -#: keystone/common/pemutils.py:389 -#, python-format -msgid "" -"failed to base64 decode %(pem_type)s PEM at position%(position)d: " -"%(err_msg)s" -msgstr "" -"failed to base64 decode %(pem_type)s PEM at position%(position)d: " -"%(err_msg)s" - -#: keystone/common/utils.py:164 keystone/credential/controllers.py:44 -msgid "Invalid blob in credential" -msgstr "Invalid blob in credential" - -#: keystone/common/wsgi.py:330 -#, python-format -msgid "%s field is required and cannot be empty" -msgstr "" - -#: keystone/common/wsgi.py:342 -#, python-format -msgid "%s field(s) cannot be empty" -msgstr "" - -#: keystone/common/wsgi.py:563 -msgid "The resource could not be found." -msgstr "The resource could not be found." - -#: keystone/common/wsgi.py:704 -#, python-format -msgid "Unexpected status requested for JSON Home response, %s" -msgstr "" - -#: keystone/common/cache/_memcache_pool.py:113 -#, python-format -msgid "Unable to get a connection from pool id %(id)s after %(seconds)s seconds." -msgstr "" - -#: keystone/common/cache/core.py:132 -msgid "region not type dogpile.cache.CacheRegion" -msgstr "region not type dogpile.cache.CacheRegion" - -#: keystone/common/cache/backends/mongo.py:231 -msgid "db_hosts value is required" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:236 -msgid "database db_name is required" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:241 -msgid "cache_collection name is required" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:252 -msgid "integer value expected for w (write concern attribute)" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:260 -msgid "replicaset_name required when use_replica is True" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:275 -msgid "integer value expected for mongo_ttl_seconds" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:301 -msgid "no ssl support available" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:310 -#, python-format -msgid "" -"Invalid ssl_cert_reqs value of %s, must be one of \"NONE\", \"OPTIONAL\"," -" \"REQUIRED\"" -msgstr "" - -#: keystone/common/kvs/core.py:71 -#, python-format -msgid "Lock Timeout occurred for key, %(target)s" -msgstr "" - -#: keystone/common/kvs/core.py:106 -#, python-format -msgid "KVS region %s is already configured. Cannot reconfigure." -msgstr "" - -#: keystone/common/kvs/core.py:145 -#, python-format -msgid "Key Value Store not configured: %s" -msgstr "" - -#: keystone/common/kvs/core.py:198 -msgid "`key_mangler` option must be a function reference" -msgstr "" - -#: keystone/common/kvs/core.py:353 -#, python-format -msgid "Lock key must match target key: %(lock)s != %(target)s" -msgstr "" - -#: keystone/common/kvs/core.py:357 -msgid "Must be called within an active lock context." -msgstr "" - -#: keystone/common/kvs/backends/memcached.py:69 -#, python-format -msgid "Maximum lock attempts on %s occurred." -msgstr "" - -#: keystone/common/kvs/backends/memcached.py:108 -#, python-format -msgid "" -"Backend `%(driver)s` is not a valid memcached backend. Valid drivers: " -"%(driver_list)s" -msgstr "" - -#: keystone/common/kvs/backends/memcached.py:178 -msgid "`key_mangler` functions must be callable." -msgstr "" - -#: keystone/common/ldap/core.py:191 -#, python-format -msgid "Invalid LDAP deref option: %(option)s. Choose one of: %(options)s" -msgstr "" +msgid "Failed to validate token" +msgstr "Failed to validate token" -#: keystone/common/ldap/core.py:201 #, python-format msgid "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" msgstr "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" -#: keystone/common/ldap/core.py:213 +#, python-format +msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" +msgstr "Invalid LDAP TLS_AVAIL option: %s. TLS not available" + #, python-format msgid "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" msgstr "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" -#: keystone/common/ldap/core.py:588 msgid "Invalid TLS / LDAPS combination" msgstr "Invalid TLS / LDAPS combination" -#: keystone/common/ldap/core.py:593 -#, python-format -msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" -msgstr "Invalid LDAP TLS_AVAIL option: %s. TLS not available" - -#: keystone/common/ldap/core.py:603 -#, python-format -msgid "tls_cacertfile %s not found or is not a file" -msgstr "tls_cacertfile %s not found or is not a file" +msgid "Invalid blob in credential" +msgstr "Invalid blob in credential" -#: keystone/common/ldap/core.py:615 -#, python-format -msgid "tls_cacertdir %s not found or is not a directory" -msgstr "tls_cacertdir %s not found or is not a directory" +msgid "Invalid limit value" +msgstr "Invalid limit value" -#: keystone/common/ldap/core.py:1325 -#, python-format -msgid "ID attribute %(id_attr)s not found in LDAP object %(dn)s" -msgstr "" +msgid "Invalid username or password" +msgstr "Invalid username or password" -#: keystone/common/ldap/core.py:1369 #, python-format msgid "LDAP %s create" msgstr "LDAP %s create" -#: keystone/common/ldap/core.py:1374 -#, python-format -msgid "LDAP %s update" -msgstr "LDAP %s update" - -#: keystone/common/ldap/core.py:1379 #, python-format msgid "LDAP %s delete" msgstr "LDAP %s delete" -#: keystone/common/ldap/core.py:1521 -msgid "" -"Disabling an entity where the 'enable' attribute is ignored by " -"configuration." -msgstr "" - -#: keystone/common/ldap/core.py:1532 -#, python-format -msgid "Cannot change %(option_name)s %(attr)s" -msgstr "Cannot change %(option_name)s %(attr)s" - -#: keystone/common/ldap/core.py:1619 #, python-format -msgid "Member %(member)s is already a member of group %(group)s" -msgstr "" - -#: keystone/common/sql/core.py:219 -msgid "" -"Cannot truncate a driver call without hints list as first parameter after" -" self " -msgstr "" - -#: keystone/common/sql/core.py:410 -msgid "Duplicate Entry" -msgstr "" - -#: keystone/common/sql/core.py:426 -#, python-format -msgid "An unexpected error occurred when trying to store %s" -msgstr "" - -#: keystone/common/sql/migration_helpers.py:187 -#: keystone/common/sql/migration_helpers.py:245 -#, python-format -msgid "%s extension does not exist." -msgstr "" +msgid "LDAP %s update" +msgstr "LDAP %s update" -#: keystone/common/validation/validators.py:54 #, python-format -msgid "Invalid input for field '%(path)s'. The value is '%(value)s'." -msgstr "" - -#: keystone/contrib/ec2/controllers.py:318 -msgid "Token belongs to another user" -msgstr "Token belongs to another user" - -#: keystone/contrib/ec2/controllers.py:346 -msgid "Credential belongs to another user" -msgstr "Credential belongs to another user" +msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." +msgstr "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." -#: keystone/contrib/endpoint_filter/backends/sql.py:69 -#, python-format -msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" -msgstr "Endpoint %(endpoint_id)s not found in project %(project_id)s" +msgid "Marker could not be found" +msgstr "Marker could not be found" -#: keystone/contrib/endpoint_filter/backends/sql.py:180 -msgid "Endpoint Group Project Association not found" -msgstr "" +msgid "Name field is required and cannot be empty" +msgstr "Name field is required and cannot be empty" -#: keystone/contrib/endpoint_policy/core.py:258 -#, python-format -msgid "No policy is associated with endpoint %(endpoint_id)s." -msgstr "" +msgid "No authenticated user" +msgstr "No authenticated user" -#: keystone/contrib/federation/controllers.py:274 -msgid "Missing entity ID from environment" -msgstr "" +msgid "No options specified" +msgstr "No options specified" -#: keystone/contrib/federation/controllers.py:282 -msgid "Request must have an origin query parameter" -msgstr "" +msgid "Non-default domain is not supported" +msgstr "Non-default domain is not supported" -#: keystone/contrib/federation/controllers.py:292 #, python-format -msgid "%(host)s is not a trusted dashboard host" -msgstr "" - -#: keystone/contrib/federation/controllers.py:333 -msgid "Use a project scoped token when attempting to create a SAML assertion" -msgstr "" +msgid "Project (%s)" +msgstr "Project (%s)" -#: keystone/contrib/federation/idp.py:454 #, python-format -msgid "Cannot open certificate %(cert_file)s. Reason: %(reason)s" -msgstr "" - -#: keystone/contrib/federation/idp.py:521 -msgid "Ensure configuration option idp_entity_id is set." -msgstr "" - -#: keystone/contrib/federation/idp.py:524 -msgid "Ensure configuration option idp_sso_endpoint is set." -msgstr "" - -#: keystone/contrib/federation/idp.py:544 -msgid "" -"idp_contact_type must be one of: [technical, other, support, " -"administrative or billing." -msgstr "" - -#: keystone/contrib/federation/utils.py:178 -msgid "Federation token is expired" -msgstr "" - -#: keystone/contrib/federation/utils.py:208 -msgid "" -"Could not find Identity Provider identifier in environment, check " -"[federation] remote_id_attribute for details." -msgstr "" - -#: keystone/contrib/federation/utils.py:213 -msgid "" -"Incoming identity provider identifier not included among the accepted " -"identifiers." -msgstr "" +msgid "Project is disabled: %s" +msgstr "Project is disabled: %s" -#: keystone/contrib/federation/utils.py:501 -#, python-format -msgid "User type %s not supported" -msgstr "" +msgid "Request Token does not have an authorizing user id" +msgstr "Request Token does not have an authorizing user id" -#: keystone/contrib/federation/utils.py:537 #, python-format msgid "" -"Invalid rule: %(identity_value)s. Both 'groups' and 'domain' keywords " -"must be specified." -msgstr "" - -#: keystone/contrib/federation/utils.py:753 -#, python-format -msgid "Identity Provider %(idp)s is disabled" -msgstr "" - -#: keystone/contrib/federation/utils.py:761 -#, python-format -msgid "Service Provider %(sp)s is disabled" -msgstr "" - -#: keystone/contrib/oauth1/controllers.py:99 -msgid "Cannot change consumer secret" -msgstr "Cannot change consumer secret" - -#: keystone/contrib/oauth1/controllers.py:131 -msgid "Cannot list request tokens with a token issued via delegation." -msgstr "" - -#: keystone/contrib/oauth1/controllers.py:192 -#: keystone/contrib/oauth1/backends/sql.py:270 -msgid "User IDs do not match" -msgstr "User IDs do not match" - -#: keystone/contrib/oauth1/controllers.py:199 -msgid "Could not find role" -msgstr "Could not find role" - -#: keystone/contrib/oauth1/controllers.py:248 -msgid "Invalid signature" +"Request attribute %(attribute)s must be less than or equal to %(size)i. The " +"server could not comply with the request because the attribute size is " +"invalid (too large). The client is assumed to be in error." msgstr "" +"Request attribute %(attribute)s must be less than or equal to %(size)i. The " +"server could not comply with the request because the attribute size is " +"invalid (too large). The client is assumed to be in error." -#: keystone/contrib/oauth1/controllers.py:299 -#: keystone/contrib/oauth1/controllers.py:377 msgid "Request token is expired" msgstr "Request token is expired" -#: keystone/contrib/oauth1/controllers.py:313 -msgid "There should not be any non-oauth parameters" -msgstr "There should not be any non-oauth parameters" - -#: keystone/contrib/oauth1/controllers.py:317 -msgid "provided consumer key does not match stored consumer key" -msgstr "provided consumer key does not match stored consumer key" - -#: keystone/contrib/oauth1/controllers.py:321 -msgid "provided verifier does not match stored verifier" -msgstr "provided verifier does not match stored verifier" - -#: keystone/contrib/oauth1/controllers.py:325 -msgid "provided request key does not match stored request key" -msgstr "provided request key does not match stored request key" - -#: keystone/contrib/oauth1/controllers.py:329 -msgid "Request Token does not have an authorizing user id" -msgstr "Request Token does not have an authorizing user id" - -#: keystone/contrib/oauth1/controllers.py:366 -msgid "Cannot authorize a request token with a token issued via delegation." -msgstr "" - -#: keystone/contrib/oauth1/controllers.py:396 -msgid "authorizing user does not have role required" -msgstr "authorizing user does not have role required" - -#: keystone/contrib/oauth1/controllers.py:409 -msgid "User is not a member of the requested project" -msgstr "User is not a member of the requested project" - -#: keystone/contrib/oauth1/backends/sql.py:91 -msgid "Consumer not found" -msgstr "Consumer not found" - -#: keystone/contrib/oauth1/backends/sql.py:186 msgid "Request token not found" msgstr "Request token not found" -#: keystone/contrib/oauth1/backends/sql.py:250 -msgid "Access token not found" -msgstr "Access token not found" - -#: keystone/contrib/revoke/controllers.py:33 -#, python-format -msgid "invalid date format %s" -msgstr "" - -#: keystone/contrib/revoke/core.py:150 -msgid "" -"The revoke call must not have both domain_id and project_id. This is a " -"bug in the Keystone server. The current request is aborted." -msgstr "" - -#: keystone/contrib/revoke/core.py:218 keystone/token/provider.py:207 -#: keystone/token/provider.py:230 keystone/token/provider.py:296 -#: keystone/token/provider.py:303 -msgid "Failed to validate token" -msgstr "Failed to validate token" - -#: keystone/identity/controllers.py:72 -msgid "Enabled field must be a boolean" -msgstr "Enabled field must be a boolean" - -#: keystone/identity/controllers.py:98 -msgid "Enabled field should be a boolean" -msgstr "Enabled field should be a boolean" - -#: keystone/identity/core.py:112 -#, python-format -msgid "Database at /domains/%s/config" -msgstr "" - -#: keystone/identity/core.py:287 keystone/identity/backends/ldap.py:59 -#: keystone/identity/backends/ldap.py:61 keystone/identity/backends/ldap.py:67 -#: keystone/identity/backends/ldap.py:69 keystone/identity/backends/sql.py:104 -#: keystone/identity/backends/sql.py:106 -msgid "Invalid user / password" -msgstr "" - -#: keystone/identity/core.py:693 -#, python-format -msgid "User is disabled: %s" -msgstr "User is disabled: %s" - -#: keystone/identity/core.py:735 -msgid "Cannot change user ID" -msgstr "" - -#: keystone/identity/backends/ldap.py:99 -msgid "Cannot change user name" -msgstr "" - -#: keystone/identity/backends/ldap.py:188 keystone/identity/backends/sql.py:188 -#: keystone/identity/backends/sql.py:206 #, python-format -msgid "User '%(user_id)s' not found in group '%(group_id)s'" -msgstr "" - -#: keystone/identity/backends/ldap.py:339 -#, python-format -msgid "User %(user_id)s is already a member of group %(group_id)s" -msgstr "User %(user_id)s is already a member of group %(group_id)s" - -#: keystone/models/token_model.py:61 -msgid "Found invalid token: scoped to both project and domain." -msgstr "" +msgid "Role %s not found" +msgstr "Role %s not found" -#: keystone/openstack/common/versionutils.py:108 -#, python-format -msgid "" -"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " -"may be removed in %(remove_in)s." -msgstr "" -"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " -"may be removed in %(remove_in)s." +msgid "Scoping to both domain and project is not allowed" +msgstr "Scoping to both domain and project is not allowed" -#: keystone/openstack/common/versionutils.py:112 -#, python-format -msgid "" -"%(what)s is deprecated as of %(as_of)s and may be removed in " -"%(remove_in)s. It will not be superseded." -msgstr "" -"%(what)s is deprecated as of %(as_of)s and may be removed in " -"%(remove_in)s. It will not be superseded." +msgid "Scoping to both domain and trust is not allowed" +msgstr "Scoping to both domain and trust is not allowed" -#: keystone/openstack/common/versionutils.py:116 -#, python-format -msgid "%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s." -msgstr "" +msgid "Scoping to both project and trust is not allowed" +msgstr "Scoping to both project and trust is not allowed" -#: keystone/openstack/common/versionutils.py:119 -#, python-format -msgid "%(what)s is deprecated as of %(as_of)s. It will not be superseded." -msgstr "" +msgid "Specify a domain or project, not both" +msgstr "Specify a domain or project, not both" -#: keystone/openstack/common/versionutils.py:241 -#, python-format -msgid "Deprecated: %s" -msgstr "Deprecated: %s" +msgid "Specify a user or group, not both" +msgstr "Specify a user or group, not both" -#: keystone/openstack/common/versionutils.py:259 #, python-format -msgid "Fatal call to deprecated config: %(msg)s" -msgstr "Fatal call to deprecated config: %(msg)s" - -#: keystone/resource/controllers.py:231 -msgid "" -"Cannot use parents_as_list and parents_as_ids query params at the same " -"time." -msgstr "" - -#: keystone/resource/controllers.py:237 msgid "" -"Cannot use subtree_as_list and subtree_as_ids query params at the same " -"time." -msgstr "" - -#: keystone/resource/core.py:80 -#, python-format -msgid "max hierarchy depth reached for %s branch." -msgstr "" - -#: keystone/resource/core.py:97 -msgid "cannot create a project within a different domain than its parents." -msgstr "" - -#: keystone/resource/core.py:101 -#, python-format -msgid "cannot create a project in a branch containing a disabled project: %s" -msgstr "" - -#: keystone/resource/core.py:123 -#, python-format -msgid "Domain is disabled: %s" -msgstr "Domain is disabled: %s" - -#: keystone/resource/core.py:141 -#, python-format -msgid "Domain cannot be named %s" +"String length exceeded.The length of string '%(string)s' exceeded the limit " +"of column %(type)s(CHAR(%(length)d))." msgstr "" +"String length exceeded.The length of string '%(string)s' exceeded the limit " +"of column %(type)s(CHAR(%(length)d))." -#: keystone/resource/core.py:144 #, python-format -msgid "Domain cannot have ID %s" -msgstr "" - -#: keystone/resource/core.py:156 -#, python-format -msgid "Project is disabled: %s" -msgstr "Project is disabled: %s" - -#: keystone/resource/core.py:176 -#, python-format -msgid "cannot enable project %s since it has disabled parents" -msgstr "" - -#: keystone/resource/core.py:184 -#, python-format -msgid "cannot disable project %s since its subtree contains enabled projects" -msgstr "" - -#: keystone/resource/core.py:195 -msgid "Update of `parent_id` is not allowed." -msgstr "" - -#: keystone/resource/core.py:222 -#, python-format -msgid "cannot delete the project %s since it is not a leaf in the hierarchy." -msgstr "" - -#: keystone/resource/core.py:376 -msgid "Multiple domains are not supported" -msgstr "" - -#: keystone/resource/core.py:429 -msgid "delete the default domain" -msgstr "" - -#: keystone/resource/core.py:440 -msgid "cannot delete a domain that is enabled, please disable it first." -msgstr "" +msgid "The Keystone configuration file %(config_file)s could not be found." +msgstr "The Keystone configuration file %(config_file)s could not be found." -#: keystone/resource/core.py:841 -msgid "No options specified" -msgstr "No options specified" +msgid "The action you have requested has not been implemented." +msgstr "The action you have requested has not been implemented." -#: keystone/resource/core.py:847 -#, python-format -msgid "" -"The value of group %(group)s specified in the config should be a " -"dictionary of options" -msgstr "" +msgid "The request you have made requires authentication." +msgstr "The request you have made requires authentication." -#: keystone/resource/core.py:871 -#, python-format -msgid "" -"Option %(option)s found with no group specified while checking domain " -"configuration request" -msgstr "" +msgid "The resource could not be found." +msgstr "The resource could not be found." -#: keystone/resource/core.py:878 -#, python-format -msgid "Group %(group)s is not supported for domain specific configurations" -msgstr "" +msgid "There should not be any non-oauth parameters" +msgstr "There should not be any non-oauth parameters" -#: keystone/resource/core.py:885 -#, python-format msgid "" -"Option %(option)s in group %(group)s is not supported for domain specific" -" configurations" -msgstr "" - -#: keystone/resource/core.py:938 -msgid "An unexpected error occurred when retrieving domain configs" -msgstr "" - -#: keystone/resource/core.py:1013 keystone/resource/core.py:1097 -#: keystone/resource/core.py:1167 keystone/resource/config_backends/sql.py:70 -#, python-format -msgid "option %(option)s in group %(group)s" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client is " +"assumed to be in error." msgstr "" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client is " +"assumed to be in error." -#: keystone/resource/core.py:1016 keystone/resource/core.py:1102 -#: keystone/resource/core.py:1163 -#, python-format -msgid "group %(group)s" -msgstr "" +msgid "Token belongs to another user" +msgstr "Token belongs to another user" -#: keystone/resource/core.py:1018 -msgid "any options" -msgstr "" +msgid "Token does not belong to specified tenant." +msgstr "Token does not belong to specified tenant." -#: keystone/resource/core.py:1062 -#, python-format -msgid "" -"Trying to update option %(option)s in group %(group)s, so that, and only " -"that, option must be specified in the config" -msgstr "" +msgid "Trustee has no delegated roles." +msgstr "Trustee has no delegated roles." -#: keystone/resource/core.py:1067 -#, python-format -msgid "" -"Trying to update group %(group)s, so that, and only that, group must be " -"specified in the config" -msgstr "" +msgid "Trustor is disabled." +msgstr "Trustor is disabled." -#: keystone/resource/core.py:1076 #, python-format -msgid "" -"request to update group %(group)s, but config provided contains group " -"%(group_other)s instead" -msgstr "" +msgid "Unable to locate domain config directory: %s" +msgstr "Unable to locate domain config directory: %s" -#: keystone/resource/core.py:1083 #, python-format -msgid "" -"Trying to update option %(option)s in group %(group)s, but config " -"provided contains option %(option_other)s instead" -msgstr "" - -#: keystone/resource/backends/ldap.py:151 -#: keystone/resource/backends/ldap.py:159 -#: keystone/resource/backends/ldap.py:163 -msgid "Domains are read-only against LDAP" -msgstr "" +msgid "Unable to lookup user %s" +msgstr "Unable to lookup user %s" -#: keystone/server/eventlet.py:77 -msgid "" -"Running keystone via eventlet is deprecated as of Kilo in favor of " -"running in a WSGI server (e.g. mod_wsgi). Support for keystone under " -"eventlet will be removed in the \"M\"-Release." -msgstr "" +msgid "Unable to sign token." +msgstr "Unable to sign token." -#: keystone/server/eventlet.py:90 -#, python-format -msgid "Failed to start the %(name)s server" -msgstr "" +msgid "Unknown Target" +msgstr "Unknown Target" -#: keystone/token/controllers.py:391 #, python-format msgid "User %(u_id)s is unauthorized for tenant %(t_id)s" msgstr "User %(u_id)s is unauthorized for tenant %(t_id)s" -#: keystone/token/controllers.py:410 keystone/token/controllers.py:413 -msgid "Token does not belong to specified tenant." -msgstr "Token does not belong to specified tenant." - -#: keystone/token/persistence/backends/kvs.py:133 #, python-format -msgid "Unknown token version %s" -msgstr "" +msgid "User %(user_id)s has no access to domain %(domain_id)s" +msgstr "User %(user_id)s has no access to domain %(domain_id)s" -#: keystone/token/providers/common.py:250 -#: keystone/token/providers/common.py:355 #, python-format msgid "User %(user_id)s has no access to project %(project_id)s" msgstr "User %(user_id)s has no access to project %(project_id)s" -#: keystone/token/providers/common.py:255 -#: keystone/token/providers/common.py:360 #, python-format -msgid "User %(user_id)s has no access to domain %(domain_id)s" -msgstr "User %(user_id)s has no access to domain %(domain_id)s" - -#: keystone/token/providers/common.py:282 -msgid "Trustor is disabled." -msgstr "Trustor is disabled." +msgid "User %(user_id)s is already a member of group %(group_id)s" +msgstr "User %(user_id)s is already a member of group %(group_id)s" -#: keystone/token/providers/common.py:346 -msgid "Trustee has no delegated roles." -msgstr "Trustee has no delegated roles." +msgid "User IDs do not match" +msgstr "User IDs do not match" -#: keystone/token/providers/common.py:407 #, python-format -msgid "Invalid audit info data type: %(data)s (%(type)s)" -msgstr "" +msgid "User is disabled: %s" +msgstr "User is disabled: %s" + +msgid "User is not a member of the requested project" +msgstr "User is not a member of the requested project" -#: keystone/token/providers/common.py:435 msgid "User is not a trustee." msgstr "User is not a trustee." -#: keystone/token/providers/common.py:579 -msgid "" -"Attempting to use OS-FEDERATION token with V2 Identity Service, use V3 " -"Authentication" -msgstr "" +msgid "User not found" +msgstr "User not found" -#: keystone/token/providers/common.py:597 -msgid "Domain scoped token is not supported" -msgstr "Domain scoped token is not supported" +msgid "You are not authorized to perform the requested action." +msgstr "You are not authorized to perform the requested action." -#: keystone/token/providers/pki.py:48 keystone/token/providers/pkiz.py:30 -msgid "Unable to sign token." -msgstr "Unable to sign token." +msgid "authorizing user does not have role required" +msgstr "authorizing user does not have role required" -#: keystone/token/providers/fernet/core.py:215 -msgid "" -"This is not a v2.0 Fernet token. Use v3 for trust, domain, or federated " -"tokens." -msgstr "" +msgid "pad must be single character" +msgstr "pad must be single character" -#: keystone/token/providers/fernet/token_formatters.py:189 -#, python-format -msgid "This is not a recognized Fernet payload version: %s" -msgstr "" +msgid "padded base64url text must be multiple of 4 characters" +msgstr "padded base64url text must be multiple of 4 characters" -#: keystone/trust/controllers.py:148 -msgid "Redelegation allowed for delegated by trust only" -msgstr "" +msgid "provided consumer key does not match stored consumer key" +msgstr "provided consumer key does not match stored consumer key" -#: keystone/trust/controllers.py:181 -msgid "The authenticated user should match the trustor." -msgstr "" +msgid "provided request key does not match stored request key" +msgstr "provided request key does not match stored request key" -#: keystone/trust/controllers.py:186 -msgid "At least one role should be specified." -msgstr "" +msgid "provided verifier does not match stored verifier" +msgstr "provided verifier does not match stored verifier" -#: keystone/trust/core.py:57 -#, python-format -msgid "" -"Remaining redelegation depth of %(redelegation_depth)d out of allowed " -"range of [0..%(max_count)d]" -msgstr "" +msgid "region not type dogpile.cache.CacheRegion" +msgstr "region not type dogpile.cache.CacheRegion" -#: keystone/trust/core.py:66 #, python-format -msgid "" -"Field \"remaining_uses\" is set to %(value)s while it must not be set in " -"order to redelegate a trust" -msgstr "" - -#: keystone/trust/core.py:77 -msgid "Requested expiration time is more than redelegated trust can provide" -msgstr "" - -#: keystone/trust/core.py:87 -msgid "Some of requested roles are not in redelegated trust" -msgstr "" - -#: keystone/trust/core.py:116 -msgid "One of the trust agents is disabled or deleted" -msgstr "" - -#: keystone/trust/core.py:135 -msgid "remaining_uses must be a positive integer or null." -msgstr "" +msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" +msgstr "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" -#: keystone/trust/core.py:141 #, python-format -msgid "" -"Requested redelegation depth of %(requested_count)d is greater than " -"allowed %(max_count)d" -msgstr "" +msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" +msgstr "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" -#: keystone/trust/core.py:147 -msgid "remaining_uses must not be set if redelegation is allowed" -msgstr "" +#, python-format +msgid "text is not a multiple of 4, but contains pad \"%s\"" +msgstr "text is not a multiple of 4, but contains pad \"%s\"" -#: keystone/trust/core.py:157 -msgid "" -"Modifying \"redelegation_count\" upon redelegation is forbidden. Omitting" -" this parameter is advised." -msgstr "" +#, python-format +msgid "tls_cacertdir %s not found or is not a directory" +msgstr "tls_cacertdir %s not found or is not a directory" +#, python-format +msgid "tls_cacertfile %s not found or is not a file" +msgstr "tls_cacertfile %s not found or is not a file" diff --git a/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po index 6ebff226..336c5d33 100644 --- a/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Spanish (http://www.transifex.com/projects/p/keystone/" -"language/es/)\n" +"Language-Team: Spanish (http://www.transifex.com/openstack/keystone/language/" +"es/)\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "No se puede abrir el archivo de plantilla %s" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po index c40440be..8657e66a 100644 --- a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"Language-Team: French (http://www.transifex.com/openstack/keystone/language/" "fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Impossible d'ouvrir le fichier modèle %s" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po index d8dc409f..ba787ee3 100644 --- a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po @@ -9,70 +9,33 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" -"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-06-26 17:13+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"Language-Team: French (http://www.transifex.com/openstack/keystone/language/" "fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: keystone/notifications.py:304 -msgid "Failed to construct notifier" -msgstr "Échec de construction de la notification" - -#: keystone/notifications.py:389 -#, python-format -msgid "Failed to send %(res_id)s %(event_type)s notification" -msgstr "Échec de l'envoi de la notification %(res_id)s %(event_type)s" - -#: keystone/notifications.py:606 -#, python-format -msgid "Failed to send %(action)s %(event_type)s notification" -msgstr "Échec de l'envoi de la notification %(action)s %(event_type)s " - -#: keystone/catalog/core.py:62 -#, python-format -msgid "Malformed endpoint - %(url)r is not a string" -msgstr "Critère mal formé - %(url)r n'est pas une chaine de caractère" - -#: keystone/catalog/core.py:66 -#, python-format -msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" -msgstr "Noeud final incorrect %(url)s - clé inconnue %(keyerror)s" - -#: keystone/catalog/core.py:71 #, python-format msgid "" -"Malformed endpoint '%(url)s'. The following type error occurred during " -"string substitution: %(typeerror)s" -msgstr "" -"Noeud final incorrect '%(url)s'. L'erreur suivante est survenue pendant la " -"substitution de chaine : %(typeerror)s" - -#: keystone/catalog/core.py:77 -#, python-format -msgid "" -"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +"Circular reference or a repeated entry found in region tree - %(region_id)s." msgstr "" -"Noeud final incorrect '%s - Format incomplet (un type de notification manque-" -"t-il ?)" +"Référence circulaire ou entrée dupliquée trouvée dans l'arbre de la région - " +"%(region_id)s." -#: keystone/common/openssl.py:93 #, python-format msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" msgstr "La commande %(to_exec)s a retourné %(retcode)s- %(output)s" -#: keystone/common/openssl.py:121 #, python-format -msgid "Failed to remove file %(file_path)r: %(error)s" -msgstr "Échec de la suppression du fichier %(file_path)r: %(error)s" +msgid "Could not bind to %(host)s:%(port)s" +msgstr "Impossible de s'attacher à %(host)s:%(port)s" -#: keystone/common/utils.py:239 msgid "" "Error setting up the debug environment. Verify that the option --debug-url " "has the format <host>:<port> and that a debugger processes is listening on " @@ -82,103 +45,53 @@ msgstr "" "l'option --debug-url a le format <host>:<port> et que le processus de " "débogage écoute sur ce port." -#: keystone/common/cache/core.py:100 #, python-format -msgid "" -"Unable to build cache config-key. Expected format \"<argname>:<value>\". " -"Skipping unknown format: %s" -msgstr "" - -#: keystone/common/environment/eventlet_server.py:99 -#, python-format -msgid "Could not bind to %(host)s:%(port)s" -msgstr "Impossible de s'attacher à %(host)s:%(port)s" +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "Erreur lors de la signature d'une assertion : %(reason)s" -#: keystone/common/environment/eventlet_server.py:185 -msgid "Server error" -msgstr "Erreur serveur" +msgid "Failed to construct notifier" +msgstr "Échec de construction de la notification" -#: keystone/contrib/endpoint_policy/core.py:129 -#: keystone/contrib/endpoint_policy/core.py:228 #, python-format -msgid "" -"Circular reference or a repeated entry found in region tree - %(region_id)s." -msgstr "" -"Référence circulaire ou entrée dupliquée trouvée dans l'arbre de la région - " -"%(region_id)s." +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "Échec de la suppression du fichier %(file_path)r: %(error)s" -#: keystone/contrib/federation/idp.py:410 #, python-format -msgid "Error when signing assertion, reason: %(reason)s" -msgstr "Erreur lors de la signature d'une assertion : %(reason)s" - -#: keystone/contrib/oauth1/core.py:136 -msgid "Cannot retrieve Authorization headers" -msgstr "" - -#: keystone/openstack/common/loopingcall.py:95 -msgid "in fixed duration looping call" -msgstr "dans l'appel en boucle de durée fixe" +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "Échec de l'envoi de la notification %(action)s %(event_type)s " -#: keystone/openstack/common/loopingcall.py:138 -msgid "in dynamic looping call" -msgstr "dans l'appel en boucle dynamique" +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "Échec de l'envoi de la notification %(res_id)s %(event_type)s" -#: keystone/openstack/common/service.py:268 -msgid "Unhandled exception" -msgstr "Exception non gérée" +msgid "Failed to validate token" +msgstr "Echec de validation du token" -#: keystone/resource/core.py:477 #, python-format -msgid "" -"Circular reference or a repeated entry found projects hierarchy - " -"%(project_id)s." -msgstr "" +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "Noeud final incorrect %(url)s - clé inconnue %(keyerror)s" -#: keystone/resource/core.py:939 #, python-format msgid "" -"Unexpected results in response for domain config - %(count)s responses, " -"first option is %(option)s, expected option %(expected)s" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" msgstr "" +"Noeud final incorrect '%s - Format incomplet (un type de notification manque-" +"t-il ?)" -#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 #, python-format msgid "" -"Circular reference or a repeated entry found in projects hierarchy - " -"%(project_id)s." -msgstr "" - -#: keystone/token/provider.py:292 -#, python-format -msgid "Unexpected error or malformed token determining token expiry: %s" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" msgstr "" +"Noeud final incorrect '%(url)s'. L'erreur suivante est survenue pendant la " +"substitution de chaine : %(typeerror)s" -#: keystone/token/persistence/backends/kvs.py:226 #, python-format -msgid "" -"Reinitializing revocation list due to error in loading revocation list from " -"backend. Expected `list` type got `%(type)s`. Old revocation list data: " -"%(list)r" -msgstr "" +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "Critère mal formé - %(url)r n'est pas une chaine de caractère" -#: keystone/token/providers/common.py:611 -msgid "Failed to validate token" -msgstr "Echec de validation du token" +msgid "Server error" +msgstr "Erreur serveur" -#: keystone/token/providers/pki.py:47 msgid "Unable to sign token" msgstr "Impossible de signer le jeton" - -#: keystone/token/providers/fernet/utils.py:38 -#, python-format -msgid "" -"Either [fernet_tokens] key_repository does not exist or Keystone does not " -"have sufficient permission to access it: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:79 -msgid "" -"Failed to create [fernet_tokens] key_repository: either it already exists or " -"you don't have sufficient permissions to create it" -msgstr "" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po index 065540dc..08cee0e0 100644 --- a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po @@ -5,38 +5,23 @@ # Translators: # Bruno Cornec <bruno.cornec@hp.com>, 2014 # Maxime COQUEREL <max.coquerel@gmail.com>, 2014 -# Andrew_Melim <nokostya.translation@gmail.com>, 2014 +# Andrew Melim <nokostya.translation@gmail.com>, 2014 msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" -"PO-Revision-Date: 2015-03-08 17:01+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-08-01 06:26+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"Language-Team: French (http://www.transifex.com/openstack/keystone/language/" "fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: keystone/assignment/core.py:250 -#, python-format -msgid "Creating the default role %s because it does not exist." -msgstr "Création du rôle par défaut %s, car il n'existe pas" - -#: keystone/assignment/core.py:258 -#, python-format -msgid "Creating the default role %s failed because it was already created" -msgstr "" - -#: keystone/auth/controllers.py:64 -msgid "Loading auth-plugins by class-name is deprecated." -msgstr "Chargement de auth-plugins par class-name est déprécié" - -#: keystone/auth/controllers.py:106 #, python-format msgid "" "\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " @@ -45,68 +30,41 @@ msgstr "" "\"expires_at\" a des valeurs conflictuelles %(existing)s et %(new)s. " "Utilsation de la première valeur." -#: keystone/common/openssl.py:81 #, python-format -msgid "Running command - %s" -msgstr "Exécution de la commande %s" - -#: keystone/common/wsgi.py:79 -msgid "No bind information present in token" -msgstr "Aucune information d'attachement n'est présente dans le jeton" - -#: keystone/common/wsgi.py:83 -#, python-format -msgid "Named bind mode %s not in bind information" -msgstr "" -"Le mode d'attachement nommé %s n'est pas dans l'information d'attachement" - -#: keystone/common/wsgi.py:90 -msgid "Kerberos credentials required and not present" -msgstr "L'identitification Kerberos est requise mais non présente" - -#: keystone/common/wsgi.py:94 -msgid "Kerberos credentials do not match those in bind" -msgstr "L'identification Kerberos ne correspond pas à celle de l'attachement" - -#: keystone/common/wsgi.py:98 -msgid "Kerberos bind authentication successful" -msgstr "Attachement Kerberos identifié correctement" +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "Ahour du mandataire '%(proxy)s' au KVS %(name)s." -#: keystone/common/wsgi.py:105 #, python-format msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" msgstr "" "Impossible de vérifier l'attachement inconnu: {%(bind_type)s: " "%(identifier)s}" -#: keystone/common/environment/eventlet_server.py:103 #, python-format -msgid "Starting %(arg0)s on %(host)s:%(port)s" -msgstr "Démarrage de %(arg0)s sur %(host)s:%(port)s" +msgid "Creating the default role %s because it does not exist." +msgstr "Création du rôle par défaut %s, car il n'existe pas" -#: keystone/common/kvs/core.py:138 #, python-format -msgid "Adding proxy '%(proxy)s' to KVS %(name)s." -msgstr "Ahour du mandataire '%(proxy)s' au KVS %(name)s." +msgid "KVS region %s key_mangler disabled." +msgstr "Région KVS %s key_mangler désactivée" -#: keystone/common/kvs/core.py:188 -#, python-format -msgid "Using %(func)s as KVS region %(name)s key_mangler" -msgstr "Utilise %(func)s comme région KVS %(name)s key_mangler" +msgid "Kerberos bind authentication successful" +msgstr "Attachement Kerberos identifié correctement" + +msgid "Kerberos credentials do not match those in bind" +msgstr "L'identification Kerberos ne correspond pas à celle de l'attachement" + +msgid "Kerberos credentials required and not present" +msgstr "L'identitification Kerberos est requise mais non présente" -#: keystone/common/kvs/core.py:200 #, python-format -msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgid "Named bind mode %s not in bind information" msgstr "" -"Utilisation du dogpile sha1_mangle_key par défaut comme région KVS %s " -"key_mangler" +"Le mode d'attachement nommé %s n'est pas dans l'information d'attachement" -#: keystone/common/kvs/core.py:210 -#, python-format -msgid "KVS region %s key_mangler disabled." -msgstr "Région KVS %s key_mangler désactivée" +msgid "No bind information present in token" +msgstr "Aucune information d'attachement n'est présente dans le jeton" -#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 #, python-format msgid "" "Received the following notification: service %(service)s, resource_type: " @@ -115,109 +73,24 @@ msgstr "" "Réception de la notification suivante: service %(service)s, resource_type: " "%(resource_type)s, operation %(operation)s payload %(payload)s" -#: keystone/openstack/common/eventlet_backdoor.py:146 -#, python-format -msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" -msgstr "Eventlet backdoor en écoute sur le port %(port)s for process %(pid)d" - -#: keystone/openstack/common/service.py:173 -#, python-format -msgid "Caught %s, exiting" -msgstr "%s interceptée, sortie" - -#: keystone/openstack/common/service.py:231 -msgid "Parent process has died unexpectedly, exiting" -msgstr "Processus parent arrêté de manière inattendue, sortie" - -#: keystone/openstack/common/service.py:262 -#, python-format -msgid "Child caught %s, exiting" -msgstr "L'enfant a reçu %s, sortie" - -#: keystone/openstack/common/service.py:301 -msgid "Forking too fast, sleeping" -msgstr "Bifurcation trop rapide, pause" - -#: keystone/openstack/common/service.py:320 #, python-format -msgid "Started child %d" -msgstr "Enfant démarré %d" - -#: keystone/openstack/common/service.py:330 -#, python-format -msgid "Starting %d workers" -msgstr "Démarrage des travailleurs %d" - -#: keystone/openstack/common/service.py:347 -#, python-format -msgid "Child %(pid)d killed by signal %(sig)d" -msgstr "Enfant %(pid)d arrêté par le signal %(sig)d" - -#: keystone/openstack/common/service.py:351 -#, python-format -msgid "Child %(pid)s exited with status %(code)d" -msgstr "Processus fils %(pid)s terminé avec le status %(code)d" - -#: keystone/openstack/common/service.py:390 -#, python-format -msgid "Caught %s, stopping children" -msgstr "%s interceptée, arrêt de l'enfant" - -#: keystone/openstack/common/service.py:399 -msgid "Wait called after thread killed. Cleaning up." -msgstr "Pause demandée après suppression de thread. Nettoyage." +msgid "Running command - %s" +msgstr "Exécution de la commande %s" -#: keystone/openstack/common/service.py:415 #, python-format -msgid "Waiting on %d children to exit" -msgstr "En attente %d enfants pour sortie" +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "Démarrage de %(arg0)s sur %(host)s:%(port)s" -#: keystone/token/persistence/backends/sql.py:279 #, python-format msgid "Total expired tokens removed: %d" msgstr "Total des jetons expirés effacés: %d" -#: keystone/token/providers/fernet/utils.py:72 -msgid "" -"[fernet_tokens] key_repository does not appear to exist; attempting to " -"create it" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:130 -#, python-format -msgid "Created a new key: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:143 -msgid "Key repository is already initialized; aborting." -msgstr "" - -#: keystone/token/providers/fernet/utils.py:179 -#, python-format -msgid "Starting key rotation with %(count)s key files: %(list)s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:185 -#, python-format -msgid "Current primary key is: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:187 -#, python-format -msgid "Next primary key will be: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:197 -#, python-format -msgid "Promoted key 0 to be the primary: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:213 #, python-format -msgid "Excess keys to purge: %s" -msgstr "" +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "Utilise %(func)s comme région KVS %(name)s key_mangler" -#: keystone/token/providers/fernet/utils.py:237 #, python-format -msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" msgstr "" +"Utilisation du dogpile sha1_mangle_key par défaut comme région KVS %s " +"key_mangler" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po index a83b88a5..d2fddf29 100644 --- a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po @@ -9,142 +9,34 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-19 06:04+0000\n" -"PO-Revision-Date: 2015-03-19 02:24+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-07-29 06:04+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"Language-Team: French (http://www.transifex.com/openstack/keystone/language/" "fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: keystone/cli.py:159 -msgid "keystone-manage pki_setup is not recommended for production use." -msgstr "" -"keystone-manage pki_setup n'est pas recommandé pour une utilisation en " -"production." - -#: keystone/cli.py:178 -msgid "keystone-manage ssl_setup is not recommended for production use." -msgstr "" -"keystone-manage ssl_setup n'est pas recommandé pour une utilisation en " -"production." - -#: keystone/cli.py:493 -#, python-format -msgid "Ignoring file (%s) while scanning domain config directory" -msgstr "" - -#: keystone/exception.py:49 -msgid "missing exception kwargs (programmer error)" -msgstr "" - -#: keystone/assignment/controllers.py:60 -#, python-format -msgid "Authentication failed: %s" -msgstr "L'authentification a échoué: %s" - -#: keystone/assignment/controllers.py:576 -#, python-format -msgid "" -"Group %(group)s not found for role-assignment - %(target)s with Role: " -"%(role)s" -msgstr "" - -#: keystone/auth/controllers.py:449 -#, python-format -msgid "" -"User %(user_id)s doesn't have access to default project %(project_id)s. The " -"token will be unscoped rather than scoped to the project." -msgstr "" - -#: keystone/auth/controllers.py:457 -#, python-format -msgid "" -"User %(user_id)s's default project %(project_id)s is disabled. The token " -"will be unscoped rather than scoped to the project." -msgstr "" - -#: keystone/auth/controllers.py:466 -#, python-format -msgid "" -"User %(user_id)s's default project %(project_id)s not found. The token will " -"be unscoped rather than scoped to the project." -msgstr "" - -#: keystone/common/authorization.py:55 -msgid "RBAC: Invalid user data in token" -msgstr "RBAC: Donnée utilisation non valide dans le token" - -#: keystone/common/controller.py:79 keystone/middleware/core.py:224 -msgid "RBAC: Invalid token" -msgstr "RBAC : Jeton non valide" - -#: keystone/common/controller.py:104 keystone/common/controller.py:201 -#: keystone/common/controller.py:740 -msgid "RBAC: Bypassing authorization" -msgstr "RBAC : Autorisation ignorée" - -#: keystone/common/controller.py:669 keystone/common/controller.py:704 -msgid "Invalid token found while getting domain ID for list request" -msgstr "" - -#: keystone/common/controller.py:677 -msgid "No domain information specified as part of list request" -msgstr "" - -#: keystone/common/utils.py:103 -#, python-format -msgid "Truncating user password to %d characters." -msgstr "" - -#: keystone/common/wsgi.py:242 -#, python-format -msgid "Authorization failed. %(exception)s from %(remote_addr)s" -msgstr "Echec d'autorisation. %(exception)s depuis %(remote_addr)s" - -#: keystone/common/wsgi.py:361 -msgid "Invalid token in _get_trust_id_for_request" -msgstr "Jeton invalide dans _get_trust_id_for_request" - -#: keystone/common/cache/backends/mongo.py:403 -#, python-format -msgid "" -"TTL index already exists on db collection <%(c_name)s>, remove index <" -"%(indx_name)s> first to make updated mongo_ttl_seconds value to be effective" -msgstr "" - -#: keystone/common/kvs/core.py:134 #, python-format msgid "%s is not a dogpile.proxy.ProxyBackend" msgstr "%s n'est pas un dogpile.proxy.ProxyBackend" -#: keystone/common/kvs/core.py:403 #, python-format -msgid "KVS lock released (timeout reached) for: %s" -msgstr "Verrou KVS relaché (temps limite atteint) pour : %s" - -#: keystone/common/ldap/core.py:1026 -msgid "" -"LDAP Server does not support paging. Disable paging in keystone.conf to " -"avoid this message." -msgstr "" -"Le serveur LDAP ne prend pas en charge la pagination. Désactivez la " -"pagination dans keystone.conf pour éviter de recevoir ce message." +msgid "Authorization failed. %(exception)s from %(remote_addr)s" +msgstr "Echec d'autorisation. %(exception)s depuis %(remote_addr)s" -#: keystone/common/ldap/core.py:1225 #, python-format msgid "" -"Invalid additional attribute mapping: \"%s\". Format must be " -"<ldap_attribute>:<keystone_attribute>" +"Endpoint %(endpoint_id)s referenced in association for policy %(policy_id)s " +"not found." msgstr "" -"Mauvais mappage d'attribut additionnel: \"%s\". Le format doit être " -"<ldap_attribute>:<keystone_attribute>" +"Le point d'entrée %(endpoint_id)s référencé en association avec la politique " +"%(policy_id)s est introuvable." -#: keystone/common/ldap/core.py:1336 #, python-format msgid "" "ID attribute %(id_attr)s for LDAP object %(dn)s has multiple values and " @@ -154,150 +46,56 @@ msgstr "" "par conséquent ne peut être utilisé comme un ID. Obtention de l'ID depuis le " "DN à la place." -#: keystone/common/ldap/core.py:1669 #, python-format msgid "" -"When deleting entries for %(search_base)s, could not delete nonexistent " -"entries %(entries)s%(dots)s" +"Invalid additional attribute mapping: \"%s\". Format must be " +"<ldap_attribute>:<keystone_attribute>" msgstr "" +"Mauvais mappage d'attribut additionnel: \"%s\". Le format doit être " +"<ldap_attribute>:<keystone_attribute>" -#: keystone/contrib/endpoint_policy/core.py:91 #, python-format -msgid "" -"Endpoint %(endpoint_id)s referenced in association for policy %(policy_id)s " -"not found." -msgstr "" -"Le point d'entrée %(endpoint_id)s référencé en association avec la politique " -"%(policy_id)s est introuvable." +msgid "Invalid domain name (%s) found in config file name" +msgstr "Non de domaine trouvé non valide (%s) dans le fichier de configuration" -#: keystone/contrib/endpoint_policy/core.py:179 #, python-format -msgid "" -"Unsupported policy association found - Policy %(policy_id)s, Endpoint " -"%(endpoint_id)s, Service %(service_id)s, Region %(region_id)s, " -msgstr "" +msgid "KVS lock released (timeout reached) for: %s" +msgstr "Verrou KVS relaché (temps limite atteint) pour : %s" -#: keystone/contrib/endpoint_policy/core.py:195 -#, python-format msgid "" -"Policy %(policy_id)s referenced in association for endpoint %(endpoint_id)s " -"not found." +"LDAP Server does not support paging. Disable paging in keystone.conf to " +"avoid this message." msgstr "" +"Le serveur LDAP ne prend pas en charge la pagination. Désactivez la " +"pagination dans keystone.conf pour éviter de recevoir ce message." -#: keystone/contrib/federation/utils.py:200 -#, python-format -msgid "Impossible to identify the IdP %s " -msgstr "" +msgid "RBAC: Bypassing authorization" +msgstr "RBAC : Autorisation ignorée" -#: keystone/contrib/federation/utils.py:523 -msgid "Ignoring user name" -msgstr "" +msgid "RBAC: Invalid token" +msgstr "RBAC : Jeton non valide" -#: keystone/identity/controllers.py:139 -#, python-format -msgid "Unable to remove user %(user)s from %(tenant)s." -msgstr "Impossible de supprimer l'utilisateur %(user)s depuis %(tenant)s." +msgid "RBAC: Invalid user data in token" +msgstr "RBAC: Donnée utilisation non valide dans le token" -#: keystone/identity/controllers.py:158 #, python-format msgid "Unable to add user %(user)s to %(tenant)s." msgstr "Impossible d'ajouter l'utilisateur %(user)s à %(tenant)s." -#: keystone/identity/core.py:122 -#, python-format -msgid "Invalid domain name (%s) found in config file name" -msgstr "Non de domaine trouvé non valide (%s) dans le fichier de configuration" - -#: keystone/identity/core.py:160 #, python-format msgid "Unable to locate domain config directory: %s" msgstr "Impossible de localiser le répertoire de configuration domaine: %s" -#: keystone/middleware/core.py:149 -msgid "" -"XML support has been removed as of the Kilo release and should not be " -"referenced or used in deployment. Please remove references to " -"XmlBodyMiddleware from your configuration. This compatibility stub will be " -"removed in the L release" -msgstr "" - -#: keystone/middleware/core.py:234 -msgid "Auth context already exists in the request environment" -msgstr "" - -#: keystone/openstack/common/loopingcall.py:87 -#, python-format -msgid "task %(func_name)r run outlasted interval by %(delay).2f sec" -msgstr "" - -#: keystone/openstack/common/service.py:351 #, python-format -msgid "pid %d not in child list" -msgstr "PID %d absent de la liste d'enfants" - -#: keystone/resource/core.py:1214 -#, python-format -msgid "" -"Found what looks like an unmatched config option substitution reference - " -"domain: %(domain)s, group: %(group)s, option: %(option)s, value: %(value)s. " -"Perhaps the config option to which it refers has yet to be added?" -msgstr "" - -#: keystone/resource/core.py:1221 -#, python-format -msgid "" -"Found what looks like an incorrectly constructed config option substitution " -"reference - domain: %(domain)s, group: %(group)s, option: %(option)s, value: " -"%(value)s." -msgstr "" - -#: keystone/token/persistence/core.py:228 -#, python-format -msgid "" -"`token_api.%s` is deprecated as of Juno in favor of utilizing methods on " -"`token_provider_api` and may be removed in Kilo." -msgstr "" - -#: keystone/token/persistence/backends/kvs.py:57 -msgid "" -"It is recommended to only use the base key-value-store implementation for " -"the token driver for testing purposes. Please use keystone.token.persistence." -"backends.memcache.Token or keystone.token.persistence.backends.sql.Token " -"instead." -msgstr "" - -#: keystone/token/persistence/backends/kvs.py:206 -#, python-format -msgid "Token `%s` is expired, not adding to the revocation list." -msgstr "" - -#: keystone/token/persistence/backends/kvs.py:240 -#, python-format -msgid "" -"Removing `%s` from revocation list due to invalid expires data in revocation " -"list." -msgstr "" - -#: keystone/token/providers/fernet/utils.py:46 -#, python-format -msgid "[fernet_tokens] key_repository is world readable: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:90 -#, python-format -msgid "" -"Unable to change the ownership of [fernet_tokens] key_repository without a " -"keystone user ID and keystone group ID both being provided: %s" -msgstr "" +msgid "Unable to remove user %(user)s from %(tenant)s." +msgstr "Impossible de supprimer l'utilisateur %(user)s depuis %(tenant)s." -#: keystone/token/providers/fernet/utils.py:112 -#, python-format -msgid "" -"Unable to change the ownership of the new key without a keystone user ID and " -"keystone group ID both being provided: %s" +msgid "keystone-manage pki_setup is not recommended for production use." msgstr "" +"keystone-manage pki_setup n'est pas recommandé pour une utilisation en " +"production." -#: keystone/token/providers/fernet/utils.py:204 -msgid "" -"[fernet_tokens] max_active_keys must be at least 1 to maintain a primary key." +msgid "keystone-manage ssl_setup is not recommended for production use." msgstr "" +"keystone-manage ssl_setup n'est pas recommandé pour une utilisation en " +"production." diff --git a/keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po index 767c150e..102329f6 100644 --- a/keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Hungarian (http://www.transifex.com/projects/p/keystone/" +"Language-Team: Hungarian (http://www.transifex.com/openstack/keystone/" "language/hu/)\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Nem nyitható meg a sablonfájl: %s" diff --git a/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po index 35010103..db15042f 100644 --- a/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Italian (http://www.transifex.com/projects/p/keystone/" -"language/it/)\n" +"Language-Team: Italian (http://www.transifex.com/openstack/keystone/language/" +"it/)\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Impossibile aprire il file di template %s" diff --git a/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po index b83aaad2..e5ec3075 100644 --- a/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Japanese (http://www.transifex.com/projects/p/keystone/" +"Language-Team: Japanese (http://www.transifex.com/openstack/keystone/" "language/ja/)\n" "Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "テンプレートファイル %s を開けません" diff --git a/keystone-moon/keystone/locale/keystone-log-critical.pot b/keystone-moon/keystone/locale/keystone-log-critical.pot index e07dd7a9..e6a96bf1 100644 --- a/keystone-moon/keystone/locale/keystone-log-critical.pot +++ b/keystone-moon/keystone/locale/keystone-log-critical.pot @@ -1,21 +1,21 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2015. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: keystone 2014.2.dev28.g7e410ae\n" +"Project-Id-Version: keystone 8.0.0.0b3.dev14\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-01 06:07+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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" #: keystone/catalog/backends/templated.py:106 #, python-format diff --git a/keystone-moon/keystone/locale/keystone-log-error.pot b/keystone-moon/keystone/locale/keystone-log-error.pot index bca25a19..375fb4b8 100644 --- a/keystone-moon/keystone/locale/keystone-log-error.pot +++ b/keystone-moon/keystone/locale/keystone-log-error.pot @@ -6,49 +6,49 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: keystone 2015.1.dev362\n" +"Project-Id-Version: keystone 8.0.0.0b3.dev14\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"POT-Creation-Date: 2015-08-01 06:07+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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" -#: keystone/notifications.py:304 +#: keystone/notifications.py:396 msgid "Failed to construct notifier" msgstr "" -#: keystone/notifications.py:389 +#: keystone/notifications.py:491 #, python-format msgid "Failed to send %(res_id)s %(event_type)s notification" msgstr "" -#: keystone/notifications.py:606 +#: keystone/notifications.py:760 #, python-format msgid "Failed to send %(action)s %(event_type)s notification" msgstr "" -#: keystone/catalog/core.py:62 +#: keystone/catalog/core.py:63 #, python-format msgid "Malformed endpoint - %(url)r is not a string" msgstr "" -#: keystone/catalog/core.py:66 +#: keystone/catalog/core.py:68 #, python-format msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" msgstr "" -#: keystone/catalog/core.py:71 +#: keystone/catalog/core.py:76 #, python-format msgid "" "Malformed endpoint '%(url)s'. The following type error occurred during " "string substitution: %(typeerror)s" msgstr "" -#: keystone/catalog/core.py:77 +#: keystone/catalog/core.py:82 #, python-format msgid "" "Malformed endpoint %s - incomplete format (are you missing a type " @@ -65,7 +65,7 @@ msgstr "" msgid "Failed to remove file %(file_path)r: %(error)s" msgstr "" -#: keystone/common/utils.py:239 +#: keystone/common/utils.py:241 msgid "" "Error setting up the debug environment. Verify that the option --debug-" "url has the format <host>:<port> and that a debugger processes is " @@ -79,24 +79,16 @@ msgid "" "Skipping unknown format: %s" msgstr "" -#: keystone/common/environment/eventlet_server.py:99 +#: keystone/common/environment/eventlet_server.py:112 #, python-format msgid "Could not bind to %(host)s:%(port)s" msgstr "" -#: keystone/common/environment/eventlet_server.py:185 +#: keystone/common/environment/eventlet_server.py:205 msgid "Server error" msgstr "" -#: keystone/contrib/endpoint_policy/core.py:129 -#: keystone/contrib/endpoint_policy/core.py:228 -#, python-format -msgid "" -"Circular reference or a repeated entry found in region tree - " -"%(region_id)s." -msgstr "" - -#: keystone/contrib/federation/idp.py:410 +#: keystone/contrib/federation/idp.py:428 #, python-format msgid "Error when signing assertion, reason: %(reason)s" msgstr "" @@ -105,45 +97,40 @@ msgstr "" msgid "Cannot retrieve Authorization headers" msgstr "" -#: keystone/openstack/common/loopingcall.py:95 -msgid "in fixed duration looping call" -msgstr "" - -#: keystone/openstack/common/loopingcall.py:138 -msgid "in dynamic looping call" -msgstr "" - -#: keystone/openstack/common/service.py:268 -msgid "Unhandled exception" +#: keystone/endpoint_policy/core.py:132 keystone/endpoint_policy/core.py:231 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - " +"%(region_id)s." msgstr "" -#: keystone/resource/core.py:477 +#: keystone/resource/core.py:485 #, python-format msgid "" "Circular reference or a repeated entry found projects hierarchy - " "%(project_id)s." msgstr "" -#: keystone/resource/core.py:939 +#: keystone/resource/core.py:950 #, python-format msgid "" "Unexpected results in response for domain config - %(count)s responses, " "first option is %(option)s, expected option %(expected)s" msgstr "" -#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#: keystone/resource/backends/sql.py:101 keystone/resource/backends/sql.py:120 #, python-format msgid "" "Circular reference or a repeated entry found in projects hierarchy - " "%(project_id)s." msgstr "" -#: keystone/token/provider.py:292 +#: keystone/token/provider.py:284 #, python-format msgid "Unexpected error or malformed token determining token expiry: %s" msgstr "" -#: keystone/token/persistence/backends/kvs.py:226 +#: keystone/token/persistence/backends/kvs.py:225 #, python-format msgid "" "Reinitializing revocation list due to error in loading revocation list " @@ -151,7 +138,7 @@ msgid "" "data: %(list)r" msgstr "" -#: keystone/token/providers/common.py:611 +#: keystone/token/providers/common.py:678 msgid "Failed to validate token" msgstr "" @@ -166,6 +153,11 @@ msgid "" " have sufficient permission to access it: %s" msgstr "" +#: keystone/token/providers/fernet/utils.py:62 +#, python-format +msgid "Unable to convert Keystone user or group ID. Error: %s" +msgstr "" + #: keystone/token/providers/fernet/utils.py:79 msgid "" "Failed to create [fernet_tokens] key_repository: either it already exists" diff --git a/keystone-moon/keystone/locale/keystone-log-info.pot b/keystone-moon/keystone/locale/keystone-log-info.pot index 17abd1df..f4c52cd4 100644 --- a/keystone-moon/keystone/locale/keystone-log-info.pot +++ b/keystone-moon/keystone/locale/keystone-log-info.pot @@ -6,16 +6,16 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: keystone 2015.1.dev362\n" +"Project-Id-Version: keystone 8.0.0.0b3.dev45\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" #: keystone/assignment/core.py:250 #, python-format @@ -27,11 +27,7 @@ msgstr "" msgid "Creating the default role %s failed because it was already created" msgstr "" -#: keystone/auth/controllers.py:64 -msgid "Loading auth-plugins by class-name is deprecated." -msgstr "" - -#: keystone/auth/controllers.py:106 +#: keystone/auth/controllers.py:109 #, python-format msgid "" "\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use" @@ -43,124 +39,74 @@ msgstr "" msgid "Running command - %s" msgstr "" -#: keystone/common/wsgi.py:79 +#: keystone/common/wsgi.py:82 msgid "No bind information present in token" msgstr "" -#: keystone/common/wsgi.py:83 +#: keystone/common/wsgi.py:86 #, python-format msgid "Named bind mode %s not in bind information" msgstr "" -#: keystone/common/wsgi.py:90 +#: keystone/common/wsgi.py:93 msgid "Kerberos credentials required and not present" msgstr "" -#: keystone/common/wsgi.py:94 +#: keystone/common/wsgi.py:97 msgid "Kerberos credentials do not match those in bind" msgstr "" -#: keystone/common/wsgi.py:98 +#: keystone/common/wsgi.py:101 msgid "Kerberos bind authentication successful" msgstr "" -#: keystone/common/wsgi.py:105 +#: keystone/common/wsgi.py:108 #, python-format msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" msgstr "" -#: keystone/common/environment/eventlet_server.py:103 +#: keystone/common/environment/eventlet_server.py:116 #, python-format msgid "Starting %(arg0)s on %(host)s:%(port)s" msgstr "" -#: keystone/common/kvs/core.py:138 +#: keystone/common/kvs/core.py:137 #, python-format msgid "Adding proxy '%(proxy)s' to KVS %(name)s." msgstr "" -#: keystone/common/kvs/core.py:188 +#: keystone/common/kvs/core.py:187 #, python-format msgid "Using %(func)s as KVS region %(name)s key_mangler" msgstr "" -#: keystone/common/kvs/core.py:200 +#: keystone/common/kvs/core.py:199 #, python-format msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" msgstr "" -#: keystone/common/kvs/core.py:210 +#: keystone/common/kvs/core.py:209 #, python-format msgid "KVS region %s key_mangler disabled." msgstr "" -#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#: keystone/contrib/example/core.py:69 keystone/contrib/example/core.py:78 #, python-format msgid "" "Received the following notification: service %(service)s, resource_type: " "%(resource_type)s, operation %(operation)s payload %(payload)s" msgstr "" -#: keystone/openstack/common/eventlet_backdoor.py:146 -#, python-format -msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" -msgstr "" - -#: keystone/openstack/common/service.py:173 -#, python-format -msgid "Caught %s, exiting" -msgstr "" - -#: keystone/openstack/common/service.py:231 -msgid "Parent process has died unexpectedly, exiting" -msgstr "" - -#: keystone/openstack/common/service.py:262 -#, python-format -msgid "Child caught %s, exiting" -msgstr "" - -#: keystone/openstack/common/service.py:301 -msgid "Forking too fast, sleeping" -msgstr "" - -#: keystone/openstack/common/service.py:320 -#, python-format -msgid "Started child %d" -msgstr "" - -#: keystone/openstack/common/service.py:330 +#: keystone/token/persistence/backends/sql.py:283 #, python-format -msgid "Starting %d workers" -msgstr "" - -#: keystone/openstack/common/service.py:347 -#, python-format -msgid "Child %(pid)d killed by signal %(sig)d" -msgstr "" - -#: keystone/openstack/common/service.py:351 -#, python-format -msgid "Child %(pid)s exited with status %(code)d" -msgstr "" - -#: keystone/openstack/common/service.py:390 -#, python-format -msgid "Caught %s, stopping children" -msgstr "" - -#: keystone/openstack/common/service.py:399 -msgid "Wait called after thread killed. Cleaning up." -msgstr "" - -#: keystone/openstack/common/service.py:415 -#, python-format -msgid "Waiting on %d children to exit" +msgid "Total expired tokens removed: %d" msgstr "" -#: keystone/token/persistence/backends/sql.py:279 +#: keystone/token/providers/fernet/token_formatters.py:163 #, python-format -msgid "Total expired tokens removed: %d" +msgid "" +"Fernet token created with length of %d characters, which exceeds 255 " +"characters" msgstr "" #: keystone/token/providers/fernet/utils.py:72 @@ -178,33 +124,33 @@ msgstr "" msgid "Key repository is already initialized; aborting." msgstr "" -#: keystone/token/providers/fernet/utils.py:179 +#: keystone/token/providers/fernet/utils.py:184 #, python-format msgid "Starting key rotation with %(count)s key files: %(list)s" msgstr "" -#: keystone/token/providers/fernet/utils.py:185 +#: keystone/token/providers/fernet/utils.py:190 #, python-format msgid "Current primary key is: %s" msgstr "" -#: keystone/token/providers/fernet/utils.py:187 +#: keystone/token/providers/fernet/utils.py:192 #, python-format msgid "Next primary key will be: %s" msgstr "" -#: keystone/token/providers/fernet/utils.py:197 +#: keystone/token/providers/fernet/utils.py:202 #, python-format msgid "Promoted key 0 to be the primary: %s" msgstr "" -#: keystone/token/providers/fernet/utils.py:213 +#: keystone/token/providers/fernet/utils.py:223 #, python-format -msgid "Excess keys to purge: %s" +msgid "Excess key to purge: %s" msgstr "" -#: keystone/token/providers/fernet/utils.py:237 +#: keystone/token/providers/fernet/utils.py:257 #, python-format -msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgid "Loaded %(count)d encryption keys (max_active_keys=%(max)d) from: %(dir)s" msgstr "" diff --git a/keystone-moon/keystone/locale/keystone-log-warning.pot b/keystone-moon/keystone/locale/keystone-log-warning.pot index ddf2931c..1109bcbe 100644 --- a/keystone-moon/keystone/locale/keystone-log-warning.pot +++ b/keystone-moon/keystone/locale/keystone-log-warning.pot @@ -6,103 +6,91 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: keystone 2015.1.dev497\n" +"Project-Id-Version: keystone 8.0.0.0b3.dev122\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-19 06:04+0000\n" +"POT-Creation-Date: 2015-08-16 06:06+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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" -#: keystone/cli.py:159 -msgid "keystone-manage pki_setup is not recommended for production use." -msgstr "" - -#: keystone/cli.py:178 -msgid "keystone-manage ssl_setup is not recommended for production use." -msgstr "" - -#: keystone/cli.py:493 -#, python-format -msgid "Ignoring file (%s) while scanning domain config directory" -msgstr "" - -#: keystone/exception.py:49 +#: keystone/exception.py:48 msgid "missing exception kwargs (programmer error)" msgstr "" -#: keystone/assignment/controllers.py:60 -#, python-format -msgid "Authentication failed: %s" -msgstr "" - -#: keystone/assignment/controllers.py:576 -#, python-format -msgid "" -"Group %(group)s not found for role-assignment - %(target)s with Role: " -"%(role)s" -msgstr "" - -#: keystone/auth/controllers.py:449 +#: keystone/auth/controllers.py:446 #, python-format msgid "" "User %(user_id)s doesn't have access to default project %(project_id)s. " "The token will be unscoped rather than scoped to the project." msgstr "" -#: keystone/auth/controllers.py:457 +#: keystone/auth/controllers.py:454 #, python-format msgid "" "User %(user_id)s's default project %(project_id)s is disabled. The token " "will be unscoped rather than scoped to the project." msgstr "" -#: keystone/auth/controllers.py:466 +#: keystone/auth/controllers.py:463 #, python-format msgid "" "User %(user_id)s's default project %(project_id)s not found. The token " "will be unscoped rather than scoped to the project." msgstr "" +#: keystone/cmd/cli.py:158 +msgid "keystone-manage pki_setup is not recommended for production use." +msgstr "" + +#: keystone/cmd/cli.py:177 +msgid "keystone-manage ssl_setup is not recommended for production use." +msgstr "" + +#: keystone/cmd/cli.py:483 +#, python-format +msgid "Ignoring file (%s) while scanning domain config directory" +msgstr "" + #: keystone/common/authorization.py:55 msgid "RBAC: Invalid user data in token" msgstr "" -#: keystone/common/controller.py:79 keystone/middleware/core.py:224 +#: keystone/common/controller.py:83 keystone/middleware/core.py:194 msgid "RBAC: Invalid token" msgstr "" -#: keystone/common/controller.py:104 keystone/common/controller.py:201 -#: keystone/common/controller.py:740 +#: keystone/common/controller.py:108 keystone/common/controller.py:205 +#: keystone/common/controller.py:755 msgid "RBAC: Bypassing authorization" msgstr "" -#: keystone/common/controller.py:669 keystone/common/controller.py:704 -msgid "Invalid token found while getting domain ID for list request" +#: keystone/common/controller.py:710 +msgid "No domain information specified as part of list request" msgstr "" -#: keystone/common/controller.py:677 -msgid "No domain information specified as part of list request" +#: keystone/common/openssl.py:73 +msgid "Failed to invoke ``openssl version``, assuming is v1.0 or newer" msgstr "" -#: keystone/common/utils.py:103 +#: keystone/common/utils.py:105 #, python-format msgid "Truncating user password to %d characters." msgstr "" -#: keystone/common/wsgi.py:242 -#, python-format -msgid "Authorization failed. %(exception)s from %(remote_addr)s" +#: keystone/common/utils.py:527 +msgid "Couldn't find the auth context." msgstr "" -#: keystone/common/wsgi.py:361 -msgid "Invalid token in _get_trust_id_for_request" +#: keystone/common/wsgi.py:243 +#, python-format +msgid "Authorization failed. %(exception)s from %(remote_addr)s" msgstr "" -#: keystone/common/cache/backends/mongo.py:403 +#: keystone/common/cache/backends/mongo.py:407 #, python-format msgid "" "TTL index already exists on db collection <%(c_name)s>, remove index " @@ -110,79 +98,74 @@ msgid "" "effective" msgstr "" -#: keystone/common/kvs/core.py:134 +#: keystone/common/kvs/core.py:133 #, python-format msgid "%s is not a dogpile.proxy.ProxyBackend" msgstr "" -#: keystone/common/kvs/core.py:403 +#: keystone/common/kvs/core.py:402 #, python-format msgid "KVS lock released (timeout reached) for: %s" msgstr "" -#: keystone/common/ldap/core.py:1026 +#: keystone/common/ldap/core.py:1029 msgid "" "LDAP Server does not support paging. Disable paging in keystone.conf to " "avoid this message." msgstr "" -#: keystone/common/ldap/core.py:1225 +#: keystone/common/ldap/core.py:1224 #, python-format msgid "" "Invalid additional attribute mapping: \"%s\". Format must be " "<ldap_attribute>:<keystone_attribute>" msgstr "" -#: keystone/common/ldap/core.py:1336 +#: keystone/common/ldap/core.py:1335 #, python-format msgid "" "ID attribute %(id_attr)s for LDAP object %(dn)s has multiple values and " "therefore cannot be used as an ID. Will get the ID from DN instead" msgstr "" -#: keystone/common/ldap/core.py:1669 +#: keystone/common/ldap/core.py:1668 #, python-format msgid "" "When deleting entries for %(search_base)s, could not delete nonexistent " "entries %(entries)s%(dots)s" msgstr "" -#: keystone/contrib/endpoint_policy/core.py:91 +#: keystone/contrib/federation/utils.py:545 +msgid "Ignoring user name" +msgstr "" + +#: keystone/endpoint_policy/core.py:94 #, python-format msgid "" "Endpoint %(endpoint_id)s referenced in association for policy " "%(policy_id)s not found." msgstr "" -#: keystone/contrib/endpoint_policy/core.py:179 +#: keystone/endpoint_policy/core.py:182 #, python-format msgid "" "Unsupported policy association found - Policy %(policy_id)s, Endpoint " "%(endpoint_id)s, Service %(service_id)s, Region %(region_id)s, " msgstr "" -#: keystone/contrib/endpoint_policy/core.py:195 +#: keystone/endpoint_policy/core.py:198 #, python-format msgid "" "Policy %(policy_id)s referenced in association for endpoint " "%(endpoint_id)s not found." msgstr "" -#: keystone/contrib/federation/utils.py:200 -#, python-format -msgid "Impossible to identify the IdP %s " -msgstr "" - -#: keystone/contrib/federation/utils.py:523 -msgid "Ignoring user name" -msgstr "" - -#: keystone/identity/controllers.py:139 +#: keystone/identity/controllers.py:141 #, python-format msgid "Unable to remove user %(user)s from %(tenant)s." msgstr "" -#: keystone/identity/controllers.py:158 +#: keystone/identity/controllers.py:160 #, python-format msgid "Unable to add user %(user)s to %(tenant)s." msgstr "" @@ -197,29 +180,18 @@ msgstr "" msgid "Unable to locate domain config directory: %s" msgstr "" -#: keystone/middleware/core.py:149 +#: keystone/identity/core.py:602 +#, python-format msgid "" -"XML support has been removed as of the Kilo release and should not be " -"referenced or used in deployment. Please remove references to " -"XmlBodyMiddleware from your configuration. This compatibility stub will " -"be removed in the L release" +"Found multiple domains being mapped to a driver that does not support " +"that (e.g. LDAP) - Domain ID: %(domain)s, Default Driver: %(driver)s" msgstr "" -#: keystone/middleware/core.py:234 +#: keystone/middleware/core.py:204 msgid "Auth context already exists in the request environment" msgstr "" -#: keystone/openstack/common/loopingcall.py:87 -#, python-format -msgid "task %(func_name)r run outlasted interval by %(delay).2f sec" -msgstr "" - -#: keystone/openstack/common/service.py:351 -#, python-format -msgid "pid %d not in child list" -msgstr "" - -#: keystone/resource/core.py:1214 +#: keystone/resource/core.py:1237 #, python-format msgid "" "Found what looks like an unmatched config option substitution reference -" @@ -228,7 +200,7 @@ msgid "" "added?" msgstr "" -#: keystone/resource/core.py:1221 +#: keystone/resource/core.py:1244 #, python-format msgid "" "Found what looks like an incorrectly constructed config option " @@ -236,27 +208,26 @@ msgid "" "%(option)s, value: %(value)s." msgstr "" -#: keystone/token/persistence/core.py:228 +#: keystone/token/persistence/core.py:225 #, python-format msgid "" "`token_api.%s` is deprecated as of Juno in favor of utilizing methods on " "`token_provider_api` and may be removed in Kilo." msgstr "" -#: keystone/token/persistence/backends/kvs.py:57 +#: keystone/token/persistence/backends/kvs.py:58 msgid "" "It is recommended to only use the base key-value-store implementation for" -" the token driver for testing purposes. Please use " -"keystone.token.persistence.backends.memcache.Token or " -"keystone.token.persistence.backends.sql.Token instead." +" the token driver for testing purposes. Please use 'memcache' or 'sql' " +"instead." msgstr "" -#: keystone/token/persistence/backends/kvs.py:206 +#: keystone/token/persistence/backends/kvs.py:205 #, python-format msgid "Token `%s` is expired, not adding to the revocation list." msgstr "" -#: keystone/token/persistence/backends/kvs.py:240 +#: keystone/token/persistence/backends/kvs.py:239 #, python-format msgid "" "Removing `%s` from revocation list due to invalid expires data in " @@ -282,7 +253,7 @@ msgid "" "and keystone group ID both being provided: %s" msgstr "" -#: keystone/token/providers/fernet/utils.py:204 +#: keystone/token/providers/fernet/utils.py:210 msgid "" "[fernet_tokens] max_active_keys must be at least 1 to maintain a primary " "key." diff --git a/keystone-moon/keystone/locale/keystone.pot b/keystone-moon/keystone/locale/keystone.pot index df46fa72..315891aa 100644 --- a/keystone-moon/keystone/locale/keystone.pot +++ b/keystone-moon/keystone/locale/keystone.pot @@ -6,97 +6,18 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: keystone 2015.1.dev497\n" +"Project-Id-Version: keystone 8.0.0.0b3.dev122\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-19 06:03+0000\n" +"POT-Creation-Date: 2015-08-16 06:06+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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" -#: keystone/clean.py:24 -#, python-format -msgid "%s cannot be empty." -msgstr "" - -#: keystone/clean.py:26 -#, python-format -msgid "%(property_name)s cannot be less than %(min_length)s characters." -msgstr "" - -#: keystone/clean.py:31 -#, python-format -msgid "%(property_name)s should not be greater than %(max_length)s characters." -msgstr "" - -#: keystone/clean.py:40 -#, python-format -msgid "%(property_name)s is not a %(display_expected_type)s" -msgstr "" - -#: keystone/cli.py:283 -msgid "At least one option must be provided" -msgstr "" - -#: keystone/cli.py:290 -msgid "--all option cannot be mixed with other options" -msgstr "" - -#: keystone/cli.py:301 -#, python-format -msgid "Unknown domain '%(name)s' specified by --domain-name" -msgstr "" - -#: keystone/cli.py:365 keystone/tests/unit/test_cli.py:213 -msgid "At least one option must be provided, use either --all or --domain-name" -msgstr "" - -#: keystone/cli.py:371 keystone/tests/unit/test_cli.py:229 -msgid "The --all option cannot be used with the --domain-name option" -msgstr "" - -#: keystone/cli.py:397 keystone/tests/unit/test_cli.py:246 -#, python-format -msgid "" -"Invalid domain name: %(domain)s found in config file name: %(file)s - " -"ignoring this file." -msgstr "" - -#: keystone/cli.py:405 keystone/tests/unit/test_cli.py:187 -#, python-format -msgid "" -"Domain: %(domain)s already has a configuration defined - ignoring file: " -"%(file)s." -msgstr "" - -#: keystone/cli.py:419 -#, python-format -msgid "Error parsing configuration file for domain: %(domain)s, file: %(file)s." -msgstr "" - -#: keystone/cli.py:452 -#, python-format -msgid "" -"To get a more detailed information on this error, re-run this command for" -" the specific domain, i.e.: keystone-manage domain_config_upload " -"--domain-name %s" -msgstr "" - -#: keystone/cli.py:470 -#, python-format -msgid "Unable to locate domain config directory: %s" -msgstr "" - -#: keystone/cli.py:503 -msgid "" -"Unable to access the keystone database, please check it is configured " -"correctly." -msgstr "" - -#: keystone/exception.py:79 +#: keystone/exception.py:78 #, python-format msgid "" "Expecting to find %(attribute)s in %(target)s - the server could not " @@ -104,26 +25,38 @@ msgid "" "incorrect. The client is assumed to be in error." msgstr "" -#: keystone/exception.py:90 +#: keystone/exception.py:87 #, python-format -msgid "%(detail)s" +msgid "Cannot create an endpoint with an invalid URL: %(url)s" msgstr "" #: keystone/exception.py:94 +#, python-format +msgid "%(detail)s" +msgstr "" + +#: keystone/exception.py:98 msgid "" "Timestamp not in expected format. The server could not comply with the " "request since it is either malformed or otherwise incorrect. The client " "is assumed to be in error." msgstr "" -#: keystone/exception.py:103 +#: keystone/exception.py:107 +msgid "" +"The 'expires_at' must not be before now. The server could not comply with" +" the request since it is either malformed or otherwise incorrect. The " +"client is assumed to be in error." +msgstr "" + +#: keystone/exception.py:116 #, python-format msgid "" "String length exceeded.The length of string '%(string)s' exceeded the " "limit of column %(type)s(CHAR(%(length)d))." msgstr "" -#: keystone/exception.py:109 +#: keystone/exception.py:122 #, python-format msgid "" "Request attribute %(attribute)s must be less than or equal to %(size)i. " @@ -131,88 +64,88 @@ msgid "" "is invalid (too large). The client is assumed to be in error." msgstr "" -#: keystone/exception.py:119 +#: keystone/exception.py:132 #, python-format msgid "" "The specified parent region %(parent_region_id)s would create a circular " "region hierarchy." msgstr "" -#: keystone/exception.py:126 +#: keystone/exception.py:139 #, python-format msgid "" "The password length must be less than or equal to %(size)i. The server " "could not comply with the request because the password is invalid." msgstr "" -#: keystone/exception.py:134 +#: keystone/exception.py:147 #, python-format msgid "" "Unable to delete region %(region_id)s because it or its child regions " "have associated endpoints." msgstr "" -#: keystone/exception.py:141 +#: keystone/exception.py:154 msgid "" "The certificates you requested are not available. It is likely that this " "server does not use PKI tokens otherwise this is the result of " "misconfiguration." msgstr "" -#: keystone/exception.py:150 +#: keystone/exception.py:163 msgid "(Disable debug mode to suppress these details.)" msgstr "" -#: keystone/exception.py:155 +#: keystone/exception.py:168 #, python-format msgid "%(message)s %(amendment)s" msgstr "" -#: keystone/exception.py:163 +#: keystone/exception.py:176 msgid "The request you have made requires authentication." msgstr "" -#: keystone/exception.py:169 +#: keystone/exception.py:182 msgid "Authentication plugin error." msgstr "" -#: keystone/exception.py:177 +#: keystone/exception.py:190 #, python-format msgid "Unable to find valid groups while using mapping %(mapping_id)s" msgstr "" -#: keystone/exception.py:182 +#: keystone/exception.py:195 msgid "Attempted to authenticate with an unsupported method." msgstr "" -#: keystone/exception.py:190 +#: keystone/exception.py:203 msgid "Additional authentications steps required." msgstr "" -#: keystone/exception.py:198 +#: keystone/exception.py:211 msgid "You are not authorized to perform the requested action." msgstr "" -#: keystone/exception.py:205 +#: keystone/exception.py:218 #, python-format msgid "You are not authorized to perform the requested action: %(action)s" msgstr "" -#: keystone/exception.py:210 +#: keystone/exception.py:223 #, python-format msgid "" "Could not change immutable attribute(s) '%(attributes)s' in target " "%(target)s" msgstr "" -#: keystone/exception.py:215 +#: keystone/exception.py:228 #, python-format msgid "" "Group membership across backend boundaries is not allowed, group in " "question is %(group_id)s, user is %(user_id)s" msgstr "" -#: keystone/exception.py:221 +#: keystone/exception.py:234 #, python-format msgid "" "Invalid mix of entities for policy association - only Endpoint, Service " @@ -220,225 +153,229 @@ msgid "" "Service: %(service_id)s, Region: %(region_id)s" msgstr "" -#: keystone/exception.py:228 +#: keystone/exception.py:241 #, python-format msgid "Invalid domain specific configuration: %(reason)s" msgstr "" -#: keystone/exception.py:232 +#: keystone/exception.py:245 #, python-format msgid "Could not find: %(target)s" msgstr "" -#: keystone/exception.py:238 +#: keystone/exception.py:251 #, python-format msgid "Could not find endpoint: %(endpoint_id)s" msgstr "" -#: keystone/exception.py:245 +#: keystone/exception.py:258 msgid "An unhandled exception has occurred: Could not find metadata." msgstr "" -#: keystone/exception.py:250 +#: keystone/exception.py:263 #, python-format msgid "Could not find policy: %(policy_id)s" msgstr "" -#: keystone/exception.py:254 +#: keystone/exception.py:267 msgid "Could not find policy association" msgstr "" -#: keystone/exception.py:258 +#: keystone/exception.py:271 #, python-format msgid "Could not find role: %(role_id)s" msgstr "" -#: keystone/exception.py:262 +#: keystone/exception.py:275 #, python-format msgid "" "Could not find role assignment with role: %(role_id)s, user or group: " "%(actor_id)s, project or domain: %(target_id)s" msgstr "" -#: keystone/exception.py:268 +#: keystone/exception.py:281 #, python-format msgid "Could not find region: %(region_id)s" msgstr "" -#: keystone/exception.py:272 +#: keystone/exception.py:285 #, python-format msgid "Could not find service: %(service_id)s" msgstr "" -#: keystone/exception.py:276 +#: keystone/exception.py:289 #, python-format msgid "Could not find domain: %(domain_id)s" msgstr "" -#: keystone/exception.py:280 +#: keystone/exception.py:293 #, python-format msgid "Could not find project: %(project_id)s" msgstr "" -#: keystone/exception.py:284 +#: keystone/exception.py:297 #, python-format msgid "Cannot create project with parent: %(project_id)s" msgstr "" -#: keystone/exception.py:288 +#: keystone/exception.py:301 #, python-format msgid "Could not find token: %(token_id)s" msgstr "" -#: keystone/exception.py:292 +#: keystone/exception.py:305 #, python-format msgid "Could not find user: %(user_id)s" msgstr "" -#: keystone/exception.py:296 +#: keystone/exception.py:309 #, python-format msgid "Could not find group: %(group_id)s" msgstr "" -#: keystone/exception.py:300 +#: keystone/exception.py:313 #, python-format msgid "Could not find mapping: %(mapping_id)s" msgstr "" -#: keystone/exception.py:304 +#: keystone/exception.py:317 #, python-format msgid "Could not find trust: %(trust_id)s" msgstr "" -#: keystone/exception.py:308 +#: keystone/exception.py:321 #, python-format msgid "No remaining uses for trust: %(trust_id)s" msgstr "" -#: keystone/exception.py:312 +#: keystone/exception.py:325 #, python-format msgid "Could not find credential: %(credential_id)s" msgstr "" -#: keystone/exception.py:316 +#: keystone/exception.py:329 #, python-format msgid "Could not find version: %(version)s" msgstr "" -#: keystone/exception.py:320 +#: keystone/exception.py:333 #, python-format msgid "Could not find Endpoint Group: %(endpoint_group_id)s" msgstr "" -#: keystone/exception.py:324 +#: keystone/exception.py:337 #, python-format msgid "Could not find Identity Provider: %(idp_id)s" msgstr "" -#: keystone/exception.py:328 +#: keystone/exception.py:341 #, python-format msgid "Could not find Service Provider: %(sp_id)s" msgstr "" -#: keystone/exception.py:332 +#: keystone/exception.py:345 #, python-format msgid "" "Could not find federated protocol %(protocol_id)s for Identity Provider: " "%(idp_id)s" msgstr "" -#: keystone/exception.py:343 +#: keystone/exception.py:356 #, python-format msgid "" "Could not find %(group_or_option)s in domain configuration for domain " "%(domain_id)s" msgstr "" -#: keystone/exception.py:348 +#: keystone/exception.py:361 #, python-format msgid "Conflict occurred attempting to store %(type)s - %(details)s" msgstr "" -#: keystone/exception.py:356 +#: keystone/exception.py:369 msgid "An unexpected error prevented the server from fulfilling your request." msgstr "" -#: keystone/exception.py:359 +#: keystone/exception.py:372 #, python-format msgid "" "An unexpected error prevented the server from fulfilling your request: " "%(exception)s" msgstr "" -#: keystone/exception.py:382 +#: keystone/exception.py:395 #, python-format msgid "Unable to consume trust %(trust_id)s, unable to acquire lock." msgstr "" -#: keystone/exception.py:387 +#: keystone/exception.py:400 msgid "" "Expected signing certificates are not available on the server. Please " "check Keystone configuration." msgstr "" -#: keystone/exception.py:393 +#: keystone/exception.py:406 #, python-format msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." msgstr "" -#: keystone/exception.py:398 +#: keystone/exception.py:411 #, python-format msgid "" "Group %(group_id)s returned by mapping %(mapping_id)s was not found in " "the backend." msgstr "" -#: keystone/exception.py:403 +#: keystone/exception.py:416 #, python-format msgid "Error while reading metadata file, %(reason)s" msgstr "" -#: keystone/exception.py:407 +#: keystone/exception.py:420 #, python-format msgid "" "Unexpected combination of grant attributes - User: %(user_id)s, Group: " "%(group_id)s, Project: %(project_id)s, Domain: %(domain_id)s" msgstr "" -#: keystone/exception.py:414 +#: keystone/exception.py:427 msgid "The action you have requested has not been implemented." msgstr "" -#: keystone/exception.py:421 +#: keystone/exception.py:434 msgid "The service you have requested is no longer available on this server." msgstr "" -#: keystone/exception.py:428 +#: keystone/exception.py:441 #, python-format msgid "The Keystone configuration file %(config_file)s could not be found." msgstr "" -#: keystone/exception.py:433 +#: keystone/exception.py:446 msgid "" "No encryption keys found; run keystone-manage fernet_setup to bootstrap " "one." msgstr "" -#: keystone/exception.py:438 +#: keystone/exception.py:451 #, python-format msgid "" "The Keystone domain-specific configuration has specified more than one " "SQL driver (only one is permitted): %(source)s." msgstr "" -#: keystone/exception.py:445 +#: keystone/exception.py:458 #, python-format msgid "" "%(mod_name)s doesn't provide database migrations. The migration " "repository path at %(path)s doesn't exist or isn't a directory." msgstr "" -#: keystone/exception.py:457 +#: keystone/exception.py:465 +msgid "Token version is unrecognizable or unsupported." +msgstr "" + +#: keystone/exception.py:470 #, python-format msgid "" "Unable to sign SAML assertion. It is likely that this server does not " @@ -446,107 +383,112 @@ msgid "" " %(reason)s" msgstr "" -#: keystone/exception.py:465 +#: keystone/exception.py:478 msgid "" "No Authorization headers found, cannot proceed with OAuth related calls, " "if running under HTTPd or Apache, ensure WSGIPassAuthorization is set to " "On." msgstr "" -#: keystone/notifications.py:250 +#: keystone/notifications.py:273 #, python-format msgid "%(event)s is not a valid notification event, must be one of: %(actions)s" msgstr "" -#: keystone/notifications.py:259 +#: keystone/notifications.py:282 #, python-format msgid "Method not callable: %s" msgstr "" -#: keystone/assignment/controllers.py:107 keystone/identity/controllers.py:69 +#: keystone/assignment/controllers.py:99 keystone/identity/controllers.py:71 #: keystone/resource/controllers.py:78 msgid "Name field is required and cannot be empty" msgstr "" -#: keystone/assignment/controllers.py:330 -#: keystone/assignment/controllers.py:753 +#: keystone/assignment/controllers.py:155 +#: keystone/assignment/controllers.py:174 +msgid "User roles not supported: tenant_id required" +msgstr "" + +#: keystone/assignment/controllers.py:338 +#: keystone/assignment/controllers.py:579 msgid "Specify a domain or project, not both" msgstr "" -#: keystone/assignment/controllers.py:333 +#: keystone/assignment/controllers.py:341 msgid "Specify one of domain or project" msgstr "" -#: keystone/assignment/controllers.py:338 -#: keystone/assignment/controllers.py:758 +#: keystone/assignment/controllers.py:346 +#: keystone/assignment/controllers.py:584 msgid "Specify a user or group, not both" msgstr "" -#: keystone/assignment/controllers.py:341 +#: keystone/assignment/controllers.py:349 msgid "Specify one of user or group" msgstr "" -#: keystone/assignment/controllers.py:742 +#: keystone/assignment/controllers.py:568 msgid "Combining effective and group filter will always result in an empty list." msgstr "" -#: keystone/assignment/controllers.py:747 +#: keystone/assignment/controllers.py:573 msgid "" "Combining effective, domain and inherited filters will always result in " "an empty list." msgstr "" -#: keystone/assignment/core.py:228 +#: keystone/assignment/core.py:233 msgid "Must specify either domain or project" msgstr "" -#: keystone/assignment/core.py:493 +#: keystone/assignment/core.py:903 #, python-format msgid "Project (%s)" msgstr "" -#: keystone/assignment/core.py:495 +#: keystone/assignment/core.py:905 #, python-format msgid "Domain (%s)" msgstr "" -#: keystone/assignment/core.py:497 +#: keystone/assignment/core.py:907 msgid "Unknown Target" msgstr "" -#: keystone/assignment/backends/ldap.py:92 +#: keystone/assignment/backends/ldap.py:91 msgid "Domain metadata not supported by LDAP" msgstr "" -#: keystone/assignment/backends/ldap.py:381 +#: keystone/assignment/backends/ldap.py:397 #, python-format msgid "User %(user_id)s already has role %(role_id)s in tenant %(tenant_id)s" msgstr "" -#: keystone/assignment/backends/ldap.py:387 +#: keystone/assignment/backends/ldap.py:403 #, python-format msgid "Role %s not found" msgstr "" -#: keystone/assignment/backends/ldap.py:402 -#: keystone/assignment/backends/sql.py:335 +#: keystone/assignment/backends/ldap.py:418 +#: keystone/assignment/backends/sql.py:334 #, python-format msgid "Cannot remove role that has not been granted, %s" msgstr "" -#: keystone/assignment/backends/sql.py:356 +#: keystone/assignment/backends/sql.py:410 #, python-format msgid "Unexpected assignment type encountered, %s" msgstr "" -#: keystone/assignment/role_backends/ldap.py:61 keystone/catalog/core.py:103 -#: keystone/common/ldap/core.py:1401 keystone/resource/backends/ldap.py:149 +#: keystone/assignment/role_backends/ldap.py:61 keystone/catalog/core.py:135 +#: keystone/common/ldap/core.py:1400 keystone/resource/backends/ldap.py:148 #, python-format msgid "Duplicate ID, %s." msgstr "" #: keystone/assignment/role_backends/ldap.py:69 -#: keystone/common/ldap/core.py:1391 +#: keystone/common/ldap/core.py:1390 #, python-format msgid "Duplicate name, %s." msgstr "" @@ -556,222 +498,249 @@ msgstr "" msgid "Cannot duplicate name %s" msgstr "" -#: keystone/auth/controllers.py:60 -#, python-format -msgid "" -"Cannot load an auth-plugin by class-name without a \"method\" attribute " -"defined: %s" -msgstr "" - -#: keystone/auth/controllers.py:71 -#, python-format -msgid "" -"Auth plugin %(plugin)s is requesting previously registered method " -"%(method)s" -msgstr "" - -#: keystone/auth/controllers.py:115 +#: keystone/auth/controllers.py:118 #, python-format msgid "" "Unable to reconcile identity attribute %(attribute)s as it has " "conflicting values %(new)s and %(old)s" msgstr "" -#: keystone/auth/controllers.py:336 +#: keystone/auth/controllers.py:333 msgid "Scoping to both domain and project is not allowed" msgstr "" -#: keystone/auth/controllers.py:339 +#: keystone/auth/controllers.py:336 msgid "Scoping to both domain and trust is not allowed" msgstr "" -#: keystone/auth/controllers.py:342 +#: keystone/auth/controllers.py:339 msgid "Scoping to both project and trust is not allowed" msgstr "" -#: keystone/auth/controllers.py:512 +#: keystone/auth/controllers.py:509 msgid "User not found" msgstr "" -#: keystone/auth/controllers.py:616 +#: keystone/auth/controllers.py:613 msgid "A project-scoped token is required to produce a service catalog." msgstr "" -#: keystone/auth/plugins/external.py:46 +#: keystone/auth/plugins/external.py:42 msgid "No authenticated user" msgstr "" -#: keystone/auth/plugins/external.py:56 +#: keystone/auth/plugins/external.py:52 #, python-format msgid "Unable to lookup user %s" msgstr "" -#: keystone/auth/plugins/external.py:107 +#: keystone/auth/plugins/external.py:100 msgid "auth_type is not Negotiate" msgstr "" -#: keystone/auth/plugins/mapped.py:244 -msgid "Could not map user" -msgstr "" - -#: keystone/auth/plugins/oauth1.py:39 -#, python-format -msgid "%s not supported" +#: keystone/auth/plugins/mapped.py:239 +msgid "" +"Could not map user while setting ephemeral user identity. Either mapping " +"rules must specify user id/name or REMOTE_USER environment variable must " +"be set." msgstr "" -#: keystone/auth/plugins/oauth1.py:57 +#: keystone/auth/plugins/oauth1.py:51 msgid "Access token is expired" msgstr "" -#: keystone/auth/plugins/oauth1.py:71 +#: keystone/auth/plugins/oauth1.py:65 msgid "Could not validate the access token" msgstr "" -#: keystone/auth/plugins/password.py:46 +#: keystone/auth/plugins/password.py:45 msgid "Invalid username or password" msgstr "" -#: keystone/auth/plugins/token.py:72 keystone/token/controllers.py:160 +#: keystone/auth/plugins/token.py:70 keystone/token/controllers.py:162 msgid "rescope a scoped token" msgstr "" -#: keystone/catalog/controllers.py:168 +#: keystone/catalog/controllers.py:175 #, python-format msgid "Conflicting region IDs specified: \"%(url_id)s\" != \"%(ref_id)s\"" msgstr "" -#: keystone/common/authorization.py:47 keystone/common/wsgi.py:64 +#: keystone/cmd/cli.py:286 +msgid "At least one option must be provided" +msgstr "" + +#: keystone/cmd/cli.py:293 +msgid "--all option cannot be mixed with other options" +msgstr "" + +#: keystone/cmd/cli.py:300 #, python-format -msgid "token reference must be a KeystoneToken type, got: %s" +msgid "Unknown domain '%(name)s' specified by --domain-name" msgstr "" -#: keystone/common/base64utils.py:66 -msgid "pad must be single character" +#: keystone/cmd/cli.py:355 keystone/tests/unit/test_cli.py:215 +msgid "At least one option must be provided, use either --all or --domain-name" msgstr "" -#: keystone/common/base64utils.py:215 +#: keystone/cmd/cli.py:361 keystone/tests/unit/test_cli.py:231 +msgid "The --all option cannot be used with the --domain-name option" +msgstr "" + +#: keystone/cmd/cli.py:387 keystone/tests/unit/test_cli.py:248 #, python-format -msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" +msgid "" +"Invalid domain name: %(domain)s found in config file name: %(file)s - " +"ignoring this file." msgstr "" -#: keystone/common/base64utils.py:219 +#: keystone/cmd/cli.py:395 keystone/tests/unit/test_cli.py:189 #, python-format -msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" +msgid "" +"Domain: %(domain)s already has a configuration defined - ignoring file: " +"%(file)s." msgstr "" -#: keystone/common/base64utils.py:225 +#: keystone/cmd/cli.py:409 #, python-format -msgid "text is not a multiple of 4, but contains pad \"%s\"" +msgid "Error parsing configuration file for domain: %(domain)s, file: %(file)s." msgstr "" -#: keystone/common/base64utils.py:244 keystone/common/base64utils.py:265 -msgid "padded base64url text must be multiple of 4 characters" +#: keystone/cmd/cli.py:442 +#, python-format +msgid "" +"To get a more detailed information on this error, re-run this command for" +" the specific domain, i.e.: keystone-manage domain_config_upload " +"--domain-name %s" msgstr "" -#: keystone/common/controller.py:237 keystone/token/providers/common.py:589 -msgid "Non-default domain is not supported" +#: keystone/cmd/cli.py:460 +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "" + +#: keystone/cmd/cli.py:493 +msgid "" +"Unable to access the keystone database, please check it is configured " +"correctly." msgstr "" -#: keystone/common/controller.py:305 keystone/identity/core.py:428 -#: keystone/resource/core.py:761 keystone/resource/backends/ldap.py:61 +#: keystone/cmd/cli.py:559 #, python-format -msgid "Expected dict or list: %s" +msgid "Error while parsing rules %(path)s: %(err)s" msgstr "" -#: keystone/common/controller.py:318 -msgid "Marker could not be found" +#: keystone/cmd/cli.py:568 +#, python-format +msgid "Error while opening file %(path)s: %(err)s" msgstr "" -#: keystone/common/controller.py:329 -msgid "Invalid limit value" +#: keystone/cmd/cli.py:578 +#, python-format +msgid "Error while parsing line: '%(line)s': %(err)s" msgstr "" -#: keystone/common/controller.py:637 -msgid "Cannot change Domain ID" +#: keystone/common/authorization.py:47 keystone/common/wsgi.py:66 +#, python-format +msgid "token reference must be a KeystoneToken type, got: %s" +msgstr "" + +#: keystone/common/base64utils.py:71 +msgid "pad must be single character" msgstr "" -#: keystone/common/controller.py:666 -msgid "domain_id is required as part of entity" +#: keystone/common/base64utils.py:220 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" msgstr "" -#: keystone/common/controller.py:701 -msgid "A domain-scoped token must be used" +#: keystone/common/base64utils.py:224 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" msgstr "" -#: keystone/common/dependency.py:68 +#: keystone/common/base64utils.py:230 #, python-format -msgid "Unregistered dependency: %(name)s for %(targets)s" +msgid "text is not a multiple of 4, but contains pad \"%s\"" msgstr "" -#: keystone/common/dependency.py:108 -msgid "event_callbacks must be a dict" +#: keystone/common/base64utils.py:249 keystone/common/base64utils.py:270 +msgid "padded base64url text must be multiple of 4 characters" msgstr "" -#: keystone/common/dependency.py:113 +#: keystone/common/clean.py:24 #, python-format -msgid "event_callbacks[%s] must be a dict" +msgid "%s cannot be empty." msgstr "" -#: keystone/common/pemutils.py:223 +#: keystone/common/clean.py:26 #, python-format -msgid "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" +msgid "%(property_name)s cannot be less than %(min_length)s characters." msgstr "" -#: keystone/common/pemutils.py:242 +#: keystone/common/clean.py:31 #, python-format -msgid "" -"unknown pem header \"%(pem_header)s\", valid headers are: " -"%(valid_pem_headers)s" +msgid "%(property_name)s should not be greater than %(max_length)s characters." msgstr "" -#: keystone/common/pemutils.py:298 +#: keystone/common/clean.py:40 #, python-format -msgid "failed to find end matching \"%s\"" +msgid "%(property_name)s is not a %(display_expected_type)s" +msgstr "" + +#: keystone/common/controller.py:229 keystone/common/controller.py:245 +#: keystone/token/providers/common.py:636 +msgid "Non-default domain is not supported" msgstr "" -#: keystone/common/pemutils.py:302 +#: keystone/common/controller.py:322 keystone/common/controller.py:350 +#: keystone/identity/core.py:506 keystone/resource/core.py:774 +#: keystone/resource/backends/ldap.py:61 #, python-format -msgid "" -"beginning & end PEM headers do not match (%(begin_pem_header)s!= " -"%(end_pem_header)s)" +msgid "Expected dict or list: %s" +msgstr "" + +#: keystone/common/controller.py:363 +msgid "Marker could not be found" +msgstr "" + +#: keystone/common/controller.py:374 +msgid "Invalid limit value" msgstr "" -#: keystone/common/pemutils.py:377 +#: keystone/common/controller.py:682 +msgid "Cannot change Domain ID" +msgstr "" + +#: keystone/common/dependency.py:64 #, python-format -msgid "unknown pem_type: \"%s\"" +msgid "Unregistered dependency: %(name)s for %(targets)s" msgstr "" -#: keystone/common/pemutils.py:389 +#: keystone/common/json_home.py:76 #, python-format -msgid "" -"failed to base64 decode %(pem_type)s PEM at position%(position)d: " -"%(err_msg)s" +msgid "Unexpected status requested for JSON Home response, %s" msgstr "" -#: keystone/common/utils.py:164 keystone/credential/controllers.py:44 +#: keystone/common/utils.py:166 keystone/credential/controllers.py:44 msgid "Invalid blob in credential" msgstr "" -#: keystone/common/wsgi.py:330 +#: keystone/common/wsgi.py:335 #, python-format msgid "%s field is required and cannot be empty" msgstr "" -#: keystone/common/wsgi.py:342 +#: keystone/common/wsgi.py:347 #, python-format msgid "%s field(s) cannot be empty" msgstr "" -#: keystone/common/wsgi.py:563 +#: keystone/common/wsgi.py:558 msgid "The resource could not be found." msgstr "" -#: keystone/common/wsgi.py:704 -#, python-format -msgid "Unexpected status requested for JSON Home response, %s" -msgstr "" - -#: keystone/common/cache/_memcache_pool.py:113 +#: keystone/common/cache/_memcache_pool.py:124 #, python-format msgid "Unable to get a connection from pool id %(id)s after %(seconds)s seconds." msgstr "" @@ -815,31 +784,31 @@ msgid "" " \"REQUIRED\"" msgstr "" -#: keystone/common/kvs/core.py:71 +#: keystone/common/kvs/core.py:70 #, python-format msgid "Lock Timeout occurred for key, %(target)s" msgstr "" -#: keystone/common/kvs/core.py:106 +#: keystone/common/kvs/core.py:105 #, python-format msgid "KVS region %s is already configured. Cannot reconfigure." msgstr "" -#: keystone/common/kvs/core.py:145 +#: keystone/common/kvs/core.py:144 #, python-format msgid "Key Value Store not configured: %s" msgstr "" -#: keystone/common/kvs/core.py:198 +#: keystone/common/kvs/core.py:197 msgid "`key_mangler` option must be a function reference" msgstr "" -#: keystone/common/kvs/core.py:353 +#: keystone/common/kvs/core.py:352 #, python-format msgid "Lock key must match target key: %(lock)s != %(target)s" msgstr "" -#: keystone/common/kvs/core.py:357 +#: keystone/common/kvs/core.py:356 msgid "Must be called within an active lock context." msgstr "" @@ -848,28 +817,28 @@ msgstr "" msgid "Maximum lock attempts on %s occurred." msgstr "" -#: keystone/common/kvs/backends/memcached.py:108 +#: keystone/common/kvs/backends/memcached.py:109 #, python-format msgid "" -"Backend `%(driver)s` is not a valid memcached backend. Valid drivers: " -"%(driver_list)s" +"Backend `%(backend)s` is not a valid memcached backend. Valid backends: " +"%(backend_list)s" msgstr "" -#: keystone/common/kvs/backends/memcached.py:178 +#: keystone/common/kvs/backends/memcached.py:185 msgid "`key_mangler` functions must be callable." msgstr "" -#: keystone/common/ldap/core.py:191 +#: keystone/common/ldap/core.py:193 #, python-format msgid "Invalid LDAP deref option: %(option)s. Choose one of: %(options)s" msgstr "" -#: keystone/common/ldap/core.py:201 +#: keystone/common/ldap/core.py:203 #, python-format msgid "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" msgstr "" -#: keystone/common/ldap/core.py:213 +#: keystone/common/ldap/core.py:215 #, python-format msgid "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" msgstr "" @@ -893,38 +862,38 @@ msgstr "" msgid "tls_cacertdir %s not found or is not a directory" msgstr "" -#: keystone/common/ldap/core.py:1326 +#: keystone/common/ldap/core.py:1325 #, python-format msgid "ID attribute %(id_attr)s not found in LDAP object %(dn)s" msgstr "" -#: keystone/common/ldap/core.py:1370 +#: keystone/common/ldap/core.py:1369 #, python-format msgid "LDAP %s create" msgstr "" -#: keystone/common/ldap/core.py:1375 +#: keystone/common/ldap/core.py:1374 #, python-format msgid "LDAP %s update" msgstr "" -#: keystone/common/ldap/core.py:1380 +#: keystone/common/ldap/core.py:1379 #, python-format msgid "LDAP %s delete" msgstr "" -#: keystone/common/ldap/core.py:1522 +#: keystone/common/ldap/core.py:1521 msgid "" "Disabling an entity where the 'enable' attribute is ignored by " "configuration." msgstr "" -#: keystone/common/ldap/core.py:1533 +#: keystone/common/ldap/core.py:1532 #, python-format msgid "Cannot change %(option_name)s %(attr)s" msgstr "" -#: keystone/common/ldap/core.py:1620 +#: keystone/common/ldap/core.py:1619 #, python-format msgid "Member %(member)s is already a member of group %(group)s" msgstr "" @@ -935,31 +904,38 @@ msgid "" " self " msgstr "" -#: keystone/common/sql/core.py:410 +#: keystone/common/sql/core.py:445 msgid "Duplicate Entry" msgstr "" -#: keystone/common/sql/core.py:426 +#: keystone/common/sql/core.py:461 #, python-format msgid "An unexpected error occurred when trying to store %s" msgstr "" -#: keystone/common/sql/migration_helpers.py:187 -#: keystone/common/sql/migration_helpers.py:245 +#: keystone/common/sql/migration_helpers.py:171 +#: keystone/common/sql/migration_helpers.py:213 #, python-format msgid "%s extension does not exist." msgstr "" +#: keystone/common/validation/__init__.py:41 +#, python-format +msgid "" +"validated expected to find %(param_name)r in function signature for " +"%(func_name)r." +msgstr "" + #: keystone/common/validation/validators.py:54 #, python-format msgid "Invalid input for field '%(path)s'. The value is '%(value)s'." msgstr "" -#: keystone/contrib/ec2/controllers.py:318 +#: keystone/contrib/ec2/controllers.py:324 msgid "Token belongs to another user" msgstr "" -#: keystone/contrib/ec2/controllers.py:346 +#: keystone/contrib/ec2/controllers.py:352 msgid "Credential belongs to another user" msgstr "" @@ -972,42 +948,37 @@ msgstr "" msgid "Endpoint Group Project Association not found" msgstr "" -#: keystone/contrib/endpoint_policy/core.py:258 -#, python-format -msgid "No policy is associated with endpoint %(endpoint_id)s." -msgstr "" - -#: keystone/contrib/federation/controllers.py:274 -msgid "Missing entity ID from environment" -msgstr "" - -#: keystone/contrib/federation/controllers.py:282 +#: keystone/contrib/federation/controllers.py:268 msgid "Request must have an origin query parameter" msgstr "" -#: keystone/contrib/federation/controllers.py:292 +#: keystone/contrib/federation/controllers.py:273 #, python-format msgid "%(host)s is not a trusted dashboard host" msgstr "" -#: keystone/contrib/federation/controllers.py:333 +#: keystone/contrib/federation/controllers.py:304 +msgid "Missing entity ID from environment" +msgstr "" + +#: keystone/contrib/federation/controllers.py:344 msgid "Use a project scoped token when attempting to create a SAML assertion" msgstr "" -#: keystone/contrib/federation/idp.py:454 +#: keystone/contrib/federation/idp.py:476 #, python-format msgid "Cannot open certificate %(cert_file)s. Reason: %(reason)s" msgstr "" -#: keystone/contrib/federation/idp.py:521 +#: keystone/contrib/federation/idp.py:543 msgid "Ensure configuration option idp_entity_id is set." msgstr "" -#: keystone/contrib/federation/idp.py:524 +#: keystone/contrib/federation/idp.py:546 msgid "Ensure configuration option idp_sso_endpoint is set." msgstr "" -#: keystone/contrib/federation/idp.py:544 +#: keystone/contrib/federation/idp.py:566 msgid "" "idp_contact_type must be one of: [technical, other, support, " "administrative or billing." @@ -1017,95 +988,93 @@ msgstr "" msgid "Federation token is expired" msgstr "" -#: keystone/contrib/federation/utils.py:208 -msgid "" -"Could not find Identity Provider identifier in environment, check " -"[federation] remote_id_attribute for details." +#: keystone/contrib/federation/utils.py:231 +msgid "Could not find Identity Provider identifier in environment" msgstr "" -#: keystone/contrib/federation/utils.py:213 +#: keystone/contrib/federation/utils.py:235 msgid "" "Incoming identity provider identifier not included among the accepted " "identifiers." msgstr "" -#: keystone/contrib/federation/utils.py:501 +#: keystone/contrib/federation/utils.py:523 #, python-format msgid "User type %s not supported" msgstr "" -#: keystone/contrib/federation/utils.py:537 +#: keystone/contrib/federation/utils.py:559 #, python-format msgid "" "Invalid rule: %(identity_value)s. Both 'groups' and 'domain' keywords " "must be specified." msgstr "" -#: keystone/contrib/federation/utils.py:753 +#: keystone/contrib/federation/utils.py:775 #, python-format msgid "Identity Provider %(idp)s is disabled" msgstr "" -#: keystone/contrib/federation/utils.py:761 +#: keystone/contrib/federation/utils.py:783 #, python-format msgid "Service Provider %(sp)s is disabled" msgstr "" -#: keystone/contrib/oauth1/controllers.py:99 +#: keystone/contrib/oauth1/controllers.py:96 msgid "Cannot change consumer secret" msgstr "" -#: keystone/contrib/oauth1/controllers.py:131 +#: keystone/contrib/oauth1/controllers.py:128 msgid "Cannot list request tokens with a token issued via delegation." msgstr "" -#: keystone/contrib/oauth1/controllers.py:192 +#: keystone/contrib/oauth1/controllers.py:189 #: keystone/contrib/oauth1/backends/sql.py:270 msgid "User IDs do not match" msgstr "" -#: keystone/contrib/oauth1/controllers.py:199 +#: keystone/contrib/oauth1/controllers.py:196 msgid "Could not find role" msgstr "" -#: keystone/contrib/oauth1/controllers.py:248 +#: keystone/contrib/oauth1/controllers.py:245 msgid "Invalid signature" msgstr "" -#: keystone/contrib/oauth1/controllers.py:299 -#: keystone/contrib/oauth1/controllers.py:377 +#: keystone/contrib/oauth1/controllers.py:296 +#: keystone/contrib/oauth1/controllers.py:374 msgid "Request token is expired" msgstr "" -#: keystone/contrib/oauth1/controllers.py:313 +#: keystone/contrib/oauth1/controllers.py:310 msgid "There should not be any non-oauth parameters" msgstr "" -#: keystone/contrib/oauth1/controllers.py:317 +#: keystone/contrib/oauth1/controllers.py:314 msgid "provided consumer key does not match stored consumer key" msgstr "" -#: keystone/contrib/oauth1/controllers.py:321 +#: keystone/contrib/oauth1/controllers.py:318 msgid "provided verifier does not match stored verifier" msgstr "" -#: keystone/contrib/oauth1/controllers.py:325 +#: keystone/contrib/oauth1/controllers.py:322 msgid "provided request key does not match stored request key" msgstr "" -#: keystone/contrib/oauth1/controllers.py:329 +#: keystone/contrib/oauth1/controllers.py:326 msgid "Request Token does not have an authorizing user id" msgstr "" -#: keystone/contrib/oauth1/controllers.py:366 +#: keystone/contrib/oauth1/controllers.py:363 msgid "Cannot authorize a request token with a token issued via delegation." msgstr "" -#: keystone/contrib/oauth1/controllers.py:396 +#: keystone/contrib/oauth1/controllers.py:390 msgid "authorizing user does not have role required" msgstr "" -#: keystone/contrib/oauth1/controllers.py:409 +#: keystone/contrib/oauth1/controllers.py:403 msgid "User is not a member of the requested project" msgstr "" @@ -1126,23 +1095,28 @@ msgstr "" msgid "invalid date format %s" msgstr "" -#: keystone/contrib/revoke/core.py:150 +#: keystone/contrib/revoke/core.py:159 msgid "" "The revoke call must not have both domain_id and project_id. This is a " "bug in the Keystone server. The current request is aborted." msgstr "" -#: keystone/contrib/revoke/core.py:218 keystone/token/provider.py:207 -#: keystone/token/provider.py:230 keystone/token/provider.py:296 -#: keystone/token/provider.py:303 +#: keystone/contrib/revoke/core.py:227 keystone/token/provider.py:197 +#: keystone/token/provider.py:220 keystone/token/provider.py:286 +#: keystone/token/provider.py:293 msgid "Failed to validate token" msgstr "" -#: keystone/identity/controllers.py:72 +#: keystone/endpoint_policy/core.py:261 +#, python-format +msgid "No policy is associated with endpoint %(endpoint_id)s." +msgstr "" + +#: keystone/identity/controllers.py:74 msgid "Enabled field must be a boolean" msgstr "" -#: keystone/identity/controllers.py:98 +#: keystone/identity/controllers.py:100 msgid "Enabled field should be a boolean" msgstr "" @@ -1151,33 +1125,40 @@ msgstr "" msgid "Database at /domains/%s/config" msgstr "" -#: keystone/identity/core.py:287 keystone/identity/backends/ldap.py:59 -#: keystone/identity/backends/ldap.py:61 keystone/identity/backends/ldap.py:67 -#: keystone/identity/backends/ldap.py:69 keystone/identity/backends/sql.py:104 +#: keystone/identity/core.py:189 +#, python-format +msgid "" +"Domain specific sql drivers are not supported via the Identity API. One " +"is specified in /domains/%s/config" +msgstr "" + +#: keystone/identity/core.py:361 keystone/identity/backends/ldap.py:58 +#: keystone/identity/backends/ldap.py:60 keystone/identity/backends/ldap.py:66 +#: keystone/identity/backends/ldap.py:68 keystone/identity/backends/sql.py:104 #: keystone/identity/backends/sql.py:106 msgid "Invalid user / password" msgstr "" -#: keystone/identity/core.py:693 +#: keystone/identity/core.py:771 #, python-format msgid "User is disabled: %s" msgstr "" -#: keystone/identity/core.py:735 +#: keystone/identity/core.py:813 msgid "Cannot change user ID" msgstr "" -#: keystone/identity/backends/ldap.py:99 +#: keystone/identity/backends/ldap.py:98 msgid "Cannot change user name" msgstr "" -#: keystone/identity/backends/ldap.py:188 keystone/identity/backends/sql.py:188 +#: keystone/identity/backends/ldap.py:187 keystone/identity/backends/sql.py:188 #: keystone/identity/backends/sql.py:206 #, python-format msgid "User '%(user_id)s' not found in group '%(group_id)s'" msgstr "" -#: keystone/identity/backends/ldap.py:339 +#: keystone/identity/backends/ldap.py:338 #, python-format msgid "User %(user_id)s is already a member of group %(group_id)s" msgstr "" @@ -1186,198 +1167,168 @@ msgstr "" msgid "Found invalid token: scoped to both project and domain." msgstr "" -#: keystone/openstack/common/versionutils.py:108 -#, python-format -msgid "" -"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " -"may be removed in %(remove_in)s." -msgstr "" - -#: keystone/openstack/common/versionutils.py:112 -#, python-format -msgid "" -"%(what)s is deprecated as of %(as_of)s and may be removed in " -"%(remove_in)s. It will not be superseded." -msgstr "" - -#: keystone/openstack/common/versionutils.py:116 -#, python-format -msgid "%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s." -msgstr "" - -#: keystone/openstack/common/versionutils.py:119 -#, python-format -msgid "%(what)s is deprecated as of %(as_of)s. It will not be superseded." -msgstr "" - -#: keystone/openstack/common/versionutils.py:241 -#, python-format -msgid "Deprecated: %s" -msgstr "" - -#: keystone/openstack/common/versionutils.py:259 -#, python-format -msgid "Fatal call to deprecated config: %(msg)s" -msgstr "" - -#: keystone/resource/controllers.py:231 +#: keystone/resource/controllers.py:234 msgid "" "Cannot use parents_as_list and parents_as_ids query params at the same " "time." msgstr "" -#: keystone/resource/controllers.py:237 +#: keystone/resource/controllers.py:240 msgid "" "Cannot use subtree_as_list and subtree_as_ids query params at the same " "time." msgstr "" -#: keystone/resource/core.py:80 +#: keystone/resource/core.py:82 #, python-format msgid "max hierarchy depth reached for %s branch." msgstr "" -#: keystone/resource/core.py:97 +#: keystone/resource/core.py:100 msgid "cannot create a project within a different domain than its parents." msgstr "" -#: keystone/resource/core.py:101 +#: keystone/resource/core.py:104 #, python-format msgid "cannot create a project in a branch containing a disabled project: %s" msgstr "" -#: keystone/resource/core.py:123 +#: keystone/resource/core.py:126 #, python-format msgid "Domain is disabled: %s" msgstr "" -#: keystone/resource/core.py:141 +#: keystone/resource/core.py:145 #, python-format msgid "Domain cannot be named %s" msgstr "" -#: keystone/resource/core.py:144 +#: keystone/resource/core.py:148 #, python-format msgid "Domain cannot have ID %s" msgstr "" -#: keystone/resource/core.py:156 +#: keystone/resource/core.py:160 #, python-format msgid "Project is disabled: %s" msgstr "" -#: keystone/resource/core.py:176 +#: keystone/resource/core.py:180 #, python-format msgid "cannot enable project %s since it has disabled parents" msgstr "" -#: keystone/resource/core.py:184 +#: keystone/resource/core.py:188 #, python-format msgid "cannot disable project %s since its subtree contains enabled projects" msgstr "" -#: keystone/resource/core.py:195 +#: keystone/resource/core.py:199 msgid "Update of `parent_id` is not allowed." msgstr "" -#: keystone/resource/core.py:222 +#: keystone/resource/core.py:226 #, python-format msgid "cannot delete the project %s since it is not a leaf in the hierarchy." msgstr "" -#: keystone/resource/core.py:376 +#: keystone/resource/core.py:253 +msgid "Project field is required and cannot be empty." +msgstr "" + +#: keystone/resource/core.py:389 msgid "Multiple domains are not supported" msgstr "" -#: keystone/resource/core.py:429 +#: keystone/resource/core.py:442 msgid "delete the default domain" msgstr "" -#: keystone/resource/core.py:440 +#: keystone/resource/core.py:453 msgid "cannot delete a domain that is enabled, please disable it first." msgstr "" -#: keystone/resource/core.py:841 +#: keystone/resource/core.py:859 msgid "No options specified" msgstr "" -#: keystone/resource/core.py:847 +#: keystone/resource/core.py:865 #, python-format msgid "" "The value of group %(group)s specified in the config should be a " "dictionary of options" msgstr "" -#: keystone/resource/core.py:871 +#: keystone/resource/core.py:889 #, python-format msgid "" "Option %(option)s found with no group specified while checking domain " "configuration request" msgstr "" -#: keystone/resource/core.py:878 +#: keystone/resource/core.py:896 #, python-format msgid "Group %(group)s is not supported for domain specific configurations" msgstr "" -#: keystone/resource/core.py:885 +#: keystone/resource/core.py:903 #, python-format msgid "" "Option %(option)s in group %(group)s is not supported for domain specific" " configurations" msgstr "" -#: keystone/resource/core.py:938 +#: keystone/resource/core.py:956 msgid "An unexpected error occurred when retrieving domain configs" msgstr "" -#: keystone/resource/core.py:1013 keystone/resource/core.py:1097 -#: keystone/resource/core.py:1167 keystone/resource/config_backends/sql.py:70 +#: keystone/resource/core.py:1035 keystone/resource/core.py:1119 +#: keystone/resource/core.py:1190 keystone/resource/config_backends/sql.py:70 #, python-format msgid "option %(option)s in group %(group)s" msgstr "" -#: keystone/resource/core.py:1016 keystone/resource/core.py:1102 -#: keystone/resource/core.py:1163 +#: keystone/resource/core.py:1038 keystone/resource/core.py:1124 +#: keystone/resource/core.py:1186 #, python-format msgid "group %(group)s" msgstr "" -#: keystone/resource/core.py:1018 +#: keystone/resource/core.py:1040 msgid "any options" msgstr "" -#: keystone/resource/core.py:1062 +#: keystone/resource/core.py:1084 #, python-format msgid "" "Trying to update option %(option)s in group %(group)s, so that, and only " "that, option must be specified in the config" msgstr "" -#: keystone/resource/core.py:1067 +#: keystone/resource/core.py:1089 #, python-format msgid "" "Trying to update group %(group)s, so that, and only that, group must be " "specified in the config" msgstr "" -#: keystone/resource/core.py:1076 +#: keystone/resource/core.py:1098 #, python-format msgid "" "request to update group %(group)s, but config provided contains group " "%(group_other)s instead" msgstr "" -#: keystone/resource/core.py:1083 +#: keystone/resource/core.py:1105 #, python-format msgid "" "Trying to update option %(option)s in group %(group)s, but config " "provided contains option %(option_other)s instead" msgstr "" -#: keystone/resource/backends/ldap.py:151 -#: keystone/resource/backends/ldap.py:159 -#: keystone/resource/backends/ldap.py:163 +#: keystone/resource/backends/ldap.py:150 +#: keystone/resource/backends/ldap.py:158 +#: keystone/resource/backends/ldap.py:162 msgid "Domains are read-only against LDAP" msgstr "" @@ -1395,54 +1346,79 @@ msgstr "" #: keystone/token/controllers.py:391 #, python-format +msgid "Project ID not found: %(t_id)s" +msgstr "" + +#: keystone/token/controllers.py:395 +#, python-format msgid "User %(u_id)s is unauthorized for tenant %(t_id)s" msgstr "" -#: keystone/token/controllers.py:410 keystone/token/controllers.py:413 +#: keystone/token/controllers.py:414 keystone/token/controllers.py:417 msgid "Token does not belong to specified tenant." msgstr "" -#: keystone/token/persistence/backends/kvs.py:133 +#: keystone/token/persistence/backends/kvs.py:132 #, python-format msgid "Unknown token version %s" msgstr "" -#: keystone/token/providers/common.py:250 -#: keystone/token/providers/common.py:355 +#: keystone/token/providers/common.py:54 +msgid "Domains are not supported by the v2 API. Please use the v3 API instead." +msgstr "" + +#: keystone/token/providers/common.py:64 +#, python-format +msgid "" +"Project not found in the default domain (please use the v3 API instead): " +"%s" +msgstr "" + +#: keystone/token/providers/common.py:83 +#, python-format +msgid "User not found in the default domain (please use the v3 API instead): %s" +msgstr "" + +#: keystone/token/providers/common.py:292 +#: keystone/token/providers/common.py:397 #, python-format msgid "User %(user_id)s has no access to project %(project_id)s" msgstr "" -#: keystone/token/providers/common.py:255 -#: keystone/token/providers/common.py:360 +#: keystone/token/providers/common.py:297 +#: keystone/token/providers/common.py:402 #, python-format msgid "User %(user_id)s has no access to domain %(domain_id)s" msgstr "" -#: keystone/token/providers/common.py:282 +#: keystone/token/providers/common.py:324 msgid "Trustor is disabled." msgstr "" -#: keystone/token/providers/common.py:346 +#: keystone/token/providers/common.py:388 msgid "Trustee has no delegated roles." msgstr "" -#: keystone/token/providers/common.py:407 +#: keystone/token/providers/common.py:449 #, python-format msgid "Invalid audit info data type: %(data)s (%(type)s)" msgstr "" -#: keystone/token/providers/common.py:435 +#: keystone/token/providers/common.py:477 msgid "User is not a trustee." msgstr "" -#: keystone/token/providers/common.py:579 +#: keystone/token/providers/common.py:546 +msgid "The configured token provider does not support bind authentication." +msgstr "" + +#: keystone/token/providers/common.py:626 msgid "" "Attempting to use OS-FEDERATION token with V2 Identity Service, use V3 " "Authentication" msgstr "" -#: keystone/token/providers/common.py:597 +#: keystone/token/providers/common.py:644 msgid "Domain scoped token is not supported" msgstr "" @@ -1450,71 +1426,75 @@ msgstr "" msgid "Unable to sign token." msgstr "" -#: keystone/token/providers/fernet/core.py:210 +#: keystone/token/providers/fernet/core.py:182 msgid "" "This is not a v2.0 Fernet token. Use v3 for trust, domain, or federated " "tokens." msgstr "" -#: keystone/token/providers/fernet/token_formatters.py:189 +#: keystone/token/providers/fernet/token_formatters.py:80 +msgid "This is not a recognized Fernet token" +msgstr "" + +#: keystone/token/providers/fernet/token_formatters.py:202 #, python-format msgid "This is not a recognized Fernet payload version: %s" msgstr "" -#: keystone/trust/controllers.py:148 +#: keystone/trust/controllers.py:144 msgid "Redelegation allowed for delegated by trust only" msgstr "" -#: keystone/trust/controllers.py:181 +#: keystone/trust/controllers.py:177 msgid "The authenticated user should match the trustor." msgstr "" -#: keystone/trust/controllers.py:186 +#: keystone/trust/controllers.py:182 msgid "At least one role should be specified." msgstr "" -#: keystone/trust/core.py:57 +#: keystone/trust/core.py:61 #, python-format msgid "" "Remaining redelegation depth of %(redelegation_depth)d out of allowed " "range of [0..%(max_count)d]" msgstr "" -#: keystone/trust/core.py:66 +#: keystone/trust/core.py:70 #, python-format msgid "" "Field \"remaining_uses\" is set to %(value)s while it must not be set in " "order to redelegate a trust" msgstr "" -#: keystone/trust/core.py:77 +#: keystone/trust/core.py:81 msgid "Requested expiration time is more than redelegated trust can provide" msgstr "" -#: keystone/trust/core.py:87 +#: keystone/trust/core.py:91 msgid "Some of requested roles are not in redelegated trust" msgstr "" -#: keystone/trust/core.py:116 +#: keystone/trust/core.py:120 msgid "One of the trust agents is disabled or deleted" msgstr "" -#: keystone/trust/core.py:135 +#: keystone/trust/core.py:139 msgid "remaining_uses must be a positive integer or null." msgstr "" -#: keystone/trust/core.py:141 +#: keystone/trust/core.py:145 #, python-format msgid "" "Requested redelegation depth of %(requested_count)d is greater than " "allowed %(max_count)d" msgstr "" -#: keystone/trust/core.py:147 +#: keystone/trust/core.py:152 msgid "remaining_uses must not be set if redelegation is allowed" msgstr "" -#: keystone/trust/core.py:157 +#: keystone/trust/core.py:162 msgid "" "Modifying \"redelegation_count\" upon redelegation is forbidden. Omitting" " this parameter is advised." diff --git a/keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po index b7f255c4..6a6f1868 100644 --- a/keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Korean (Korea) (http://www.transifex.com/projects/p/keystone/" +"Language-Team: Korean (Korea) (http://www.transifex.com/openstack/keystone/" "language/ko_KR/)\n" "Language: ko_KR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "템플리트 파일 %s을(를) 열 수 없음" diff --git a/keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po index b7749060..43b0dc54 100644 --- a/keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,20 +7,19 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/keystone/" +"Language-Team: Polish (Poland) (http://www.transifex.com/openstack/keystone/" "language/pl_PL/)\n" "Language: pl_PL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Błąd podczas otwierania pliku %s" diff --git a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po index 689a23ec..48e0c8c7 100644 --- a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/openstack/" "keystone/language/pt_BR/)\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Não é possível abrir o arquivo de modelo %s" diff --git a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po index 5f81b98d..12e4591f 100644 --- a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po +++ b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po @@ -7,66 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" -"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-06-26 17:13+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/openstack/" "keystone/language/pt_BR/)\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: keystone/notifications.py:304 -msgid "Failed to construct notifier" -msgstr "" - -#: keystone/notifications.py:389 -#, python-format -msgid "Failed to send %(res_id)s %(event_type)s notification" -msgstr "Falha ao enviar notificação %(res_id)s %(event_type)s" - -#: keystone/notifications.py:606 -#, python-format -msgid "Failed to send %(action)s %(event_type)s notification" -msgstr "" - -#: keystone/catalog/core.py:62 -#, python-format -msgid "Malformed endpoint - %(url)r is not a string" -msgstr "" - -#: keystone/catalog/core.py:66 -#, python-format -msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" -msgstr "Endpoint mal formado %(url)s - chave desconhecida %(keyerror)s" - -#: keystone/catalog/core.py:71 -#, python-format -msgid "" -"Malformed endpoint '%(url)s'. The following type error occurred during " -"string substitution: %(typeerror)s" -msgstr "" - -#: keystone/catalog/core.py:77 -#, python-format -msgid "" -"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" -msgstr "" - -#: keystone/common/openssl.py:93 -#, python-format -msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" -msgstr "" - -#: keystone/common/openssl.py:121 -#, python-format -msgid "Failed to remove file %(file_path)r: %(error)s" -msgstr "" - -#: keystone/common/utils.py:239 msgid "" "Error setting up the debug environment. Verify that the option --debug-url " "has the format <host>:<port> and that a debugger processes is listening on " @@ -76,104 +28,29 @@ msgstr "" "possui o formato <host>:<port> e que o processo debugger está escutando " "nesta porta." -#: keystone/common/cache/core.py:100 #, python-format -msgid "" -"Unable to build cache config-key. Expected format \"<argname>:<value>\". " -"Skipping unknown format: %s" -msgstr "" -"Não é possível construir chave de configuração do cache. Formato esperado " -"\"<argname>:<value>\". Pulando formato desconhecido: %s" +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "Falha ao enviar notificação %(res_id)s %(event_type)s" + +msgid "Failed to validate token" +msgstr "Falha ao validar token" -#: keystone/common/environment/eventlet_server.py:99 #, python-format -msgid "Could not bind to %(host)s:%(port)s" -msgstr "" +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "Endpoint mal formado %(url)s - chave desconhecida %(keyerror)s" -#: keystone/common/environment/eventlet_server.py:185 msgid "Server error" msgstr "Erro do servidor" -#: keystone/contrib/endpoint_policy/core.py:129 -#: keystone/contrib/endpoint_policy/core.py:228 -#, python-format -msgid "" -"Circular reference or a repeated entry found in region tree - %(region_id)s." -msgstr "" - -#: keystone/contrib/federation/idp.py:410 -#, python-format -msgid "Error when signing assertion, reason: %(reason)s" -msgstr "" - -#: keystone/contrib/oauth1/core.py:136 -msgid "Cannot retrieve Authorization headers" -msgstr "" - -#: keystone/openstack/common/loopingcall.py:95 -msgid "in fixed duration looping call" -msgstr "em uma chamada de laço de duração fixa" - -#: keystone/openstack/common/loopingcall.py:138 -msgid "in dynamic looping call" -msgstr "em chamada de laço dinâmico" - -#: keystone/openstack/common/service.py:268 -msgid "Unhandled exception" -msgstr "Exceção não tratada" - -#: keystone/resource/core.py:477 -#, python-format -msgid "" -"Circular reference or a repeated entry found projects hierarchy - " -"%(project_id)s." -msgstr "" - -#: keystone/resource/core.py:939 -#, python-format -msgid "" -"Unexpected results in response for domain config - %(count)s responses, " -"first option is %(option)s, expected option %(expected)s" -msgstr "" - -#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 #, python-format msgid "" -"Circular reference or a repeated entry found in projects hierarchy - " -"%(project_id)s." +"Unable to build cache config-key. Expected format \"<argname>:<value>\". " +"Skipping unknown format: %s" msgstr "" +"Não é possível construir chave de configuração do cache. Formato esperado " +"\"<argname>:<value>\". Pulando formato desconhecido: %s" -#: keystone/token/provider.py:292 #, python-format msgid "Unexpected error or malformed token determining token expiry: %s" msgstr "" "Erro inesperado ou token mal formado ao determinar validade do token: %s" - -#: keystone/token/persistence/backends/kvs.py:226 -#, python-format -msgid "" -"Reinitializing revocation list due to error in loading revocation list from " -"backend. Expected `list` type got `%(type)s`. Old revocation list data: " -"%(list)r" -msgstr "" - -#: keystone/token/providers/common.py:611 -msgid "Failed to validate token" -msgstr "Falha ao validar token" - -#: keystone/token/providers/pki.py:47 -msgid "Unable to sign token" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:38 -#, python-format -msgid "" -"Either [fernet_tokens] key_repository does not exist or Keystone does not " -"have sufficient permission to access it: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:79 -msgid "" -"Failed to create [fernet_tokens] key_repository: either it already exists or " -"you don't have sufficient permissions to create it" -msgstr "" diff --git a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po index fdb771c9..02ff0550 100644 --- a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po +++ b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po @@ -10,1537 +10,325 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-23 06:04+0000\n" -"PO-Revision-Date: 2015-03-21 23:03+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-08-04 18:01+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Portuguese (Brazil) " -"(http://www.transifex.com/projects/p/keystone/language/pt_BR/)\n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/openstack/" +"keystone/language/pt_BR/)\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" -#: keystone/clean.py:24 -#, python-format -msgid "%s cannot be empty." -msgstr "%s não pode estar vazio." - -#: keystone/clean.py:26 #, python-format msgid "%(property_name)s cannot be less than %(min_length)s characters." msgstr "%(property_name)s não pode ter menos de %(min_length)s caracteres." -#: keystone/clean.py:31 -#, python-format -msgid "%(property_name)s should not be greater than %(max_length)s characters." -msgstr "%(property_name)s não deve ter mais de %(max_length)s caracteres." - -#: keystone/clean.py:40 #, python-format msgid "%(property_name)s is not a %(display_expected_type)s" msgstr "%(property_name)s não é um %(display_expected_type)s" -#: keystone/cli.py:283 -msgid "At least one option must be provided" -msgstr "" - -#: keystone/cli.py:290 -msgid "--all option cannot be mixed with other options" -msgstr "" - -#: keystone/cli.py:301 -#, python-format -msgid "Unknown domain '%(name)s' specified by --domain-name" -msgstr "" - -#: keystone/cli.py:365 keystone/tests/unit/test_cli.py:213 -msgid "At least one option must be provided, use either --all or --domain-name" -msgstr "" - -#: keystone/cli.py:371 keystone/tests/unit/test_cli.py:229 -msgid "The --all option cannot be used with the --domain-name option" -msgstr "" - -#: keystone/cli.py:397 keystone/tests/unit/test_cli.py:246 -#, python-format -msgid "" -"Invalid domain name: %(domain)s found in config file name: %(file)s - " -"ignoring this file." -msgstr "" - -#: keystone/cli.py:405 keystone/tests/unit/test_cli.py:187 -#, python-format -msgid "" -"Domain: %(domain)s already has a configuration defined - ignoring file: " -"%(file)s." -msgstr "" - -#: keystone/cli.py:419 -#, python-format -msgid "Error parsing configuration file for domain: %(domain)s, file: %(file)s." -msgstr "" - -#: keystone/cli.py:452 -#, python-format -msgid "" -"To get a more detailed information on this error, re-run this command for" -" the specific domain, i.e.: keystone-manage domain_config_upload " -"--domain-name %s" -msgstr "" - -#: keystone/cli.py:470 -#, python-format -msgid "Unable to locate domain config directory: %s" -msgstr "Não é possível localizar diretório de configuração de domínio: %s" - -#: keystone/cli.py:503 -msgid "" -"Unable to access the keystone database, please check it is configured " -"correctly." -msgstr "" - -#: keystone/exception.py:79 -#, python-format -msgid "" -"Expecting to find %(attribute)s in %(target)s - the server could not " -"comply with the request since it is either malformed or otherwise " -"incorrect. The client is assumed to be in error." -msgstr "" - -#: keystone/exception.py:90 -#, python-format -msgid "%(detail)s" -msgstr "" - -#: keystone/exception.py:94 -msgid "" -"Timestamp not in expected format. The server could not comply with the " -"request since it is either malformed or otherwise incorrect. The client " -"is assumed to be in error." -msgstr "" -"A data não está no formato especificado. O servidor não pôde realizar a " -"requisição pois ela está mal formada ou incorreta. Assume-se que o " -"cliente está com erro." - -#: keystone/exception.py:103 #, python-format -msgid "" -"String length exceeded.The length of string '%(string)s' exceeded the " -"limit of column %(type)s(CHAR(%(length)d))." -msgstr "" -"Comprimento de string excedido. O comprimento de string '%(string)s' " -"excedeu o limite da coluna %(type)s(CHAR(%(length)d))." - -#: keystone/exception.py:109 -#, python-format -msgid "" -"Request attribute %(attribute)s must be less than or equal to %(size)i. " -"The server could not comply with the request because the attribute size " -"is invalid (too large). The client is assumed to be in error." -msgstr "" -"Atributo de requisição %(attribute)s deve ser menor ou igual a %(size)i. " -"O servidor não pôde atender a requisição porque o tamanho do atributo é " -"inválido (muito grande). Assume-se que o cliente está em erro." - -#: keystone/exception.py:119 -#, python-format -msgid "" -"The specified parent region %(parent_region_id)s would create a circular " -"region hierarchy." -msgstr "" - -#: keystone/exception.py:126 -#, python-format -msgid "" -"The password length must be less than or equal to %(size)i. The server " -"could not comply with the request because the password is invalid." -msgstr "" - -#: keystone/exception.py:134 -#, python-format -msgid "" -"Unable to delete region %(region_id)s because it or its child regions " -"have associated endpoints." -msgstr "" - -#: keystone/exception.py:141 -msgid "" -"The certificates you requested are not available. It is likely that this " -"server does not use PKI tokens otherwise this is the result of " -"misconfiguration." -msgstr "" - -#: keystone/exception.py:150 -msgid "(Disable debug mode to suppress these details.)" -msgstr "" +msgid "%(property_name)s should not be greater than %(max_length)s characters." +msgstr "%(property_name)s não deve ter mais de %(max_length)s caracteres." -#: keystone/exception.py:155 #, python-format -msgid "%(message)s %(amendment)s" -msgstr "" - -#: keystone/exception.py:163 -msgid "The request you have made requires authentication." -msgstr "A requisição que você fez requer autenticação." - -#: keystone/exception.py:169 -msgid "Authentication plugin error." -msgstr "Erro do plugin de autenticação." +msgid "%s cannot be empty." +msgstr "%s não pode estar vazio." -#: keystone/exception.py:177 -#, python-format -msgid "Unable to find valid groups while using mapping %(mapping_id)s" -msgstr "" +msgid "Access token is expired" +msgstr "Token de acesso expirou" -#: keystone/exception.py:182 -msgid "Attempted to authenticate with an unsupported method." -msgstr "Tentativa de autenticação com um método não suportado." +msgid "Access token not found" +msgstr "Token de acesso não encontrado" -#: keystone/exception.py:190 msgid "Additional authentications steps required." msgstr "Passos de autenticação adicionais requeridos." -#: keystone/exception.py:198 -msgid "You are not authorized to perform the requested action." -msgstr "Você não está autorizado à realizar a ação solicitada." - -#: keystone/exception.py:205 -#, python-format -msgid "You are not authorized to perform the requested action: %(action)s" -msgstr "" - -#: keystone/exception.py:210 -#, python-format -msgid "" -"Could not change immutable attribute(s) '%(attributes)s' in target " -"%(target)s" -msgstr "" - -#: keystone/exception.py:215 -#, python-format -msgid "" -"Group membership across backend boundaries is not allowed, group in " -"question is %(group_id)s, user is %(user_id)s" -msgstr "" - -#: keystone/exception.py:221 -#, python-format -msgid "" -"Invalid mix of entities for policy association - only Endpoint, Service " -"or Region+Service allowed. Request was - Endpoint: %(endpoint_id)s, " -"Service: %(service_id)s, Region: %(region_id)s" -msgstr "" - -#: keystone/exception.py:228 -#, python-format -msgid "Invalid domain specific configuration: %(reason)s" -msgstr "" - -#: keystone/exception.py:232 -#, python-format -msgid "Could not find: %(target)s" -msgstr "" - -#: keystone/exception.py:238 -#, python-format -msgid "Could not find endpoint: %(endpoint_id)s" -msgstr "" - -#: keystone/exception.py:245 msgid "An unhandled exception has occurred: Could not find metadata." msgstr "Uma exceção não tratada ocorreu: Não foi possível encontrar metadados." -#: keystone/exception.py:250 -#, python-format -msgid "Could not find policy: %(policy_id)s" -msgstr "" - -#: keystone/exception.py:254 -msgid "Could not find policy association" -msgstr "" - -#: keystone/exception.py:258 -#, python-format -msgid "Could not find role: %(role_id)s" -msgstr "" - -#: keystone/exception.py:262 -#, python-format -msgid "" -"Could not find role assignment with role: %(role_id)s, user or group: " -"%(actor_id)s, project or domain: %(target_id)s" -msgstr "" - -#: keystone/exception.py:268 -#, python-format -msgid "Could not find region: %(region_id)s" -msgstr "" - -#: keystone/exception.py:272 -#, python-format -msgid "Could not find service: %(service_id)s" -msgstr "" - -#: keystone/exception.py:276 -#, python-format -msgid "Could not find domain: %(domain_id)s" -msgstr "" - -#: keystone/exception.py:280 -#, python-format -msgid "Could not find project: %(project_id)s" -msgstr "" - -#: keystone/exception.py:284 -#, python-format -msgid "Cannot create project with parent: %(project_id)s" -msgstr "" - -#: keystone/exception.py:288 -#, python-format -msgid "Could not find token: %(token_id)s" -msgstr "" - -#: keystone/exception.py:292 -#, python-format -msgid "Could not find user: %(user_id)s" -msgstr "" - -#: keystone/exception.py:296 -#, python-format -msgid "Could not find group: %(group_id)s" -msgstr "" - -#: keystone/exception.py:300 -#, python-format -msgid "Could not find mapping: %(mapping_id)s" -msgstr "" - -#: keystone/exception.py:304 -#, python-format -msgid "Could not find trust: %(trust_id)s" -msgstr "" - -#: keystone/exception.py:308 -#, python-format -msgid "No remaining uses for trust: %(trust_id)s" -msgstr "" - -#: keystone/exception.py:312 -#, python-format -msgid "Could not find credential: %(credential_id)s" -msgstr "" - -#: keystone/exception.py:316 -#, python-format -msgid "Could not find version: %(version)s" -msgstr "" - -#: keystone/exception.py:320 -#, python-format -msgid "Could not find Endpoint Group: %(endpoint_group_id)s" -msgstr "" - -#: keystone/exception.py:324 -#, python-format -msgid "Could not find Identity Provider: %(idp_id)s" -msgstr "" - -#: keystone/exception.py:328 -#, python-format -msgid "Could not find Service Provider: %(sp_id)s" -msgstr "" - -#: keystone/exception.py:332 -#, python-format -msgid "" -"Could not find federated protocol %(protocol_id)s for Identity Provider: " -"%(idp_id)s" -msgstr "" - -#: keystone/exception.py:343 -#, python-format -msgid "" -"Could not find %(group_or_option)s in domain configuration for domain " -"%(domain_id)s" -msgstr "" - -#: keystone/exception.py:348 -#, python-format -msgid "Conflict occurred attempting to store %(type)s - %(details)s" -msgstr "" - -#: keystone/exception.py:356 -msgid "An unexpected error prevented the server from fulfilling your request." -msgstr "" - -#: keystone/exception.py:359 -#, python-format -msgid "" -"An unexpected error prevented the server from fulfilling your request: " -"%(exception)s" -msgstr "" - -#: keystone/exception.py:382 -#, python-format -msgid "Unable to consume trust %(trust_id)s, unable to acquire lock." -msgstr "" - -#: keystone/exception.py:387 -msgid "" -"Expected signing certificates are not available on the server. Please " -"check Keystone configuration." -msgstr "" - -#: keystone/exception.py:393 -#, python-format -msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." -msgstr "" -"URL de endpoint mal-formada (%(endpoint)s), veja o log de ERROS para " -"detalhes." - -#: keystone/exception.py:398 -#, python-format -msgid "" -"Group %(group_id)s returned by mapping %(mapping_id)s was not found in " -"the backend." -msgstr "" - -#: keystone/exception.py:403 -#, python-format -msgid "Error while reading metadata file, %(reason)s" -msgstr "" - -#: keystone/exception.py:407 -#, python-format -msgid "" -"Unexpected combination of grant attributes - User: %(user_id)s, Group: " -"%(group_id)s, Project: %(project_id)s, Domain: %(domain_id)s" -msgstr "" - -#: keystone/exception.py:414 -msgid "The action you have requested has not been implemented." -msgstr "A ação que você solicitou não foi implementada." - -#: keystone/exception.py:421 -msgid "The service you have requested is no longer available on this server." -msgstr "" - -#: keystone/exception.py:428 -#, python-format -msgid "The Keystone configuration file %(config_file)s could not be found." -msgstr "" - -#: keystone/exception.py:433 -msgid "" -"No encryption keys found; run keystone-manage fernet_setup to bootstrap " -"one." -msgstr "" - -#: keystone/exception.py:438 -#, python-format -msgid "" -"The Keystone domain-specific configuration has specified more than one " -"SQL driver (only one is permitted): %(source)s." -msgstr "" +msgid "Attempted to authenticate with an unsupported method." +msgstr "Tentativa de autenticação com um método não suportado." -#: keystone/exception.py:445 -#, python-format -msgid "" -"%(mod_name)s doesn't provide database migrations. The migration " -"repository path at %(path)s doesn't exist or isn't a directory." -msgstr "" +msgid "Authentication plugin error." +msgstr "Erro do plugin de autenticação." -#: keystone/exception.py:457 #, python-format -msgid "" -"Unable to sign SAML assertion. It is likely that this server does not " -"have xmlsec1 installed, or this is the result of misconfiguration. Reason" -" %(reason)s" -msgstr "" - -#: keystone/exception.py:465 -msgid "" -"No Authorization headers found, cannot proceed with OAuth related calls, " -"if running under HTTPd or Apache, ensure WSGIPassAuthorization is set to " -"On." -msgstr "" +msgid "Cannot change %(option_name)s %(attr)s" +msgstr "Não é possível alterar %(option_name)s %(attr)s" -#: keystone/notifications.py:250 -#, python-format -msgid "%(event)s is not a valid notification event, must be one of: %(actions)s" -msgstr "" +msgid "Cannot change consumer secret" +msgstr "Não é possível alterar segredo do consumidor" -#: keystone/notifications.py:259 #, python-format -msgid "Method not callable: %s" -msgstr "" - -#: keystone/assignment/controllers.py:107 keystone/identity/controllers.py:69 -#: keystone/resource/controllers.py:78 -msgid "Name field is required and cannot be empty" -msgstr "Campo nome é requerido e não pode ser vazio" - -#: keystone/assignment/controllers.py:330 -#: keystone/assignment/controllers.py:753 -msgid "Specify a domain or project, not both" -msgstr "Especifique um domínio ou projeto, não ambos" - -#: keystone/assignment/controllers.py:333 -msgid "Specify one of domain or project" -msgstr "" - -#: keystone/assignment/controllers.py:338 -#: keystone/assignment/controllers.py:758 -msgid "Specify a user or group, not both" -msgstr "Epecifique um usuário ou grupo, não ambos" - -#: keystone/assignment/controllers.py:341 -msgid "Specify one of user or group" -msgstr "" - -#: keystone/assignment/controllers.py:742 -msgid "Combining effective and group filter will always result in an empty list." -msgstr "" +msgid "Cannot remove role that has not been granted, %s" +msgstr "Não é possível remover role que não foi concedido, %s" -#: keystone/assignment/controllers.py:747 -msgid "" -"Combining effective, domain and inherited filters will always result in " -"an empty list." -msgstr "" +msgid "Consumer not found" +msgstr "Consumidor não encontrado" -#: keystone/assignment/core.py:228 -msgid "Must specify either domain or project" -msgstr "" +msgid "Could not find role" +msgstr "Não é possível encontrar role" -#: keystone/assignment/core.py:493 -#, python-format -msgid "Project (%s)" -msgstr "Projeto (%s)" +msgid "Credential belongs to another user" +msgstr "A credencial pertence à outro usuário" -#: keystone/assignment/core.py:495 #, python-format msgid "Domain (%s)" msgstr "Domínio (%s)" -#: keystone/assignment/core.py:497 -msgid "Unknown Target" -msgstr "Alvo Desconhecido" - -#: keystone/assignment/backends/ldap.py:92 -msgid "Domain metadata not supported by LDAP" -msgstr "" - -#: keystone/assignment/backends/ldap.py:381 -#, python-format -msgid "User %(user_id)s already has role %(role_id)s in tenant %(tenant_id)s" -msgstr "" - -#: keystone/assignment/backends/ldap.py:387 #, python-format -msgid "Role %s not found" -msgstr "Role %s não localizada" - -#: keystone/assignment/backends/ldap.py:402 -#: keystone/assignment/backends/sql.py:335 -#, python-format -msgid "Cannot remove role that has not been granted, %s" -msgstr "Não é possível remover role que não foi concedido, %s" +msgid "Domain is disabled: %s" +msgstr "O domínio está desativado: %s" -#: keystone/assignment/backends/sql.py:356 -#, python-format -msgid "Unexpected assignment type encountered, %s" -msgstr "" +msgid "Domain scoped token is not supported" +msgstr "O token de escopo de domínio não é suportado" -#: keystone/assignment/role_backends/ldap.py:61 keystone/catalog/core.py:103 -#: keystone/common/ldap/core.py:1400 keystone/resource/backends/ldap.py:149 #, python-format msgid "Duplicate ID, %s." msgstr "ID duplicado, %s." -#: keystone/assignment/role_backends/ldap.py:69 -#: keystone/common/ldap/core.py:1390 #, python-format msgid "Duplicate name, %s." msgstr "Nome duplicado, %s." -#: keystone/assignment/role_backends/ldap.py:119 -#, python-format -msgid "Cannot duplicate name %s" -msgstr "" - -#: keystone/auth/controllers.py:60 -#, python-format -msgid "" -"Cannot load an auth-plugin by class-name without a \"method\" attribute " -"defined: %s" -msgstr "" - -#: keystone/auth/controllers.py:71 -#, python-format -msgid "" -"Auth plugin %(plugin)s is requesting previously registered method " -"%(method)s" -msgstr "" - -#: keystone/auth/controllers.py:115 -#, python-format -msgid "" -"Unable to reconcile identity attribute %(attribute)s as it has " -"conflicting values %(new)s and %(old)s" -msgstr "" - -#: keystone/auth/controllers.py:336 -msgid "Scoping to both domain and project is not allowed" -msgstr "A definição de escopo para o domínio e o projeto não é permitida" - -#: keystone/auth/controllers.py:339 -msgid "Scoping to both domain and trust is not allowed" -msgstr "A definição de escopo para o domínio e a trust não é permitida" - -#: keystone/auth/controllers.py:342 -msgid "Scoping to both project and trust is not allowed" -msgstr "A definição de escopo para o projeto e a trust não é permitida" - -#: keystone/auth/controllers.py:512 -msgid "User not found" -msgstr "Usuário não localizado" - -#: keystone/auth/controllers.py:616 -msgid "A project-scoped token is required to produce a service catalog." -msgstr "" - -#: keystone/auth/plugins/external.py:46 -msgid "No authenticated user" -msgstr "Nenhum usuário autenticado" - -#: keystone/auth/plugins/external.py:56 -#, python-format -msgid "Unable to lookup user %s" -msgstr "Não é possível consultar o usuário %s" - -#: keystone/auth/plugins/external.py:107 -msgid "auth_type is not Negotiate" -msgstr "" - -#: keystone/auth/plugins/mapped.py:244 -msgid "Could not map user" -msgstr "" - -#: keystone/auth/plugins/oauth1.py:39 -#, python-format -msgid "%s not supported" -msgstr "" - -#: keystone/auth/plugins/oauth1.py:57 -msgid "Access token is expired" -msgstr "Token de acesso expirou" - -#: keystone/auth/plugins/oauth1.py:71 -msgid "Could not validate the access token" -msgstr "" - -#: keystone/auth/plugins/password.py:46 -msgid "Invalid username or password" -msgstr "Nome de usuário ou senha inválidos" - -#: keystone/auth/plugins/token.py:72 keystone/token/controllers.py:160 -msgid "rescope a scoped token" -msgstr "" - -#: keystone/catalog/controllers.py:168 -#, python-format -msgid "Conflicting region IDs specified: \"%(url_id)s\" != \"%(ref_id)s\"" -msgstr "" - -#: keystone/common/authorization.py:47 keystone/common/wsgi.py:64 -#, python-format -msgid "token reference must be a KeystoneToken type, got: %s" -msgstr "" - -#: keystone/common/base64utils.py:66 -msgid "pad must be single character" -msgstr "" - -#: keystone/common/base64utils.py:215 -#, python-format -msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" -msgstr "" +msgid "Enabled field must be a boolean" +msgstr "Campo habilitado precisa ser um booleano" -#: keystone/common/base64utils.py:219 -#, python-format -msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" -msgstr "" +msgid "Enabled field should be a boolean" +msgstr "Campo habilitado deve ser um booleano" -#: keystone/common/base64utils.py:225 #, python-format -msgid "text is not a multiple of 4, but contains pad \"%s\"" -msgstr "" - -#: keystone/common/base64utils.py:244 keystone/common/base64utils.py:265 -msgid "padded base64url text must be multiple of 4 characters" -msgstr "" - -#: keystone/common/controller.py:237 keystone/token/providers/common.py:589 -msgid "Non-default domain is not supported" -msgstr "O domínio não padrão não é suportado" +msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" +msgstr "Endpoint %(endpoint_id)s não encontrado no projeto %(project_id)s" -#: keystone/common/controller.py:305 keystone/identity/core.py:428 -#: keystone/resource/core.py:761 keystone/resource/backends/ldap.py:61 #, python-format msgid "Expected dict or list: %s" msgstr "Esperado dict ou list: %s" -#: keystone/common/controller.py:318 -msgid "Marker could not be found" -msgstr "Marcador não pôde ser encontrado" - -#: keystone/common/controller.py:329 -msgid "Invalid limit value" -msgstr "Valor limite inválido" - -#: keystone/common/controller.py:637 -msgid "Cannot change Domain ID" -msgstr "" - -#: keystone/common/controller.py:666 -msgid "domain_id is required as part of entity" -msgstr "" - -#: keystone/common/controller.py:701 -msgid "A domain-scoped token must be used" -msgstr "" - -#: keystone/common/dependency.py:68 -#, python-format -msgid "Unregistered dependency: %(name)s for %(targets)s" -msgstr "" - -#: keystone/common/dependency.py:108 -msgid "event_callbacks must be a dict" -msgstr "" - -#: keystone/common/dependency.py:113 -#, python-format -msgid "event_callbacks[%s] must be a dict" -msgstr "" - -#: keystone/common/pemutils.py:223 -#, python-format -msgid "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" -msgstr "" - -#: keystone/common/pemutils.py:242 -#, python-format -msgid "" -"unknown pem header \"%(pem_header)s\", valid headers are: " -"%(valid_pem_headers)s" -msgstr "" - -#: keystone/common/pemutils.py:298 -#, python-format -msgid "failed to find end matching \"%s\"" -msgstr "" - -#: keystone/common/pemutils.py:302 -#, python-format -msgid "" -"beginning & end PEM headers do not match (%(begin_pem_header)s!= " -"%(end_pem_header)s)" -msgstr "" - -#: keystone/common/pemutils.py:377 -#, python-format -msgid "unknown pem_type: \"%s\"" -msgstr "" - -#: keystone/common/pemutils.py:389 -#, python-format -msgid "" -"failed to base64 decode %(pem_type)s PEM at position%(position)d: " -"%(err_msg)s" -msgstr "" - -#: keystone/common/utils.py:164 keystone/credential/controllers.py:44 -msgid "Invalid blob in credential" -msgstr "BLOB inválido na credencial" - -#: keystone/common/wsgi.py:330 -#, python-format -msgid "%s field is required and cannot be empty" -msgstr "" - -#: keystone/common/wsgi.py:342 -#, python-format -msgid "%s field(s) cannot be empty" -msgstr "" - -#: keystone/common/wsgi.py:563 -msgid "The resource could not be found." -msgstr "O recurso não pôde ser localizado." - -#: keystone/common/wsgi.py:704 -#, python-format -msgid "Unexpected status requested for JSON Home response, %s" -msgstr "" - -#: keystone/common/cache/_memcache_pool.py:113 -#, python-format -msgid "Unable to get a connection from pool id %(id)s after %(seconds)s seconds." -msgstr "" - -#: keystone/common/cache/core.py:132 -msgid "region not type dogpile.cache.CacheRegion" -msgstr "região não é do tipo dogpile.cache.CacheRegion" - -#: keystone/common/cache/backends/mongo.py:231 -msgid "db_hosts value is required" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:236 -msgid "database db_name is required" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:241 -msgid "cache_collection name is required" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:252 -msgid "integer value expected for w (write concern attribute)" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:260 -msgid "replicaset_name required when use_replica is True" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:275 -msgid "integer value expected for mongo_ttl_seconds" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:301 -msgid "no ssl support available" -msgstr "" - -#: keystone/common/cache/backends/mongo.py:310 -#, python-format -msgid "" -"Invalid ssl_cert_reqs value of %s, must be one of \"NONE\", \"OPTIONAL\"," -" \"REQUIRED\"" -msgstr "" - -#: keystone/common/kvs/core.py:71 -#, python-format -msgid "Lock Timeout occurred for key, %(target)s" -msgstr "" - -#: keystone/common/kvs/core.py:106 -#, python-format -msgid "KVS region %s is already configured. Cannot reconfigure." -msgstr "" - -#: keystone/common/kvs/core.py:145 -#, python-format -msgid "Key Value Store not configured: %s" -msgstr "" - -#: keystone/common/kvs/core.py:198 -msgid "`key_mangler` option must be a function reference" -msgstr "" - -#: keystone/common/kvs/core.py:353 -#, python-format -msgid "Lock key must match target key: %(lock)s != %(target)s" -msgstr "" - -#: keystone/common/kvs/core.py:357 -msgid "Must be called within an active lock context." -msgstr "" - -#: keystone/common/kvs/backends/memcached.py:69 -#, python-format -msgid "Maximum lock attempts on %s occurred." -msgstr "" - -#: keystone/common/kvs/backends/memcached.py:108 -#, python-format -msgid "" -"Backend `%(driver)s` is not a valid memcached backend. Valid drivers: " -"%(driver_list)s" -msgstr "" - -#: keystone/common/kvs/backends/memcached.py:178 -msgid "`key_mangler` functions must be callable." -msgstr "" - -#: keystone/common/ldap/core.py:191 -#, python-format -msgid "Invalid LDAP deref option: %(option)s. Choose one of: %(options)s" -msgstr "" +msgid "Failed to validate token" +msgstr "Falha ao validar token" -#: keystone/common/ldap/core.py:201 #, python-format msgid "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" msgstr "" "Opção de certificado LADP TLS inválida: %(option)s. Escolha uma de: " "%(options)s" -#: keystone/common/ldap/core.py:213 +#, python-format +msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" +msgstr "Opção LDAP TLS_AVAIL inválida: %s. TLS não dsponível" + #, python-format msgid "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" msgstr "Escopo LDAP inválido: %(scope)s. Escolha um de: %(options)s" -#: keystone/common/ldap/core.py:588 msgid "Invalid TLS / LDAPS combination" msgstr "Combinação TLS / LADPS inválida" -#: keystone/common/ldap/core.py:593 -#, python-format -msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" -msgstr "Opção LDAP TLS_AVAIL inválida: %s. TLS não dsponível" - -#: keystone/common/ldap/core.py:603 -#, python-format -msgid "tls_cacertfile %s not found or is not a file" -msgstr "tls_cacertfile %s não encontrada ou não é um arquivo" +msgid "Invalid blob in credential" +msgstr "BLOB inválido na credencial" -#: keystone/common/ldap/core.py:615 -#, python-format -msgid "tls_cacertdir %s not found or is not a directory" -msgstr "tls_cacertdir %s não encontrado ou não é um diretório" +msgid "Invalid limit value" +msgstr "Valor limite inválido" -#: keystone/common/ldap/core.py:1325 -#, python-format -msgid "ID attribute %(id_attr)s not found in LDAP object %(dn)s" -msgstr "" +msgid "Invalid username or password" +msgstr "Nome de usuário ou senha inválidos" -#: keystone/common/ldap/core.py:1369 #, python-format msgid "LDAP %s create" msgstr "Criação de LDAP %s" -#: keystone/common/ldap/core.py:1374 -#, python-format -msgid "LDAP %s update" -msgstr "Atualização de LDAP %s" - -#: keystone/common/ldap/core.py:1379 #, python-format msgid "LDAP %s delete" msgstr "Exclusão de LDAP %s" -#: keystone/common/ldap/core.py:1521 -msgid "" -"Disabling an entity where the 'enable' attribute is ignored by " -"configuration." -msgstr "" - -#: keystone/common/ldap/core.py:1532 #, python-format -msgid "Cannot change %(option_name)s %(attr)s" -msgstr "Não é possível alterar %(option_name)s %(attr)s" - -#: keystone/common/ldap/core.py:1619 -#, python-format -msgid "Member %(member)s is already a member of group %(group)s" -msgstr "" - -#: keystone/common/sql/core.py:219 -msgid "" -"Cannot truncate a driver call without hints list as first parameter after" -" self " -msgstr "" - -#: keystone/common/sql/core.py:410 -msgid "Duplicate Entry" -msgstr "" - -#: keystone/common/sql/core.py:426 -#, python-format -msgid "An unexpected error occurred when trying to store %s" -msgstr "" - -#: keystone/common/sql/migration_helpers.py:187 -#: keystone/common/sql/migration_helpers.py:245 -#, python-format -msgid "%s extension does not exist." -msgstr "" +msgid "LDAP %s update" +msgstr "Atualização de LDAP %s" -#: keystone/common/validation/validators.py:54 #, python-format -msgid "Invalid input for field '%(path)s'. The value is '%(value)s'." +msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." msgstr "" +"URL de endpoint mal-formada (%(endpoint)s), veja o log de ERROS para " +"detalhes." -#: keystone/contrib/ec2/controllers.py:318 -msgid "Token belongs to another user" -msgstr "O token pertence à outro usuário" - -#: keystone/contrib/ec2/controllers.py:346 -msgid "Credential belongs to another user" -msgstr "A credencial pertence à outro usuário" - -#: keystone/contrib/endpoint_filter/backends/sql.py:69 -#, python-format -msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" -msgstr "Endpoint %(endpoint_id)s não encontrado no projeto %(project_id)s" +msgid "Marker could not be found" +msgstr "Marcador não pôde ser encontrado" -#: keystone/contrib/endpoint_filter/backends/sql.py:180 -msgid "Endpoint Group Project Association not found" -msgstr "" +msgid "Name field is required and cannot be empty" +msgstr "Campo nome é requerido e não pode ser vazio" -#: keystone/contrib/endpoint_policy/core.py:258 -#, python-format -msgid "No policy is associated with endpoint %(endpoint_id)s." -msgstr "" +msgid "No authenticated user" +msgstr "Nenhum usuário autenticado" -#: keystone/contrib/federation/controllers.py:274 -msgid "Missing entity ID from environment" -msgstr "" +msgid "No options specified" +msgstr "Nenhuma opção especificada" -#: keystone/contrib/federation/controllers.py:282 -msgid "Request must have an origin query parameter" -msgstr "" +msgid "Non-default domain is not supported" +msgstr "O domínio não padrão não é suportado" -#: keystone/contrib/federation/controllers.py:292 #, python-format -msgid "%(host)s is not a trusted dashboard host" -msgstr "" - -#: keystone/contrib/federation/controllers.py:333 -msgid "Use a project scoped token when attempting to create a SAML assertion" -msgstr "" +msgid "Project (%s)" +msgstr "Projeto (%s)" -#: keystone/contrib/federation/idp.py:454 #, python-format -msgid "Cannot open certificate %(cert_file)s. Reason: %(reason)s" -msgstr "" - -#: keystone/contrib/federation/idp.py:521 -msgid "Ensure configuration option idp_entity_id is set." -msgstr "" - -#: keystone/contrib/federation/idp.py:524 -msgid "Ensure configuration option idp_sso_endpoint is set." -msgstr "" - -#: keystone/contrib/federation/idp.py:544 -msgid "" -"idp_contact_type must be one of: [technical, other, support, " -"administrative or billing." -msgstr "" - -#: keystone/contrib/federation/utils.py:178 -msgid "Federation token is expired" -msgstr "" - -#: keystone/contrib/federation/utils.py:208 -msgid "" -"Could not find Identity Provider identifier in environment, check " -"[federation] remote_id_attribute for details." -msgstr "" - -#: keystone/contrib/federation/utils.py:213 -msgid "" -"Incoming identity provider identifier not included among the accepted " -"identifiers." -msgstr "" +msgid "Project is disabled: %s" +msgstr "O projeto está desativado: %s" -#: keystone/contrib/federation/utils.py:501 -#, python-format -msgid "User type %s not supported" -msgstr "" +msgid "Request Token does not have an authorizing user id" +msgstr "Token de Requisição não possui um ID de usuário autorizado" -#: keystone/contrib/federation/utils.py:537 #, python-format msgid "" -"Invalid rule: %(identity_value)s. Both 'groups' and 'domain' keywords " -"must be specified." -msgstr "" - -#: keystone/contrib/federation/utils.py:753 -#, python-format -msgid "Identity Provider %(idp)s is disabled" -msgstr "" - -#: keystone/contrib/federation/utils.py:761 -#, python-format -msgid "Service Provider %(sp)s is disabled" -msgstr "" - -#: keystone/contrib/oauth1/controllers.py:99 -msgid "Cannot change consumer secret" -msgstr "Não é possível alterar segredo do consumidor" - -#: keystone/contrib/oauth1/controllers.py:131 -msgid "Cannot list request tokens with a token issued via delegation." -msgstr "" - -#: keystone/contrib/oauth1/controllers.py:192 -#: keystone/contrib/oauth1/backends/sql.py:270 -msgid "User IDs do not match" -msgstr "ID de usuário não confere" - -#: keystone/contrib/oauth1/controllers.py:199 -msgid "Could not find role" -msgstr "Não é possível encontrar role" - -#: keystone/contrib/oauth1/controllers.py:248 -msgid "Invalid signature" +"Request attribute %(attribute)s must be less than or equal to %(size)i. The " +"server could not comply with the request because the attribute size is " +"invalid (too large). The client is assumed to be in error." msgstr "" +"Atributo de requisição %(attribute)s deve ser menor ou igual a %(size)i. O " +"servidor não pôde atender a requisição porque o tamanho do atributo é " +"inválido (muito grande). Assume-se que o cliente está em erro." -#: keystone/contrib/oauth1/controllers.py:299 -#: keystone/contrib/oauth1/controllers.py:377 msgid "Request token is expired" msgstr "Token de requisição expirou" -#: keystone/contrib/oauth1/controllers.py:313 -msgid "There should not be any non-oauth parameters" -msgstr "Não deve haver nenhum parâmetro não oauth" - -#: keystone/contrib/oauth1/controllers.py:317 -msgid "provided consumer key does not match stored consumer key" -msgstr "" -"Chave de consumidor fornecida não confere com a chave de consumidor " -"armazenada" - -#: keystone/contrib/oauth1/controllers.py:321 -msgid "provided verifier does not match stored verifier" -msgstr "Verificador fornecido não confere com o verificador armazenado" - -#: keystone/contrib/oauth1/controllers.py:325 -msgid "provided request key does not match stored request key" -msgstr "" -"Chave de requisição do provedor não confere com a chave de requisição " -"armazenada" - -#: keystone/contrib/oauth1/controllers.py:329 -msgid "Request Token does not have an authorizing user id" -msgstr "Token de Requisição não possui um ID de usuário autorizado" - -#: keystone/contrib/oauth1/controllers.py:366 -msgid "Cannot authorize a request token with a token issued via delegation." -msgstr "" - -#: keystone/contrib/oauth1/controllers.py:396 -msgid "authorizing user does not have role required" -msgstr "Usuário autorizado não possui o role necessário" - -#: keystone/contrib/oauth1/controllers.py:409 -msgid "User is not a member of the requested project" -msgstr "Usuário não é um membro do projeto requisitado" - -#: keystone/contrib/oauth1/backends/sql.py:91 -msgid "Consumer not found" -msgstr "Consumidor não encontrado" - -#: keystone/contrib/oauth1/backends/sql.py:186 msgid "Request token not found" msgstr "Token de requisição não encontrado" -#: keystone/contrib/oauth1/backends/sql.py:250 -msgid "Access token not found" -msgstr "Token de acesso não encontrado" - -#: keystone/contrib/revoke/controllers.py:33 -#, python-format -msgid "invalid date format %s" -msgstr "" - -#: keystone/contrib/revoke/core.py:150 -msgid "" -"The revoke call must not have both domain_id and project_id. This is a " -"bug in the Keystone server. The current request is aborted." -msgstr "" - -#: keystone/contrib/revoke/core.py:218 keystone/token/provider.py:207 -#: keystone/token/provider.py:230 keystone/token/provider.py:296 -#: keystone/token/provider.py:303 -msgid "Failed to validate token" -msgstr "Falha ao validar token" - -#: keystone/identity/controllers.py:72 -msgid "Enabled field must be a boolean" -msgstr "Campo habilitado precisa ser um booleano" - -#: keystone/identity/controllers.py:98 -msgid "Enabled field should be a boolean" -msgstr "Campo habilitado deve ser um booleano" - -#: keystone/identity/core.py:112 -#, python-format -msgid "Database at /domains/%s/config" -msgstr "" - -#: keystone/identity/core.py:287 keystone/identity/backends/ldap.py:59 -#: keystone/identity/backends/ldap.py:61 keystone/identity/backends/ldap.py:67 -#: keystone/identity/backends/ldap.py:69 keystone/identity/backends/sql.py:104 -#: keystone/identity/backends/sql.py:106 -msgid "Invalid user / password" -msgstr "" - -#: keystone/identity/core.py:693 -#, python-format -msgid "User is disabled: %s" -msgstr "O usuário está desativado: %s" - -#: keystone/identity/core.py:735 -msgid "Cannot change user ID" -msgstr "" - -#: keystone/identity/backends/ldap.py:99 -msgid "Cannot change user name" -msgstr "" - -#: keystone/identity/backends/ldap.py:188 keystone/identity/backends/sql.py:188 -#: keystone/identity/backends/sql.py:206 #, python-format -msgid "User '%(user_id)s' not found in group '%(group_id)s'" -msgstr "" - -#: keystone/identity/backends/ldap.py:339 -#, python-format -msgid "User %(user_id)s is already a member of group %(group_id)s" -msgstr "Usuário %(user_id)s já é membro do grupo %(group_id)s" - -#: keystone/models/token_model.py:61 -msgid "Found invalid token: scoped to both project and domain." -msgstr "" +msgid "Role %s not found" +msgstr "Role %s não localizada" -#: keystone/openstack/common/versionutils.py:108 -#, python-format -msgid "" -"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " -"may be removed in %(remove_in)s." -msgstr "" -"%(what)s está deprecado desde %(as_of)s em favor de %(in_favor_of)s e " -"pode ser removido em %(remove_in)s." +msgid "Scoping to both domain and project is not allowed" +msgstr "A definição de escopo para o domínio e o projeto não é permitida" -#: keystone/openstack/common/versionutils.py:112 -#, python-format -msgid "" -"%(what)s is deprecated as of %(as_of)s and may be removed in " -"%(remove_in)s. It will not be superseded." -msgstr "" -"%(what)s está deprecado desde %(as_of)s e pode ser removido em " -"%(remove_in)s. Ele não será substituído." +msgid "Scoping to both domain and trust is not allowed" +msgstr "A definição de escopo para o domínio e a trust não é permitida" -#: keystone/openstack/common/versionutils.py:116 -#, python-format -msgid "%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s." -msgstr "" +msgid "Scoping to both project and trust is not allowed" +msgstr "A definição de escopo para o projeto e a trust não é permitida" -#: keystone/openstack/common/versionutils.py:119 -#, python-format -msgid "%(what)s is deprecated as of %(as_of)s. It will not be superseded." -msgstr "" +msgid "Specify a domain or project, not both" +msgstr "Especifique um domínio ou projeto, não ambos" -#: keystone/openstack/common/versionutils.py:241 -#, python-format -msgid "Deprecated: %s" -msgstr "Deprecado: %s" +msgid "Specify a user or group, not both" +msgstr "Epecifique um usuário ou grupo, não ambos" -#: keystone/openstack/common/versionutils.py:259 #, python-format -msgid "Fatal call to deprecated config: %(msg)s" -msgstr "Chamada fatal à configuração deprecada: %(msg)s" - -#: keystone/resource/controllers.py:231 -msgid "" -"Cannot use parents_as_list and parents_as_ids query params at the same " -"time." -msgstr "" - -#: keystone/resource/controllers.py:237 msgid "" -"Cannot use subtree_as_list and subtree_as_ids query params at the same " -"time." -msgstr "" - -#: keystone/resource/core.py:80 -#, python-format -msgid "max hierarchy depth reached for %s branch." -msgstr "" - -#: keystone/resource/core.py:97 -msgid "cannot create a project within a different domain than its parents." +"String length exceeded.The length of string '%(string)s' exceeded the limit " +"of column %(type)s(CHAR(%(length)d))." msgstr "" +"Comprimento de string excedido. O comprimento de string '%(string)s' excedeu " +"o limite da coluna %(type)s(CHAR(%(length)d))." -#: keystone/resource/core.py:101 -#, python-format -msgid "cannot create a project in a branch containing a disabled project: %s" -msgstr "" - -#: keystone/resource/core.py:123 -#, python-format -msgid "Domain is disabled: %s" -msgstr "O domínio está desativado: %s" - -#: keystone/resource/core.py:141 -#, python-format -msgid "Domain cannot be named %s" -msgstr "" - -#: keystone/resource/core.py:144 -#, python-format -msgid "Domain cannot have ID %s" -msgstr "" - -#: keystone/resource/core.py:156 -#, python-format -msgid "Project is disabled: %s" -msgstr "O projeto está desativado: %s" - -#: keystone/resource/core.py:176 -#, python-format -msgid "cannot enable project %s since it has disabled parents" -msgstr "" - -#: keystone/resource/core.py:184 -#, python-format -msgid "cannot disable project %s since its subtree contains enabled projects" -msgstr "" - -#: keystone/resource/core.py:195 -msgid "Update of `parent_id` is not allowed." -msgstr "" - -#: keystone/resource/core.py:222 -#, python-format -msgid "cannot delete the project %s since it is not a leaf in the hierarchy." -msgstr "" - -#: keystone/resource/core.py:376 -msgid "Multiple domains are not supported" -msgstr "" - -#: keystone/resource/core.py:429 -msgid "delete the default domain" -msgstr "" - -#: keystone/resource/core.py:440 -msgid "cannot delete a domain that is enabled, please disable it first." -msgstr "" - -#: keystone/resource/core.py:841 -msgid "No options specified" -msgstr "Nenhuma opção especificada" +msgid "The action you have requested has not been implemented." +msgstr "A ação que você solicitou não foi implementada." -#: keystone/resource/core.py:847 -#, python-format -msgid "" -"The value of group %(group)s specified in the config should be a " -"dictionary of options" -msgstr "" +msgid "The request you have made requires authentication." +msgstr "A requisição que você fez requer autenticação." -#: keystone/resource/core.py:871 -#, python-format -msgid "" -"Option %(option)s found with no group specified while checking domain " -"configuration request" -msgstr "" +msgid "The resource could not be found." +msgstr "O recurso não pôde ser localizado." -#: keystone/resource/core.py:878 -#, python-format -msgid "Group %(group)s is not supported for domain specific configurations" -msgstr "" +msgid "There should not be any non-oauth parameters" +msgstr "Não deve haver nenhum parâmetro não oauth" -#: keystone/resource/core.py:885 -#, python-format msgid "" -"Option %(option)s in group %(group)s is not supported for domain specific" -" configurations" -msgstr "" - -#: keystone/resource/core.py:938 -msgid "An unexpected error occurred when retrieving domain configs" -msgstr "" - -#: keystone/resource/core.py:1013 keystone/resource/core.py:1097 -#: keystone/resource/core.py:1167 keystone/resource/config_backends/sql.py:70 -#, python-format -msgid "option %(option)s in group %(group)s" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client is " +"assumed to be in error." msgstr "" +"A data não está no formato especificado. O servidor não pôde realizar a " +"requisição pois ela está mal formada ou incorreta. Assume-se que o cliente " +"está com erro." -#: keystone/resource/core.py:1016 keystone/resource/core.py:1102 -#: keystone/resource/core.py:1163 -#, python-format -msgid "group %(group)s" -msgstr "" +msgid "Token belongs to another user" +msgstr "O token pertence à outro usuário" -#: keystone/resource/core.py:1018 -msgid "any options" -msgstr "" +msgid "Token does not belong to specified tenant." +msgstr "O token não pertence ao tenant especificado." -#: keystone/resource/core.py:1062 -#, python-format -msgid "" -"Trying to update option %(option)s in group %(group)s, so that, and only " -"that, option must be specified in the config" -msgstr "" +msgid "Trustee has no delegated roles." +msgstr "Fiador não possui roles delegados." -#: keystone/resource/core.py:1067 -#, python-format -msgid "" -"Trying to update group %(group)s, so that, and only that, group must be " -"specified in the config" -msgstr "" +msgid "Trustor is disabled." +msgstr "O fiador está desativado." -#: keystone/resource/core.py:1076 #, python-format -msgid "" -"request to update group %(group)s, but config provided contains group " -"%(group_other)s instead" -msgstr "" +msgid "Unable to locate domain config directory: %s" +msgstr "Não é possível localizar diretório de configuração de domínio: %s" -#: keystone/resource/core.py:1083 #, python-format -msgid "" -"Trying to update option %(option)s in group %(group)s, but config " -"provided contains option %(option_other)s instead" -msgstr "" - -#: keystone/resource/backends/ldap.py:151 -#: keystone/resource/backends/ldap.py:159 -#: keystone/resource/backends/ldap.py:163 -msgid "Domains are read-only against LDAP" -msgstr "" +msgid "Unable to lookup user %s" +msgstr "Não é possível consultar o usuário %s" -#: keystone/server/eventlet.py:77 -msgid "" -"Running keystone via eventlet is deprecated as of Kilo in favor of " -"running in a WSGI server (e.g. mod_wsgi). Support for keystone under " -"eventlet will be removed in the \"M\"-Release." -msgstr "" +msgid "Unable to sign token." +msgstr "Não é possível assinar o token." -#: keystone/server/eventlet.py:90 -#, python-format -msgid "Failed to start the %(name)s server" -msgstr "" +msgid "Unknown Target" +msgstr "Alvo Desconhecido" -#: keystone/token/controllers.py:391 #, python-format msgid "User %(u_id)s is unauthorized for tenant %(t_id)s" msgstr "Usuário %(u_id)s não está autorizado para o tenant %(t_id)s" -#: keystone/token/controllers.py:410 keystone/token/controllers.py:413 -msgid "Token does not belong to specified tenant." -msgstr "O token não pertence ao tenant especificado." - -#: keystone/token/persistence/backends/kvs.py:133 #, python-format -msgid "Unknown token version %s" -msgstr "" +msgid "User %(user_id)s has no access to domain %(domain_id)s" +msgstr "O usuário %(user_id)s não tem acesso ao domínio %(domain_id)s" -#: keystone/token/providers/common.py:250 -#: keystone/token/providers/common.py:355 #, python-format msgid "User %(user_id)s has no access to project %(project_id)s" msgstr "O usuário %(user_id)s não tem acesso ao projeto %(project_id)s" -#: keystone/token/providers/common.py:255 -#: keystone/token/providers/common.py:360 #, python-format -msgid "User %(user_id)s has no access to domain %(domain_id)s" -msgstr "O usuário %(user_id)s não tem acesso ao domínio %(domain_id)s" - -#: keystone/token/providers/common.py:282 -msgid "Trustor is disabled." -msgstr "O fiador está desativado." +msgid "User %(user_id)s is already a member of group %(group_id)s" +msgstr "Usuário %(user_id)s já é membro do grupo %(group_id)s" -#: keystone/token/providers/common.py:346 -msgid "Trustee has no delegated roles." -msgstr "Fiador não possui roles delegados." +msgid "User IDs do not match" +msgstr "ID de usuário não confere" -#: keystone/token/providers/common.py:407 #, python-format -msgid "Invalid audit info data type: %(data)s (%(type)s)" -msgstr "" +msgid "User is disabled: %s" +msgstr "O usuário está desativado: %s" + +msgid "User is not a member of the requested project" +msgstr "Usuário não é um membro do projeto requisitado" -#: keystone/token/providers/common.py:435 msgid "User is not a trustee." msgstr "Usuário não é confiável." -#: keystone/token/providers/common.py:579 -msgid "" -"Attempting to use OS-FEDERATION token with V2 Identity Service, use V3 " -"Authentication" -msgstr "" - -#: keystone/token/providers/common.py:597 -msgid "Domain scoped token is not supported" -msgstr "O token de escopo de domínio não é suportado" - -#: keystone/token/providers/pki.py:48 keystone/token/providers/pkiz.py:30 -msgid "Unable to sign token." -msgstr "Não é possível assinar o token." +msgid "User not found" +msgstr "Usuário não localizado" -#: keystone/token/providers/fernet/core.py:215 -msgid "" -"This is not a v2.0 Fernet token. Use v3 for trust, domain, or federated " -"tokens." -msgstr "" +msgid "You are not authorized to perform the requested action." +msgstr "Você não está autorizado à realizar a ação solicitada." -#: keystone/token/providers/fernet/token_formatters.py:189 -#, python-format -msgid "This is not a recognized Fernet payload version: %s" -msgstr "" +msgid "authorizing user does not have role required" +msgstr "Usuário autorizado não possui o role necessário" -#: keystone/trust/controllers.py:148 -msgid "Redelegation allowed for delegated by trust only" +msgid "provided consumer key does not match stored consumer key" msgstr "" +"Chave de consumidor fornecida não confere com a chave de consumidor " +"armazenada" -#: keystone/trust/controllers.py:181 -msgid "The authenticated user should match the trustor." +msgid "provided request key does not match stored request key" msgstr "" +"Chave de requisição do provedor não confere com a chave de requisição " +"armazenada" -#: keystone/trust/controllers.py:186 -msgid "At least one role should be specified." -msgstr "" +msgid "provided verifier does not match stored verifier" +msgstr "Verificador fornecido não confere com o verificador armazenado" -#: keystone/trust/core.py:57 -#, python-format -msgid "" -"Remaining redelegation depth of %(redelegation_depth)d out of allowed " -"range of [0..%(max_count)d]" -msgstr "" +msgid "region not type dogpile.cache.CacheRegion" +msgstr "região não é do tipo dogpile.cache.CacheRegion" -#: keystone/trust/core.py:66 #, python-format -msgid "" -"Field \"remaining_uses\" is set to %(value)s while it must not be set in " -"order to redelegate a trust" -msgstr "" - -#: keystone/trust/core.py:77 -msgid "Requested expiration time is more than redelegated trust can provide" -msgstr "" - -#: keystone/trust/core.py:87 -msgid "Some of requested roles are not in redelegated trust" -msgstr "" - -#: keystone/trust/core.py:116 -msgid "One of the trust agents is disabled or deleted" -msgstr "" - -#: keystone/trust/core.py:135 -msgid "remaining_uses must be a positive integer or null." -msgstr "" +msgid "tls_cacertdir %s not found or is not a directory" +msgstr "tls_cacertdir %s não encontrado ou não é um diretório" -#: keystone/trust/core.py:141 #, python-format -msgid "" -"Requested redelegation depth of %(requested_count)d is greater than " -"allowed %(max_count)d" -msgstr "" - -#: keystone/trust/core.py:147 -msgid "remaining_uses must not be set if redelegation is allowed" -msgstr "" - -#: keystone/trust/core.py:157 -msgid "" -"Modifying \"redelegation_count\" upon redelegation is forbidden. Omitting" -" this parameter is advised." -msgstr "" - +msgid "tls_cacertfile %s not found or is not a file" +msgstr "tls_cacertfile %s não encontrada ou não é um arquivo" diff --git a/keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po index f8d060b3..4ec0cb4b 100644 --- a/keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,20 +7,20 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Russian (http://www.transifex.com/projects/p/keystone/" -"language/ru/)\n" +"Language-Team: Russian (http://www.transifex.com/openstack/keystone/language/" +"ru/)\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Generated-By: Babel 2.0\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "Не удается открыть файл шаблона %s" diff --git a/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..7d486e84 --- /dev/null +++ b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,24 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-08-04 13:49+0000\n" +"Last-Translator: İşbaran Akçayır <isbaran@gmail.com>\n" +"Language-Team: Turkish (Turkey) (http://www.transifex.com/openstack/keystone/" +"language/tr_TR/)\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.0\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#, python-format +msgid "Unable to open template file %s" +msgstr "%s şablon dosyası açılamıyor" diff --git a/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..cded46bb --- /dev/null +++ b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,163 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-08-04 13:50+0000\n" +"Last-Translator: İşbaran Akçayır <isbaran@gmail.com>\n" +"Language-Team: Turkish (Turkey) (http://www.transifex.com/openstack/keystone/" +"language/tr_TR/)\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.0\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgid "Cannot retrieve Authorization headers" +msgstr "Yetkilendirme başlıkları alınamıyor" + +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" +"Proje sıra düzeninde çember başvuru ya da tekrar eden girdi bulundu - " +"%(project_id)s." + +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "" +"Bölge ağacında çember başvuru ya da tekrar eden girdi bulundu - " +"%(region_id)s." + +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" +"Proje sıra düzeninde çember başvuru ya da tekrar eden girdi bulundu - " +"%(project_id)s." + +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "%(to_exec)s komutu %(retcode)s ile çıktı- %(output)s" + +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "%(host)s:%(port)s adresine bağlanılamadı" + +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" +"[fernet_tokents] key_repository mevcut değil ya da Keystone erişmek için " +"yeterli izine sahip değil: %s" + +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format <host>:<port> and that a debugger processes is listening on " +"that port." +msgstr "" +"Hata ayıklama ortamının ayarlanmasında hata. --debug-url seçeneğinin " +"<istemci>:<bağlantı noktası> biçimine sahip olduğunu ve bu bağlantı " +"noktasında hata ayıklama sürecinin dinlediğini doğrulayın." + +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "Teyit imzalanırken hata, sebep: %(reason)s" + +msgid "Failed to construct notifier" +msgstr "Bildirici inşa etme başarısız" + +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" +"[fernet_tokens] key_repository oluşturulamıyor: ya zaten mevcut ya da " +"oluşturmak için yeterli izniniz yok" + +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "%(file_path)r dosyası silinemedi: %(error)s" + +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "%(action)s %(event_type)s bildirimi gönderilemedi" + +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "%(res_id)s %(event_type)s bildirimi gönderilemedi" + +msgid "Failed to validate token" +msgstr "Jeton doğrulama başarısız" + +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "Kusurlu bitiş noktası %(url)s - bilinmeyen anahtar %(keyerror)s" + +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "" +"Kusurlu bitiş noktası %s - tamamlanmamış biçim (bir tür bildiriciniz eksik " +"olabilir mi ?)" + +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" +"Kusurlu bitiş noktası '%(url)s'. Karakter dizisi yer değiştirme sırasında şu " +"tür hatası oluştu: %(typeerror)s" + +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "Kusurlu bitiş noktası - %(url)r bir karakter dizisi değil" + +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" +"Arka uçtan feshetme listesi yüklemedeki hata sebebiyle fesih listesi yeniden " +"ilklendiriliyor. `list` beklendi `%(type)s` alındı. Eski fesih listesi " +"verisi: %(list)r" + +msgid "Server error" +msgstr "Sunucu hatası" + +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \"<argname>:<value>\". " +"Skipping unknown format: %s" +msgstr "" +"Zula yapılandırma anahtarı inşa edilemiyor. Beklenen biçim \"<değişken ismi>:" +"<değer>\". Bilinmeyen biçim atlanıyor: %s" + +#, python-format +msgid "Unable to convert Keystone user or group ID. Error: %s" +msgstr "Keystone kullanıcı veya grup kimliği dönüştürülemiyor. Hata: %s" + +msgid "Unable to sign token" +msgstr "Jeton imzalanamıyor" + +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "Jeton sona erme belirlemede beklenmeyen hata veya kusurlu jeton: %s" + +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" +"Alan yapılandırması yanıtında beklenmedik sonuçlar - %(count)s yanıt, ilk " +"seçenek %(option)s, beklenen seçenek %(expected)s" diff --git a/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-info.po new file mode 100644 index 00000000..5b6da88f --- /dev/null +++ b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-info.po @@ -0,0 +1,130 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-08-04 13:49+0000\n" +"Last-Translator: İşbaran Akçayır <isbaran@gmail.com>\n" +"Language-Team: Turkish (Turkey) (http://www.transifex.com/openstack/keystone/" +"language/tr_TR/)\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.0\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." +msgstr "" +"\"expires_at\" çatışan değerlere sahip %(existing)s ve %(new)s. İlk değer " +"kullanılacak." + +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "'%(proxy)s' vekili KVS %(name)s'e ekleniyor." + +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "Bilinmeyen bağ doğrulanamıyor: {%(bind_type)s: %(identifier)s}" + +#, python-format +msgid "Created a new key: %s" +msgstr "Yeni bir anahtar oluşturuldu: %s" + +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "Varsayılan rol %s oluşturuluyor çünkü mevcut değil." + +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "Varsayılan rol %s oluşturma başarısız çünkü zaten oluşturulmuş" + +#, python-format +msgid "Current primary key is: %s" +msgstr "Mevcut birincil anahtar: %s" + +#, python-format +msgid "" +"Fernet token created with length of %d characters, which exceeds 255 " +"characters" +msgstr "" +"Fernet jetonu %d karakter uzunluğunda oluşturuldu, bu 255 karakteri geçiyor" + +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "KVS bölgesi %s key_mangler kapalı." + +msgid "Kerberos bind authentication successful" +msgstr "Kerberos bağ kimlik doğrulama başarılı" + +msgid "Kerberos credentials do not match those in bind" +msgstr "Kerberos kimlik bilgileri bağda olanlarla eşleşmiyor" + +msgid "Kerberos credentials required and not present" +msgstr "Kerberos kimlik bilgileri gerekli ve mevcut değil" + +msgid "Key repository is already initialized; aborting." +msgstr "Anahtar deposu zaten ilklendirilmiş; iptal ediliyor." + +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "Adlandırılmış bağlama kipi %s bağlama bilgisinde değil" + +#, python-format +msgid "Next primary key will be: %s" +msgstr "Sonraki birincil anahtar şu olacak: %s" + +msgid "No bind information present in token" +msgstr "Jetonda bağlama bilgisi yok" + +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "Anahtar 0 birincil anahtarlığa yükseltildi: %s" + +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" +"Şu bildirim alındı: servis %(service)s, kaynak_türü: %(resource_type)s, " +"işlem %(operation)s faydalı yük %(payload)s" + +#, python-format +msgid "Running command - %s" +msgstr "Komut çalıştırılıyor - %s" + +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "%(host)s:%(port)s üzerinde %(arg0)s başlatılıyor" + +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "Anahtar dönüşümü %(count)s anahtar dosyasıyla başlatılıyor: %(list)s" + +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "Toplam süresi dolmuş jetonlar kaldırıldı: %d" + +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "%(func)s KVS bölgesi %(name)s key_mangler olarak kullanılıyor" + +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" +"Varsayılan dogpile sha1_mangle_key KVS bölgesi %s key_mangler olarak " +"kullanılıyor" + +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" +"[fernet_tokens] key_repository var gibi görünmüyor; oluşturmaya çalışılıyor" diff --git a/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-warning.po b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-warning.po new file mode 100644 index 00000000..1fda963e --- /dev/null +++ b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone-log-warning.po @@ -0,0 +1,249 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-08-16 06:06+0000\n" +"PO-Revision-Date: 2015-08-11 08:29+0000\n" +"Last-Translator: openstackjenkins <jenkins@openstack.org>\n" +"Language-Team: Turkish (Turkey) (http://www.transifex.com/openstack/keystone/" +"language/tr_TR/)\n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.0\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#, python-format +msgid "%s is not a dogpile.proxy.ProxyBackend" +msgstr "%s dogpile.proxy.ProxyBackend değil" + +msgid "Auth context already exists in the request environment" +msgstr "Yetki içeriği istenen ortamda zaten var" + +#, python-format +msgid "Authorization failed. %(exception)s from %(remote_addr)s" +msgstr "Yetkilendirme başarısız. %(remote_addr)s den %(exception)s" + +#, python-format +msgid "" +"Endpoint %(endpoint_id)s referenced in association for policy %(policy_id)s " +"not found." +msgstr "" +"%(policy_id)s ile ilişkisi için başvurulan bitiş noktası %(endpoint_id)s " +"bulunamadı." + +msgid "Failed to invoke ``openssl version``, assuming is v1.0 or newer" +msgstr "" +"``openssl version`` çalıştırılamadı, v1.0 ya da daha yeni olarak varsayılıyor" + +#, python-format +msgid "" +"Found multiple domains being mapped to a driver that does not support that " +"(e.g. LDAP) - Domain ID: %(domain)s, Default Driver: %(driver)s" +msgstr "" +"Bunu desteklemeyen bir sürücüye eşleştirilen birden fazla alan bulundu (örn. " +"LDAP) - Alan ID: %(domain)s, Varsayılan Sürücü: %(driver)s" + +#, python-format +msgid "" +"Found what looks like an incorrectly constructed config option substitution " +"reference - domain: %(domain)s, group: %(group)s, option: %(option)s, value: " +"%(value)s." +msgstr "" +"Düzgün inşa edilmemiş yapılandırma seçeneği yer değiştirme referansına " +"benzeyen bir şey bulundu - alan: %(domain)s, grup: %(group)s, seçenek: " +"%(option)s, değer: %(value)s." + +#, python-format +msgid "" +"Found what looks like an unmatched config option substitution reference - " +"domain: %(domain)s, group: %(group)s, option: %(option)s, value: %(value)s. " +"Perhaps the config option to which it refers has yet to be added?" +msgstr "" +"Eşleşmemiş yapılandırma seçeneği yer değiştirme referansı gibi görünen bir " +"şey bulundu - alan: %(domain)s, grup: %(group)s, seçenek: %(option)s, değer: " +"%(value)s. Belki başvurduğu yapılandırma seçeneği henüz eklenmemiştir?" + +#, python-format +msgid "Ignoring file (%s) while scanning domain config directory" +msgstr "Alan yapılandırma dizini taranırken dosya (%s) atlanıyor" + +msgid "Ignoring user name" +msgstr "Kullanıcı adı atlanıyor" + +#, python-format +msgid "" +"Invalid additional attribute mapping: \"%s\". Format must be " +"<ldap_attribute>:<keystone_attribute>" +msgstr "" +"Geçersiz ek öznitelik eşleştirmesi: \"%s\". Biçim <ldap_attribute>:" +"<keystone_attribute> olmalı" + +#, python-format +msgid "Invalid domain name (%s) found in config file name" +msgstr "Yapılandırma dosyası isminde geçersiz alan adı (%s) bulundu" + +msgid "" +"It is recommended to only use the base key-value-store implementation for " +"the token driver for testing purposes. Please use 'memcache' or 'sql' " +"instead." +msgstr "" +"Jeton sürücüsü için temel anahtar-değer-depolama uygulamasının yalnızca test " +"amaçlı kullanımı önerilir. Lütfen 'memcache' ya da 'sql' kullanın." + +#, python-format +msgid "KVS lock released (timeout reached) for: %s" +msgstr "KVS kilidi kaldırıldı (zaman aşımına uğradı): %s" + +msgid "" +"LDAP Server does not support paging. Disable paging in keystone.conf to " +"avoid this message." +msgstr "" +"LDAP Sunucu sayfalamayı desteklemiyor. Bu iletiyi almamak için sayfalamayı " +"keystone.conf'da kapatın." + +msgid "No domain information specified as part of list request" +msgstr "Listeleme isteğinin parçası olarak alan bilgisi belirtilmedi" + +#, python-format +msgid "" +"Policy %(policy_id)s referenced in association for endpoint %(endpoint_id)s " +"not found." +msgstr "" +"%(endpoint_id)s bitiş noktası için ilişkisi için başvurulan %(policy_id)s " +"ilkesi bulunamadı." + +msgid "RBAC: Bypassing authorization" +msgstr "RBAC: Yetkilendirme baypas ediliyor" + +msgid "RBAC: Invalid token" +msgstr "RBAC: Geçersiz jeton" + +msgid "RBAC: Invalid user data in token" +msgstr "RBAC: Jetonda geçersiz kullanıcı verisi" + +#, python-format +msgid "" +"Removing `%s` from revocation list due to invalid expires data in revocation " +"list." +msgstr "" +"feshetme listesindeki geçersiz sona erme tarihi verisi sebebiyle `%s` " +"feshetme listesinden kaldırılıyor." + +#, python-format +msgid "" +"TTL index already exists on db collection <%(c_name)s>, remove index <" +"%(indx_name)s> first to make updated mongo_ttl_seconds value to be effective" +msgstr "" +"TTL indisi zaten <%(c_name)s> db koleksiyonunda mevcut, güncellenmiş " +"mongo_ttl_seconds değerini etkin yapmak için önce <%(indx_name)s> indisini " +"kaldırın" + +#, python-format +msgid "Token `%s` is expired, not adding to the revocation list." +msgstr "`%s` jetonunun süresi dolmuş, feshetme listesine eklenmiyor." + +#, python-format +msgid "Truncating user password to %d characters." +msgstr "Kullanıcı parolası %d karaktere kırpılıyor." + +#, python-format +msgid "Unable to add user %(user)s to %(tenant)s." +msgstr "Kullanıcı %(user)s %(tenant)s'e eklenemiyor." + +#, python-format +msgid "" +"Unable to change the ownership of [fernet_tokens] key_repository without a " +"keystone user ID and keystone group ID both being provided: %s" +msgstr "" +"Hem keystone kullanıcı kimliği hem keystone grup kimliği verilmeden " +"[fernet_tokens] key_repository sahipliği değiştirilemiyor: %s" + +#, python-format +msgid "" +"Unable to change the ownership of the new key without a keystone user ID and " +"keystone group ID both being provided: %s" +msgstr "" +"Hem keystone kullanıcı kimliği hem keystone grup kimliği verilmeden yeni " +"anahtarın sahipliği değiştirilemiyor: %s" + +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "Alan yapılandırma dizini bulunamadı: %s" + +#, python-format +msgid "Unable to remove user %(user)s from %(tenant)s." +msgstr "Kullanıcı %(user)s %(tenant)s'den çıkarılamadı." + +#, python-format +msgid "" +"Unsupported policy association found - Policy %(policy_id)s, Endpoint " +"%(endpoint_id)s, Service %(service_id)s, Region %(region_id)s, " +msgstr "" +"Desteklenmeyen ilke ilişkilendirmesi bulundu - İlke %(policy_id)s, Bitiş " +"noktası %(endpoint_id)s, Servis %(service_id)s, Bölge %(region_id)s, " + +#, python-format +msgid "" +"User %(user_id)s doesn't have access to default project %(project_id)s. The " +"token will be unscoped rather than scoped to the project." +msgstr "" +"%(user_id)s kullanıcısı varsayılan proje %(project_id)s erişimine sahip " +"değil. Jeton projeye kapsamsız olacak, kapsamlı değil." + +#, python-format +msgid "" +"User %(user_id)s's default project %(project_id)s is disabled. The token " +"will be unscoped rather than scoped to the project." +msgstr "" +"%(user_id)s kullanıcısının varsayılan projesi %(project_id)s kapalı. Jeton " +"projeye kapsamsız olacak, kapsamlı değil." + +#, python-format +msgid "" +"User %(user_id)s's default project %(project_id)s not found. The token will " +"be unscoped rather than scoped to the project." +msgstr "" +"%(user_id)s kullanıcısının varsayılan projesi %(project_id)s bulunamadı. " +"Jeton projeye kapsamsız olacak, kapsamlı değil." + +#, python-format +msgid "" +"When deleting entries for %(search_base)s, could not delete nonexistent " +"entries %(entries)s%(dots)s" +msgstr "" +"%(search_base)s için girdiler silinirken, mevcut olmayan girdiler %(entries)s" +"%(dots)s silinemedi" + +#, python-format +msgid "[fernet_tokens] key_repository is world readable: %s" +msgstr "[fernet_tokens] key_repository herkesçe okunabilir: %s" + +msgid "" +"[fernet_tokens] max_active_keys must be at least 1 to maintain a primary key." +msgstr "" +"[fernet_tokens] max_active_keys bir birincil anahtarı korumak için en az 1 " +"olmalı." + +#, python-format +msgid "" +"`token_api.%s` is deprecated as of Juno in favor of utilizing methods on " +"`token_provider_api` and may be removed in Kilo." +msgstr "" +"`token_provider_api` üzerindeki yöntemlerden faydalanmak için `token_api.%s` " +"Juno'dan sonra tercih edilmeyecek ve Kilo'da kaldırılabilir." + +msgid "keystone-manage pki_setup is not recommended for production use." +msgstr "keystone-manage pki_setup üretimde kullanmak için tavsiye edilmez." + +msgid "keystone-manage ssl_setup is not recommended for production use." +msgstr "keystone-manage ssl_setup üretimde kullanmak için tavsiye edilmez." + +msgid "missing exception kwargs (programmer error)" +msgstr "istisna kwargs eksik (programcı hatası)" diff --git a/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone.po b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone.po new file mode 100644 index 00000000..6b962cfd --- /dev/null +++ b/keystone-moon/keystone/locale/tr_TR/LC_MESSAGES/keystone.po @@ -0,0 +1,1288 @@ +# Turkish (Turkey) translations for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Alper Çiftçi <alprciftci@gmail.com>, 2015 +# Andreas Jaeger <jaegerandi@gmail.com>, 2015 +# catborise <muhammetalisag@gmail.com>, 2013 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-08-16 06:06+0000\n" +"PO-Revision-Date: 2015-08-15 18:05+0000\n" +"Last-Translator: openstackjenkins <jenkins@openstack.org>\n" +"Language-Team: Turkish (Turkey) (http://www.transifex.com/openstack/keystone/" +"language/tr_TR/)\n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.0\n" + +#, python-format +msgid "%(detail)s" +msgstr "%(detail)s" + +#, python-format +msgid "" +"%(event)s is not a valid notification event, must be one of: %(actions)s" +msgstr "" +"%(event)s geçerli bir bilgilendirme olayı değil, şunlardan biri olmalı: " +"%(actions)s" + +#, python-format +msgid "%(host)s is not a trusted dashboard host" +msgstr "%(host)s güvenilir bir gösterge paneli istemcisi değil" + +#, python-format +msgid "%(message)s %(amendment)s" +msgstr "%(message)s %(amendment)s" + +#, python-format +msgid "" +"%(mod_name)s doesn't provide database migrations. The migration repository " +"path at %(path)s doesn't exist or isn't a directory." +msgstr "" +"%(mod_name)s veri tabanı göçü sağlamıyor. %(path)s yolundaki göç deposu yolu " +"mevcut değil ya da bir dizin değil." + +#, python-format +msgid "%(property_name)s cannot be less than %(min_length)s characters." +msgstr "%(property_name)s %(min_length)s karakterden az olamaz." + +#, python-format +msgid "%(property_name)s is not a %(display_expected_type)s" +msgstr "%(property_name)s bir %(display_expected_type)s değil" + +#, python-format +msgid "%(property_name)s should not be greater than %(max_length)s characters." +msgstr "%(property_name)s %(max_length)s karakterden büyük olmamalı." + +#, python-format +msgid "%s cannot be empty." +msgstr "%s boş olamaz." + +#, python-format +msgid "%s extension does not exist." +msgstr "%s eklentisi mevcut değil." + +#, python-format +msgid "%s field is required and cannot be empty" +msgstr "%s alanı gerekli ve boş olamaz" + +#, python-format +msgid "%s field(s) cannot be empty" +msgstr "%s alan(lar)ı boş olamaz" + +msgid "(Disable debug mode to suppress these details.)" +msgstr "(Bu detayları gizlemek için hata ayıklama kipini kapatın.)" + +msgid "--all option cannot be mixed with other options" +msgstr "--all seçeneği diğer seçeneklerle birleştirilemez" + +msgid "A project-scoped token is required to produce a service catalog." +msgstr "Servis kataloğu oluşturmak için proje-kapsamlı bir jeton gerekli." + +msgid "Access token is expired" +msgstr "Erişim jetonunun süresi dolmuş" + +msgid "Access token not found" +msgstr "Erişim jetonu bulunamadı" + +msgid "Additional authentications steps required." +msgstr "Ek kimlik doğrulama adımları gerekli." + +msgid "An unexpected error occurred when retrieving domain configs" +msgstr "Alan yapılandırmaları alınırken beklenmedik hata oluştu" + +#, python-format +msgid "An unexpected error occurred when trying to store %s" +msgstr "%s depolanırken beklenmedik bir hata oluştu" + +msgid "An unexpected error prevented the server from fulfilling your request." +msgstr "Beklenmedik bir hata sunucunun isteğinizi tamamlamasını engelledi." + +#, python-format +msgid "" +"An unexpected error prevented the server from fulfilling your request: " +"%(exception)s" +msgstr "" +"Beklenmedik bir hata sunucunun isteğinizi tamamlamasını engelledi: " +"%(exception)s" + +msgid "An unhandled exception has occurred: Could not find metadata." +msgstr "Ele alınmayan istisna oluştu: Metadata bulunamadı." + +msgid "At least one option must be provided" +msgstr "En az bir seçenek sağlanmalıdır" + +msgid "At least one option must be provided, use either --all or --domain-name" +msgstr "En az bir seçenek sağlanmalıdır, ya --all ya da --domain-name kullanın" + +msgid "At least one role should be specified." +msgstr "En az bir kural belirtilmeli." + +msgid "Attempted to authenticate with an unsupported method." +msgstr "Desteklenmeyen yöntem ile doğrulama girişiminde bulunuldu." + +msgid "" +"Attempting to use OS-FEDERATION token with V2 Identity Service, use V3 " +"Authentication" +msgstr "" +"OS-FEDERATION jetonu V2 Kimlik Servisi ile kullanılmaya çalışılıyor, V3 " +"Kimlik Doğrulama kullanın" + +msgid "Authentication plugin error." +msgstr "Kimlik doğrulama eklenti hatası." + +#, python-format +msgid "" +"Backend `%(backend)s` is not a valid memcached backend. Valid backends: " +"%(backend_list)s" +msgstr "" +"Arka uç `%(backend)s` geçerli bir memcached arka ucu değil. Geçerli arka " +"uçlar: %(backend_list)s" + +msgid "Cannot authorize a request token with a token issued via delegation." +msgstr "Vekil ile sağlanan bir jeton ile istek yetkilendirilemez." + +#, python-format +msgid "Cannot change %(option_name)s %(attr)s" +msgstr "%(option_name)s %(attr)s değiştirilemiyor" + +msgid "Cannot change Domain ID" +msgstr "Alan ID'si değiştirilemez" + +msgid "Cannot change consumer secret" +msgstr "Tüketici sırrı değiştirilemez" + +msgid "Cannot change user ID" +msgstr "Kullanıcı ID'si değiştirilemiyor" + +msgid "Cannot change user name" +msgstr "Kullanıcı adı değiştirilemiyor" + +#, python-format +msgid "Cannot create an endpoint with an invalid URL: %(url)s" +msgstr "%(url)s geçersiz URL' si ile bir bitiş noktası yaratılamıyor" + +#, python-format +msgid "Cannot create project with parent: %(project_id)s" +msgstr "Üst proje %(project_id)s ye sahip proje oluşturulamıyor" + +#, python-format +msgid "Cannot duplicate name %s" +msgstr "%s ismi kopyalanamaz" + +msgid "Cannot list request tokens with a token issued via delegation." +msgstr "Vekalet ile sağlanan bir jeton ile istek jetonları listelenemez." + +#, python-format +msgid "Cannot open certificate %(cert_file)s. Reason: %(reason)s" +msgstr "Sertifika %(cert_file)s açılamıyor. Sebep: %(reason)s" + +#, python-format +msgid "Cannot remove role that has not been granted, %s" +msgstr "Verilmemiş rol silinemez, %s" + +msgid "" +"Cannot truncate a driver call without hints list as first parameter after " +"self " +msgstr "" +"self'den sonra ilk parametre olarak ipucu listesi verilmeden bir sürücü " +"çağrısı kırpılamıyor " + +msgid "" +"Cannot use parents_as_list and parents_as_ids query params at the same time." +msgstr "" +"parents_as_list ve parents_as_ids sorgu parametreleri aynı anda kullanılamaz." + +msgid "" +"Cannot use subtree_as_list and subtree_as_ids query params at the same time." +msgstr "" +"subtree_as_list ve subtree_as_ids sorgu parametreleri aynı anda kullanılamaz." + +msgid "" +"Combining effective and group filter will always result in an empty list." +msgstr "" +"Efektif ve grup filtresini birleştirmek her zaman boş bir listeye yol açar." + +msgid "" +"Combining effective, domain and inherited filters will always result in an " +"empty list." +msgstr "" +"Efektif, alan ve miras filtrelerin birleştirilmesi her zaman boş bir listeye " +"yol açar." + +#, python-format +msgid "Conflict occurred attempting to store %(type)s - %(details)s" +msgstr "%(type)s depolanırken çatışma oluştu- %(details)s" + +#, python-format +msgid "Conflicting region IDs specified: \"%(url_id)s\" != \"%(ref_id)s\"" +msgstr "Çatışan bölge kimlikleri belirtildi: \"%(url_id)s\" != \"%(ref_id)s\"" + +msgid "Consumer not found" +msgstr "Tüketici bulunamadı" + +#, python-format +msgid "" +"Could not change immutable attribute(s) '%(attributes)s' in target %(target)s" +msgstr "" +"%(target)s hedefindeki değişmez öznitelik(ler) '%(attributes)s' " +"değiştirilemiyor" + +#, python-format +msgid "" +"Could not find %(group_or_option)s in domain configuration for domain " +"%(domain_id)s" +msgstr "" +"%(domain_id)s alanı için alan yapılandırmasında %(group_or_option)s " +"bulunamadı" + +#, python-format +msgid "Could not find Endpoint Group: %(endpoint_group_id)s" +msgstr "Bitişnoktası Grubu bulunamadı: %(endpoint_group_id)s" + +msgid "Could not find Identity Provider identifier in environment" +msgstr "Kimlik Sağlayıcı tanımlayıcısı ortamda bulunamıyor" + +#, python-format +msgid "Could not find Identity Provider: %(idp_id)s" +msgstr "Kimlik Sağlayıcı bulunamadı: %(idp_id)s" + +#, python-format +msgid "Could not find Service Provider: %(sp_id)s" +msgstr "Servis Sağlayıcı bulunamadı: %(sp_id)s" + +#, python-format +msgid "Could not find credential: %(credential_id)s" +msgstr "Kimlik bilgisi bulunamadı: %(credential_id)s" + +#, python-format +msgid "Could not find domain: %(domain_id)s" +msgstr "Alan bulunamadı: %(domain_id)s" + +#, python-format +msgid "Could not find endpoint: %(endpoint_id)s" +msgstr "Bitiş noktası bulunamadı: %(endpoint_id)s" + +#, python-format +msgid "" +"Could not find federated protocol %(protocol_id)s for Identity Provider: " +"%(idp_id)s" +msgstr "" +"Kimlik Sağlayıcı: %(idp_id)s için birleşmiş iletişim kuralı %(protocol_id)s " +"bulunamadı" + +#, python-format +msgid "Could not find group: %(group_id)s" +msgstr "Grup bulunamadı: %(group_id)s" + +#, python-format +msgid "Could not find mapping: %(mapping_id)s" +msgstr "Eşleştirme bulunamadı: %(mapping_id)s" + +msgid "Could not find policy association" +msgstr "İlke ilişkilendirme bulunamadı" + +#, python-format +msgid "Could not find policy: %(policy_id)s" +msgstr "İlke bulunamadı: %(policy_id)s" + +#, python-format +msgid "Could not find project: %(project_id)s" +msgstr "Proje bulunamadı: %(project_id)s" + +#, python-format +msgid "Could not find region: %(region_id)s" +msgstr "Bölge bulunamadı: %(region_id)s" + +msgid "Could not find role" +msgstr "Rol bulunamadı" + +#, python-format +msgid "" +"Could not find role assignment with role: %(role_id)s, user or group: " +"%(actor_id)s, project or domain: %(target_id)s" +msgstr "" +"Rol: %(role_id)s, kullanıcı veya grup: %(actor_id)s, proje veya alan: " +"%(target_id)s ile rol ataması bulunamadı" + +#, python-format +msgid "Could not find role: %(role_id)s" +msgstr "Rol bulunamadı: %(role_id)s" + +#, python-format +msgid "Could not find service: %(service_id)s" +msgstr "Servis bulunamadı: %(service_id)s" + +#, python-format +msgid "Could not find token: %(token_id)s" +msgstr "Jeton bulunamadı: %(token_id)s" + +#, python-format +msgid "Could not find trust: %(trust_id)s" +msgstr "Güven bulunamadı: %(trust_id)s" + +#, python-format +msgid "Could not find user: %(user_id)s" +msgstr "Kullanıcı bulunamadı: %(user_id)s" + +#, python-format +msgid "Could not find version: %(version)s" +msgstr "Sürüm bulunamadı: %(version)s" + +#, python-format +msgid "Could not find: %(target)s" +msgstr "Bulunamadı: %(target)s" + +msgid "Could not validate the access token" +msgstr "Erişim jetonu doğrulanamadı" + +msgid "Credential belongs to another user" +msgstr "Kimlik bilgisi başka bir kullanıcıya ait" + +#, python-format +msgid "Database at /domains/%s/config" +msgstr "/domains/%s/config konumundaki veri tabanı" + +msgid "" +"Disabling an entity where the 'enable' attribute is ignored by configuration." +msgstr "" +"'enable' özniteliği yapılandırma tarafından göz ardı edilen bir öğe " +"kapatılıyor." + +#, python-format +msgid "Domain (%s)" +msgstr "Alan (%s)" + +#, python-format +msgid "Domain cannot be named %s" +msgstr "Alan %s olarak adlandırılamaz" + +#, python-format +msgid "Domain cannot have ID %s" +msgstr "Alan %s ID'sine sahip olamaz" + +#, python-format +msgid "Domain is disabled: %s" +msgstr "Alan kapalı: %s" + +msgid "Domain metadata not supported by LDAP" +msgstr "Alan metadata'sı LDAP tarafından desteklenmiyor" + +msgid "Domain scoped token is not supported" +msgstr "Alan kapsamlı jeton desteklenmiyor" + +#, python-format +msgid "" +"Domain specific sql drivers are not supported via the Identity API. One is " +"specified in /domains/%s/config" +msgstr "" +"Alana özel sql sürücüleri Kimlik API'si tarafından desteklenmiyor. Birisi /" +"domains/%s/config içinde tanımlanmış" + +#, python-format +msgid "" +"Domain: %(domain)s already has a configuration defined - ignoring file: " +"%(file)s." +msgstr "" +"Alan: %(domain)s zaten tanımlanmış bir yapılandırmaya sahip - dosya " +"atlanıyor: %(file)s." + +msgid "Domains are not supported by the v2 API. Please use the v3 API instead." +msgstr "v2 API alanları desteklemiyor. Bunun yerine lütfen v3 API kullanın" + +msgid "Domains are read-only against LDAP" +msgstr "Alanlar LDAP'a karşı yalnızca-okunur" + +msgid "Duplicate Entry" +msgstr "Kopya Girdi" + +#, python-format +msgid "Duplicate ID, %s." +msgstr "Kopya ID, %s" + +#, python-format +msgid "Duplicate name, %s." +msgstr "Kopya isim, %s." + +msgid "Enabled field must be a boolean" +msgstr "Etkin alan bool olmalı" + +msgid "Enabled field should be a boolean" +msgstr "Etkin alan bool olmalı" + +#, python-format +msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" +msgstr "Bitiş noktası %(endpoint_id)s %(project_id)s projesinde bulunamadı" + +msgid "Endpoint Group Project Association not found" +msgstr "Bitiş Noktası Grup Proje İlişkisi bulunamadı" + +msgid "Ensure configuration option idp_entity_id is set." +msgstr "idp_entity_id yapılandırma seçeneğinin ayarlandığına emin olun." + +msgid "Ensure configuration option idp_sso_endpoint is set." +msgstr "idp_sso_endpoint yapılandırma seçeneğinin ayarlandığına emin olun." + +#, python-format +msgid "" +"Error parsing configuration file for domain: %(domain)s, file: %(file)s." +msgstr "" +"Alan: %(domain)s için yapılandırma dosyası ayrıştırılırken hata, dosya: " +"%(file)s." + +#, python-format +msgid "Error while opening file %(path)s: %(err)s" +msgstr "Dosya açılırken hata %(path)s: %(err)s" + +#, python-format +msgid "Error while parsing line: '%(line)s': %(err)s" +msgstr "Satır ayrıştırılırken hata: '%(line)s': %(err)s" + +#, python-format +msgid "Error while parsing rules %(path)s: %(err)s" +msgstr "Kurallar ayrıştırılırken hata %(path)s: %(err)s" + +#, python-format +msgid "Error while reading metadata file, %(reason)s" +msgstr "Metadata dosyası okunurken hata, %(reason)s" + +#, python-format +msgid "Expected dict or list: %s" +msgstr "Sözlük ya da liste beklendi: %s" + +msgid "" +"Expected signing certificates are not available on the server. Please check " +"Keystone configuration." +msgstr "" +"Beklenen imzalama sertifikaları sunucuda kullanılabilir değil. Lütfen " +"Keystone yapılandırmasını kontrol edin." + +#, python-format +msgid "" +"Expecting to find %(attribute)s in %(target)s - the server could not comply " +"with the request since it is either malformed or otherwise incorrect. The " +"client is assumed to be in error." +msgstr "" +"%(target)s içinde %(attribute)s bulunması bekleniyordu - sunucu talebi " +"yerine getiremedi çünkü ya istek kusurluydu ya da geçersizdi. İstemcinin " +"hatalı olduğu varsayılıyor." + +#, python-format +msgid "Failed to start the %(name)s server" +msgstr "%(name)s sunucusu başlatılamadı" + +msgid "Failed to validate token" +msgstr "Jeton doğrulama başarısız" + +msgid "Federation token is expired" +msgstr "Federasyon jetonunun süresi dolmuş" + +#, python-format +msgid "" +"Field \"remaining_uses\" is set to %(value)s while it must not be set in " +"order to redelegate a trust" +msgstr "" +"\"remaining_uses\" alanı %(value)s olarak ayarlanmış, bir güvene tekrar " +"yetki vermek için böyle ayarlanmamalı" + +msgid "Found invalid token: scoped to both project and domain." +msgstr "Geçersiz jeton bulundu: hem proje hem alana kapsanmış." + +#, python-format +msgid "Group %(group)s is not supported for domain specific configurations" +msgstr "%(group)s grubu alana özel yapılandırmalar için desteklenmiyor" + +#, python-format +msgid "" +"Group %(group_id)s returned by mapping %(mapping_id)s was not found in the " +"backend." +msgstr "" +"%(mapping_id)s eşleştirmesi tarafından döndürülen %(group_id)s grubu arka " +"uçta bulunamadı." + +#, python-format +msgid "" +"Group membership across backend boundaries is not allowed, group in question " +"is %(group_id)s, user is %(user_id)s" +msgstr "" +"Arka uç sınırları arasında grup üyeliğine izin verilmez, sorudaki grup " +"%(group_id)s, kullanıcı ise %(user_id)s" + +#, python-format +msgid "ID attribute %(id_attr)s not found in LDAP object %(dn)s" +msgstr "ID özniteliği %(id_attr)s %(dn)s LDAP nesnesinde bulunamadı" + +#, python-format +msgid "Identity Provider %(idp)s is disabled" +msgstr "Kimlik Sağlayıcı %(idp)s kapalı" + +msgid "" +"Incoming identity provider identifier not included among the accepted " +"identifiers." +msgstr "" +"Gelen kimlik sağlayıcı tanımlayıcısı kabul edilen tanımlayıcılar arasında " +"yok." + +#, python-format +msgid "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" +msgstr "" +"Geçersiz LDAP TLS sertifika seçeneği: %(option)s. Şunlardan birini seçin: " +"%(options)s" + +#, python-format +msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" +msgstr "Geçersiz LDAP TLS_AVAIL seçeneği: %s. TLS kullanılabilir değil" + +#, python-format +msgid "Invalid LDAP deref option: %(option)s. Choose one of: %(options)s" +msgstr "" +"Geçersiz LDAP referans kaldırma seçeneği: %(option)s. Şunlardan birini " +"seçin: %(options)s" + +#, python-format +msgid "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" +msgstr "Geçersiz LDAP kapsamı: %(scope)s. Şunlardan birini seçin: %(options)s" + +msgid "Invalid TLS / LDAPS combination" +msgstr "Geçersiz TLS / LDAPS kombinasyonu" + +#, python-format +msgid "Invalid audit info data type: %(data)s (%(type)s)" +msgstr "Geçersiz denetim bilgisi veri türü: %(data)s (%(type)s)" + +msgid "Invalid blob in credential" +msgstr "Kimlik bilgisinde geçersiz düğüm" + +#, python-format +msgid "" +"Invalid domain name: %(domain)s found in config file name: %(file)s - " +"ignoring this file." +msgstr "" +"Yapılandırma dosyası isminde: %(file)s geçersiz alan adı: %(domain)s bulundu " +"- bu dosya atlanıyor." + +#, python-format +msgid "Invalid domain specific configuration: %(reason)s" +msgstr "Geçersiz alana özel yapılandırma: %(reason)s" + +#, python-format +msgid "Invalid input for field '%(path)s'. The value is '%(value)s'." +msgstr "'%(path)s' alanı için geçersiz girdi. Değer '%(value)s'." + +msgid "Invalid limit value" +msgstr "Geçersiz sınır değeri" + +#, python-format +msgid "" +"Invalid mix of entities for policy association - only Endpoint, Service or " +"Region+Service allowed. Request was - Endpoint: %(endpoint_id)s, Service: " +"%(service_id)s, Region: %(region_id)s" +msgstr "" +"İlke ilişkilendirmeleri için geçersiz öğe karışımı - yalnızca Bitişnoktası, " +"Servis veya Bölge+Servise izin verilir. İstek şuydu Bitişnoktası: " +"%(endpoint_id)s, Servis: %(service_id)s, Bölge: %(region_id)s" + +#, python-format +msgid "" +"Invalid rule: %(identity_value)s. Both 'groups' and 'domain' keywords must " +"be specified." +msgstr "" +"Geçersiz kural: %(identity_value)s. Hem 'gruplar' hem 'alan' anahtar " +"kelimeleri belirtilmeli." + +msgid "Invalid signature" +msgstr "Geçersiz imza" + +#, python-format +msgid "" +"Invalid ssl_cert_reqs value of %s, must be one of \"NONE\", \"OPTIONAL\", " +"\"REQUIRED\"" +msgstr "" +"%s değerinde geçersiz ssl_cert_reqs, \"HİÇBİRİ\", \"İSTEĞE BAĞLI\", \"GEREKLİ" +"\" den biri olmalı" + +msgid "Invalid user / password" +msgstr "Geçersiz kullanıcı / parola" + +msgid "Invalid username or password" +msgstr "Geçersiz kullanıcı adı ve parola" + +#, python-format +msgid "KVS region %s is already configured. Cannot reconfigure." +msgstr "KVS bölgesi %s zaten yapılandırılmış. Yeniden yapılandırılamıyor." + +#, python-format +msgid "Key Value Store not configured: %s" +msgstr "Anahtar Değer Deposu yapılandırılmamış: %s" + +#, python-format +msgid "LDAP %s create" +msgstr "LDAP %s oluştur" + +#, python-format +msgid "LDAP %s delete" +msgstr "LDAP %s sil" + +#, python-format +msgid "LDAP %s update" +msgstr "LDAP %s güncelle" + +#, python-format +msgid "Lock Timeout occurred for key, %(target)s" +msgstr "Anahtar için Kilit Zaman Aşımı oluştu, %(target)s" + +#, python-format +msgid "Lock key must match target key: %(lock)s != %(target)s" +msgstr "Kilit anahtarı hedef anahtarla eşleşmeli: %(lock)s != %(target)s" + +#, python-format +msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." +msgstr "" +"Kusurlu bitiş noktası URL'si (%(endpoint)s), detaylar için HATA kaydına " +"bakın." + +msgid "Marker could not be found" +msgstr "İşaretçi bulunamadı" + +#, python-format +msgid "Maximum lock attempts on %s occurred." +msgstr "%s üzerinde azami kilit girişimi yapıldı." + +#, python-format +msgid "Member %(member)s is already a member of group %(group)s" +msgstr "Üye %(member)s zaten %(group)s grubunun üyesi" + +#, python-format +msgid "Method not callable: %s" +msgstr "Metod çağrılabilir değil: %s" + +msgid "Missing entity ID from environment" +msgstr "Öğe kimliği ortamdan eksik" + +msgid "" +"Modifying \"redelegation_count\" upon redelegation is forbidden. Omitting " +"this parameter is advised." +msgstr "" +"Tekrar yetkilendirme üzerine \"redelegation_count\" değiştirmeye izin " +"verilmez. Tavsiye edildiği gibi bu parametre atlanıyor." + +msgid "Multiple domains are not supported" +msgstr "Birden çok alan desteklenmiyor" + +msgid "Must be called within an active lock context." +msgstr "Etkin kilik içeriği içinde çağrılmalı." + +msgid "Must specify either domain or project" +msgstr "Alan ya da projeden biri belirtilmelidir" + +msgid "Name field is required and cannot be empty" +msgstr "İsim alanı gerekli ve boş olamaz" + +msgid "" +"No Authorization headers found, cannot proceed with OAuth related calls, if " +"running under HTTPd or Apache, ensure WSGIPassAuthorization is set to On." +msgstr "" +"Yetkilendirme başlıkları bulunamadı, OAuth ile ilişkili çağrılarla devam " +"edilemez, HTTPd veya Apache altında çalışıyorsanız, WSGIPassAuthorization " +"ayarını açtığınızdan emin olun." + +msgid "No authenticated user" +msgstr "Kimlik denetimi yapılmamış kullanıcı" + +msgid "" +"No encryption keys found; run keystone-manage fernet_setup to bootstrap one." +msgstr "" +"Şifreleme anahtarları bulundu; birini yükletmek için keystone-manage " +"fernet_setup çalıştırın." + +msgid "No options specified" +msgstr "Hiçbir seçenek belirtilmedi" + +#, python-format +msgid "No policy is associated with endpoint %(endpoint_id)s." +msgstr "Hiçbir ilke %(endpoint_id)s bitiş noktasıyla ilişkilendirilmemiş." + +#, python-format +msgid "No remaining uses for trust: %(trust_id)s" +msgstr "Güven için kalan kullanım alanı yok: %(trust_id)s" + +msgid "Non-default domain is not supported" +msgstr "Varsayılan olmayan alan desteklenmiyor" + +msgid "One of the trust agents is disabled or deleted" +msgstr "Güven ajanlarından biri kapalı ya da silinmiş" + +#, python-format +msgid "" +"Option %(option)s found with no group specified while checking domain " +"configuration request" +msgstr "" +"%(option)s seçeneği alan yapılandırma isteği kontrol edilirken hiçbir grup " +"belirtilmemiş şekilde bulundu" + +#, python-format +msgid "" +"Option %(option)s in group %(group)s is not supported for domain specific " +"configurations" +msgstr "" +"%(group)s grubundaki %(option)s seçeneği alana özel yapılandırmalarda " +"desteklenmiyor" + +#, python-format +msgid "Project (%s)" +msgstr "Proje (%s)" + +#, python-format +msgid "Project ID not found: %(t_id)s" +msgstr "Proje kimliği bulunamadı: %(t_id)s" + +msgid "Project field is required and cannot be empty." +msgstr "Proje alanı gerekli ve boş olamaz." + +#, python-format +msgid "Project is disabled: %s" +msgstr "Proje kapalı: %s" + +msgid "Redelegation allowed for delegated by trust only" +msgstr "" +"Tekrar yetki vermeye yalnızca güven tarafından yetki verilenler için izin " +"verilir" + +#, python-format +msgid "" +"Remaining redelegation depth of %(redelegation_depth)d out of allowed range " +"of [0..%(max_count)d]" +msgstr "" +"izin verilen [0..%(max_count)d] aralığı içinden %(redelegation_depth)d izin " +"verilen tekrar yetki verme derinliği" + +msgid "Request Token does not have an authorizing user id" +msgstr "İstek Jetonu yetki veren bir kullanıcı id'sine sahip değil" + +#, python-format +msgid "" +"Request attribute %(attribute)s must be less than or equal to %(size)i. The " +"server could not comply with the request because the attribute size is " +"invalid (too large). The client is assumed to be in error." +msgstr "" +"İstek özniteliği %(attribute)s %(size)i boyutuna eşit ya da daha küçük " +"olmalı. Sunucu talebi yerine getiremedi çünkü öznitelik boyutu geçersiz (çok " +"büyük). İstemcinin hata durumunda olduğu varsayılıyor." + +msgid "Request must have an origin query parameter" +msgstr "İstek bir başlangıç noktası sorgu parametresine sahip olmalı" + +msgid "Request token is expired" +msgstr "İstek jetonunun süresi dolmuş" + +msgid "Request token not found" +msgstr "İstek jetonu bulunamadı" + +msgid "Requested expiration time is more than redelegated trust can provide" +msgstr "" +"İstenen zaman bitim süresi tekrar yetkilendirilen güvenin " +"sağlayabileceğinden fazla" + +#, python-format +msgid "" +"Requested redelegation depth of %(requested_count)d is greater than allowed " +"%(max_count)d" +msgstr "" +"%(requested_count)d istenen tekrar yetki verme derinliği izin verilen " +"%(max_count)d den fazla" + +#, python-format +msgid "Role %s not found" +msgstr "%s rolü bulunamadı" + +msgid "" +"Running keystone via eventlet is deprecated as of Kilo in favor of running " +"in a WSGI server (e.g. mod_wsgi). Support for keystone under eventlet will " +"be removed in the \"M\"-Release." +msgstr "" +"Bir WSGI sunucuda (örn. mod_wsgi) çalıştırmak adına, keystone'nin eventlet " +"ile çalıştırılması Kilo'dan sonra desteklenmiyor. Eventlet altında keystone " +"desteği \"M\"-Sürümünde kaldırılacak." + +msgid "Scoping to both domain and project is not allowed" +msgstr "Hem alan hem projeye kapsamlamaya izin verilmez" + +msgid "Scoping to both domain and trust is not allowed" +msgstr "Hem alan hem güvene kapsamlamaya izin verilmez" + +msgid "Scoping to both project and trust is not allowed" +msgstr "Hem proje hem güvene kapsamlamaya izin verilmez" + +#, python-format +msgid "Service Provider %(sp)s is disabled" +msgstr "Servis Sağlayıcı %(sp)s kapalı" + +msgid "Some of requested roles are not in redelegated trust" +msgstr "İstenen rollerin bazıları tekrar yetki verilen güven içinde değil" + +msgid "Specify a domain or project, not both" +msgstr "Bir alan ya da proje belirtin, ya da her ikisini" + +msgid "Specify a user or group, not both" +msgstr "Bir kullanıcı ya da grup belirtin, ikisini birden değil" + +msgid "Specify one of domain or project" +msgstr "Alandan ya da projeden birini belirtin" + +msgid "Specify one of user or group" +msgstr "Kullanıcı ya da grup belirtin" + +#, python-format +msgid "" +"String length exceeded.The length of string '%(string)s' exceeded the limit " +"of column %(type)s(CHAR(%(length)d))." +msgstr "" +"Karakter dizisi uzunluğu aşıldı. '%(string)s' karakter dizisiz uzunluğu " +"%(type)s(CHAR(%(length)d)) sütunu sınırını aşıyor." + +msgid "" +"The 'expires_at' must not be before now. The server could not comply with " +"the request since it is either malformed or otherwise incorrect. The client " +"is assumed to be in error." +msgstr "" +"'expires_at' şu andan önce olmamalı. Sunucu talebi yerine getiremedi çünkü " +"istek ya kusurlu ya da geçersiz. İstemcinin hata durumunda olduğu " +"varsayılıyor." + +msgid "The --all option cannot be used with the --domain-name option" +msgstr "--all seçeneği --domain-name seçeneğiyle kullanılamaz" + +#, python-format +msgid "The Keystone configuration file %(config_file)s could not be found." +msgstr "Keystone yapılandırma dosyası %(config_file)s bulunamadı." + +#, python-format +msgid "" +"The Keystone domain-specific configuration has specified more than one SQL " +"driver (only one is permitted): %(source)s." +msgstr "" +"Keystone alana özel yapılandırması birden fazla SQL sürücüsü belirtti " +"(yalnızca birine izin verilir): %(source)s." + +msgid "The action you have requested has not been implemented." +msgstr "İstediğiniz eylem uygulanmamış." + +msgid "The authenticated user should match the trustor." +msgstr "Yetkilendirilen kullanıcı güven verenle eşleşmeli." + +msgid "" +"The certificates you requested are not available. It is likely that this " +"server does not use PKI tokens otherwise this is the result of " +"misconfiguration." +msgstr "" +"İstediğiniz sertifikalar kullanılabilir değil. Bu sunucu muhtemelen PKI " +"jetonlarını kullanmıyor ya da bu bir yanlış yapılandırmanın sonucu." + +#, python-format +msgid "" +"The password length must be less than or equal to %(size)i. The server could " +"not comply with the request because the password is invalid." +msgstr "" +"Parola uzunluğu %(size)i ye eşit ya da daha küçük olmalı. Sunucu talebe " +"cevap veremedi çünkü parola geçersiz." + +msgid "The request you have made requires authentication." +msgstr "Yaptığınız istek kimlik doğrulama gerektiriyor." + +msgid "The resource could not be found." +msgstr "Kaynak bulunamadı." + +msgid "" +"The revoke call must not have both domain_id and project_id. This is a bug " +"in the Keystone server. The current request is aborted." +msgstr "" +"İptal etme çağrısı hem domain_id hem project_id'ye sahip olmamalı. Bu " +"Keystone sunucudaki bir hata. Mevcut istek iptal edildi." + +msgid "The service you have requested is no longer available on this server." +msgstr "İstediğiniz servis artık bu sunucu üzerinde kullanılabilir değil." + +#, python-format +msgid "" +"The specified parent region %(parent_region_id)s would create a circular " +"region hierarchy." +msgstr "" +"Belirtilen üst bölge %(parent_region_id)s dairesel bölge sıralı dizisi " +"oluştururdu." + +#, python-format +msgid "" +"The value of group %(group)s specified in the config should be a dictionary " +"of options" +msgstr "" +"Yapılandırmada belirtilen %(group)s grubunun değeri seçenekler sözlüğü olmalı" + +msgid "There should not be any non-oauth parameters" +msgstr "Herhangi bir non-oauth parametresi olmamalı" + +#, python-format +msgid "This is not a recognized Fernet payload version: %s" +msgstr "Bu bilinen bir Fernet faydalı yük sürümü değil: %s" + +msgid "" +"This is not a v2.0 Fernet token. Use v3 for trust, domain, or federated " +"tokens." +msgstr "" +"Bu v2.0 Fernet jetonu değil. Güven, alan, veya federasyon jetonları için v3 " +"kullanın." + +msgid "" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client is " +"assumed to be in error." +msgstr "" +"Zaman damgası beklenen biçimde değil. Sunucu talebi yerine getiremedi çünkü " +"istek ya kusurlu ya da geçersiz. İstemcinin hata durumunda olduğu " +"varsayılıyor." + +#, python-format +msgid "" +"To get a more detailed information on this error, re-run this command for " +"the specific domain, i.e.: keystone-manage domain_config_upload --domain-" +"name %s" +msgstr "" +"Bu hatayla ilgili daha detaylı bilgi almak için, bu komutu belirtilen alan " +"için tekrar çalıştırın, örn.: keystone-manage domain_config_upload --domain-" +"name %s" + +msgid "Token belongs to another user" +msgstr "Jeton başka bir kullanıcıya ait" + +msgid "Token does not belong to specified tenant." +msgstr "Jeton belirtilen kiracıya ait değil." + +msgid "Trustee has no delegated roles." +msgstr "Yedieminin emanet edilen kuralları yok." + +msgid "Trustor is disabled." +msgstr "Güven kurucu kapalı." + +#, python-format +msgid "" +"Trying to update group %(group)s, so that, and only that, group must be " +"specified in the config" +msgstr "" +"%(group)s grubu güncellenmeye çalışılıyor, böylece yapılandırmada yalnızca " +"grup belirtilmeli" + +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, but config provided " +"contains option %(option_other)s instead" +msgstr "" +"%(group)s grubundaki %(option)s seçeneği güncellenmeye çalışılıyor, ama " +"sağlanan yapılandırma %(option_other)s seçeneğini içeriyor" + +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, so that, and only " +"that, option must be specified in the config" +msgstr "" +"%(group)s grubundaki %(option)s seçeneği güncellenmeye çalışıldı, böylece, " +"yapılandırmada yalnızca bu seçenek belirtilmeli" + +msgid "" +"Unable to access the keystone database, please check it is configured " +"correctly." +msgstr "" +"Keystone veri tabanına erişilemiyor, lütfen doğru yapılandırıldığından emin " +"olun." + +#, python-format +msgid "Unable to consume trust %(trust_id)s, unable to acquire lock." +msgstr "%(trust_id)s güveni tüketilemedi, kilit elde edilemiyor." + +#, python-format +msgid "" +"Unable to delete region %(region_id)s because it or its child regions have " +"associated endpoints." +msgstr "" +"Bölge %(region_id)s silinemedi çünkü kendisi ya da alt bölgelerinin " +"ilişkilendirilmiş bitiş noktaları var." + +#, python-format +msgid "Unable to find valid groups while using mapping %(mapping_id)s" +msgstr "Eşleştirme %(mapping_id)s kullanırken geçerli gruplar bulunamadı" + +#, python-format +msgid "" +"Unable to get a connection from pool id %(id)s after %(seconds)s seconds." +msgstr "%(seconds)s saniye sonra havuz %(id)s'den bağlantı alınamadı." + +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "Alan yapılandırma dizini bulunamıyor: %s" + +#, python-format +msgid "Unable to lookup user %s" +msgstr "%s kullanıcısı aranamadı" + +#, python-format +msgid "" +"Unable to reconcile identity attribute %(attribute)s as it has conflicting " +"values %(new)s and %(old)s" +msgstr "" +"Kimlik özniteliği %(attribute)s bağdaştırılamıyor çünkü çatışan değerleri " +"var %(new)s ve %(old)s" + +#, python-format +msgid "" +"Unable to sign SAML assertion. It is likely that this server does not have " +"xmlsec1 installed, or this is the result of misconfiguration. Reason " +"%(reason)s" +msgstr "" +"SAML ifadesi imzalanamıyor. Muhtemelen bu sunucuda xmlsec1 kurulu değil, " +"veya bu bir yanlış yapılandırmanın sonucu. Sebep %(reason)s" + +msgid "Unable to sign token." +msgstr "Jeton imzalanamıyor." + +#, python-format +msgid "Unexpected assignment type encountered, %s" +msgstr "Beklenmedik atama türüyle karşılaşıldı, %s" + +#, python-format +msgid "" +"Unexpected combination of grant attributes - User: %(user_id)s, Group: " +"%(group_id)s, Project: %(project_id)s, Domain: %(domain_id)s" +msgstr "" +"İzin özniteliklerinin beklenmedik katışımı - Kullanıcı: %(user_id)s, Grup: " +"%(group_id)s, Proje: %(project_id)s, Alan: %(domain_id)s" + +#, python-format +msgid "Unexpected status requested for JSON Home response, %s" +msgstr "JSON Home yanıtı için beklenmedik durum istendi, %s" + +msgid "Unknown Target" +msgstr "Bilinmeyen Hedef" + +#, python-format +msgid "Unknown domain '%(name)s' specified by --domain-name" +msgstr "--domain-name ile bilinmeyen alan '%(name)s' belirtilmiş" + +#, python-format +msgid "Unknown token version %s" +msgstr "Bilinmeyen jeton sürümü %s" + +#, python-format +msgid "Unregistered dependency: %(name)s for %(targets)s" +msgstr "Kaydı silinmiş bağımlılık: %(targets)s için %(name)s" + +msgid "Update of `parent_id` is not allowed." +msgstr "`parent_id` güncellemesine izin verilmiyor." + +msgid "Use a project scoped token when attempting to create a SAML assertion" +msgstr "" +"SAML iddiası oluşturma girişimi sırasında proje kapsamlı bir jeton kullan" + +#, python-format +msgid "User %(u_id)s is unauthorized for tenant %(t_id)s" +msgstr "%(u_id)s kullanıcısı %(t_id)s kiracısı için yetkilendirilmemiş" + +#, python-format +msgid "User %(user_id)s already has role %(role_id)s in tenant %(tenant_id)s" +msgstr "" +"Kullanıcı %(user_id)s zaten %(tenant_id)s kiracısı içinde bir %(role_id)s " +"rolüne sahip" + +#, python-format +msgid "User %(user_id)s has no access to domain %(domain_id)s" +msgstr "%(user_id)s kullanıcısının %(domain_id)s alanına erişimi yok" + +#, python-format +msgid "User %(user_id)s has no access to project %(project_id)s" +msgstr "%(user_id)s kullanıcısının %(project_id)s projesine erişimi yok" + +#, python-format +msgid "User %(user_id)s is already a member of group %(group_id)s" +msgstr "Kullanıcı %(user_id)s zaten %(group_id)s grubu üyesi" + +#, python-format +msgid "User '%(user_id)s' not found in group '%(group_id)s'" +msgstr "Kullanıcı '%(user_id)s' '%(group_id)s' grubunda bulunamadı" + +msgid "User IDs do not match" +msgstr "Kullanıcı ID leri uyuşmuyor" + +#, python-format +msgid "User is disabled: %s" +msgstr "Kullanıcı kapalı: %s" + +msgid "User is not a member of the requested project" +msgstr "Kullanıcı istenen projenin üyesi değil" + +msgid "User is not a trustee." +msgstr "Kullanıcı güvenilir değil." + +msgid "User not found" +msgstr "Kullanıcı bulunamadı" + +msgid "User roles not supported: tenant_id required" +msgstr "Kullanıcı rolleri desteklenmiyor: tenant_id gerekli" + +#, python-format +msgid "User type %s not supported" +msgstr "Kullanıcı türü %s desteklenmiyor" + +msgid "You are not authorized to perform the requested action." +msgstr "İstenen eylemi gerçekleştirmek için yetkili değilsiniz." + +#, python-format +msgid "You are not authorized to perform the requested action: %(action)s" +msgstr "İstenen eylemi gerçekleştirmek için yetkili değilsiniz: %(action)s" + +msgid "`key_mangler` functions must be callable." +msgstr "`key_mangler` fonksiyonları çağrılabilir olmalı." + +msgid "`key_mangler` option must be a function reference" +msgstr "`key_mangler` seçeneği fonksiyon referansı olmalı" + +msgid "any options" +msgstr "herhangi bir seçenek" + +msgid "auth_type is not Negotiate" +msgstr "auth_type Negotiate değil" + +msgid "authorizing user does not have role required" +msgstr "yetkilendiren kullanıcı gerekli role sahip değil" + +msgid "cache_collection name is required" +msgstr "cache_collection ismi gerekli" + +#, python-format +msgid "cannot create a project in a branch containing a disabled project: %s" +msgstr "kapalı bir proje içeren bir alt grupta proje oluşturulamaz: %s" + +msgid "cannot create a project within a different domain than its parents." +msgstr "üst projelerinden farklı alanda bir proje oluşturulamaz." + +msgid "cannot delete a domain that is enabled, please disable it first." +msgstr "etkin alan silinemez, lütfen önce kapatın." + +#, python-format +msgid "cannot delete the project %s since it is not a leaf in the hierarchy." +msgstr "%s projesi silinemiyor çünkü sıradüzen içindeki bir yaprak değil." + +#, python-format +msgid "cannot disable project %s since its subtree contains enabled projects" +msgstr "proje %s kapatılamıyor çünkü alt ağacında etkin projeler var" + +#, python-format +msgid "cannot enable project %s since it has disabled parents" +msgstr "proje %s etkinleştirilemiyor çünkü üstleri kapatılmış" + +msgid "database db_name is required" +msgstr "veri tabanı db_name gerekli" + +msgid "db_hosts value is required" +msgstr "db_hosts değeri gerekli" + +msgid "delete the default domain" +msgstr "varsayılan alanı sil" + +#, python-format +msgid "group %(group)s" +msgstr "grup %(group)s" + +msgid "" +"idp_contact_type must be one of: [technical, other, support, administrative " +"or billing." +msgstr "" +"idp_contact_type şunlardan biri olmalı: [teknik, diğer, destek, idari veya " +"faturalama." + +msgid "integer value expected for mongo_ttl_seconds" +msgstr "mongo_ttl_seconds için tam sayı değer bekleniyor" + +msgid "integer value expected for w (write concern attribute)" +msgstr "w için tam sayı değer bekleniyor (yazma ilgisi özniteliği)" + +#, python-format +msgid "invalid date format %s" +msgstr "geçersiz tarih biçimi %s" + +#, python-format +msgid "max hierarchy depth reached for %s branch." +msgstr "%s alt grubu için azami sıralı dizi derinliğine ulaşıldı." + +msgid "no ssl support available" +msgstr "ssl desteği yok" + +#, python-format +msgid "option %(option)s in group %(group)s" +msgstr "%(group)s grubundaki %(option)s seçeneği" + +msgid "pad must be single character" +msgstr "dolgu tek bir karakter olmalı" + +msgid "padded base64url text must be multiple of 4 characters" +msgstr "dolgulanmış base64url metni 4 karakterin katı olmalı" + +msgid "provided consumer key does not match stored consumer key" +msgstr "sağlanan tüketici anahtarı depolanan tüketici anahtarıyla eşleşmiyor" + +msgid "provided request key does not match stored request key" +msgstr "sağlanan istek anahtarı depolanan istek anahtarıyla eşleşmiyor" + +msgid "provided verifier does not match stored verifier" +msgstr "sağlanan doğrulayıcı depolanan doğrulayıcı ile eşleşmiyor" + +msgid "region not type dogpile.cache.CacheRegion" +msgstr "bölge dogpile.cache.CacheRegion türünde değil" + +msgid "remaining_uses must be a positive integer or null." +msgstr "remaining_uses pozitif bir değer ya da null olmalı." + +msgid "remaining_uses must not be set if redelegation is allowed" +msgstr "tekrar yetkilendirmeye izin veriliyorsa remaining_uses ayarlanmamalı" + +msgid "replicaset_name required when use_replica is True" +msgstr "use_replica True olduğunda replicaset_name gereklidir" + +#, python-format +msgid "" +"request to update group %(group)s, but config provided contains group " +"%(group_other)s instead" +msgstr "" +"%(group)s grubunu güncelleme isteği, ama sağlanan yapılandırma " +"%(group_other)s grubunu içeriyor" + +msgid "rescope a scoped token" +msgstr "kapsamlı bir jeton tekrar kapsamlandı" + +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" +msgstr "metin 4'ün katı, ama dolgu \"%s\" son karaktere 2 önceden önce" + +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" +msgstr "" +"metin 4'ün katı, ama doldurma \"%s\" doldurma karakteri olmayan son " +"karakterden önce" + +#, python-format +msgid "text is not a multiple of 4, but contains pad \"%s\"" +msgstr "metin 4'ün katı değil, ama \"%s\" dolgusu içeriyor" + +#, python-format +msgid "tls_cacertdir %s not found or is not a directory" +msgstr "tls_cacertdir %s bulunamadı ya da bir dizin" + +#, python-format +msgid "tls_cacertfile %s not found or is not a file" +msgstr "tls_cacertfile %s bulunamadı ya da bir dosya değil" + +#, python-format +msgid "token reference must be a KeystoneToken type, got: %s" +msgstr "jeton referansı bir KeystoneToken türünde olmalı, alınan: %s" diff --git a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po index a3a728e9..cbdab8a4 100644 --- a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/keystone/" +"Language-Team: Chinese (China) (http://www.transifex.com/openstack/keystone/" "language/zh_CN/)\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "无法打开模板文件 %s" diff --git a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po index a48b9382..da273412 100644 --- a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po +++ b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po @@ -4,71 +4,57 @@ # # Translators: # Xiao Xi LIU <liuxx@cn.ibm.com>, 2014 +# 刘俊朋 <liujunpeng@inspur.com>, 2015 msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" -"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-06-26 17:13+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/keystone/" +"Language-Team: Chinese (China) (http://www.transifex.com/openstack/keystone/" "language/zh_CN/)\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: keystone/notifications.py:304 -msgid "Failed to construct notifier" -msgstr "" - -#: keystone/notifications.py:389 -#, python-format -msgid "Failed to send %(res_id)s %(event_type)s notification" -msgstr "" - -#: keystone/notifications.py:606 -#, python-format -msgid "Failed to send %(action)s %(event_type)s notification" -msgstr "" - -#: keystone/catalog/core.py:62 -#, python-format -msgid "Malformed endpoint - %(url)r is not a string" -msgstr "" +msgid "Cannot retrieve Authorization headers" +msgstr "无法获取认证头信息" -#: keystone/catalog/core.py:66 #, python-format -msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" -msgstr "端点 %(url)s 的格式不正确 - 键 %(keyerror)s 未知" +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "在项目树-%(project_id)s 中发现循环引用或重复项。" -#: keystone/catalog/core.py:71 #, python-format msgid "" -"Malformed endpoint '%(url)s'. The following type error occurred during " -"string substitution: %(typeerror)s" -msgstr "" -"端点 '%(url)s' 的格式不正确。在字符串替换时发生以下类型错误:%(typeerror)s" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "在域树- %(region_id)s 中发现循环引用或重复项。" -#: keystone/catalog/core.py:77 #, python-format msgid "" -"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" -msgstr "端点 %s 的格式不完整 - (是否缺少了类型通告者?)" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "在项目树-%(project_id)s 中发现循环引用或重复项。" -#: keystone/common/openssl.py:93 #, python-format msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" msgstr "命令 %(to_exec)s 已退出,退出码及输出为 %(retcode)s- %(output)s" -#: keystone/common/openssl.py:121 #, python-format -msgid "Failed to remove file %(file_path)r: %(error)s" -msgstr "无法删除文件%(file_path)r: %(error)s" +msgid "Could not bind to %(host)s:%(port)s" +msgstr "无法绑定至 %(host)s:%(port)s" + +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "[fernet_tokens] 键仓库不存在或者ketystone没有足够的权限去访问它: %s。" -#: keystone/common/utils.py:239 msgid "" "Error setting up the debug environment. Verify that the option --debug-url " "has the format <host>:<port> and that a debugger processes is listening on " @@ -77,101 +63,82 @@ msgstr "" "设置调试环境出错。请确保选项--debug-url 的格式是这样的<host>:<port> ,和确保" "有一个调试进程正在监听那个端口" -#: keystone/common/cache/core.py:100 #, python-format -msgid "" -"Unable to build cache config-key. Expected format \"<argname>:<value>\". " -"Skipping unknown format: %s" -msgstr "" - -#: keystone/common/environment/eventlet_server.py:99 -#, python-format -msgid "Could not bind to %(host)s:%(port)s" -msgstr "无法绑定至 %(host)s:%(port)s" +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "对断言进行签名时出错,原因:%(reason)s" -#: keystone/common/environment/eventlet_server.py:185 -msgid "Server error" -msgstr "服务器报错" +msgid "Failed to construct notifier" +msgstr "构造通知器失败" -#: keystone/contrib/endpoint_policy/core.py:129 -#: keystone/contrib/endpoint_policy/core.py:228 -#, python-format msgid "" -"Circular reference or a repeated entry found in region tree - %(region_id)s." -msgstr "在域树- %(region_id)s 中发现循环引用或重复项。" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "创建[Fernet_tokens] 键仓库失败:它已存在或你没有足够的权限去创建它。" -#: keystone/contrib/federation/idp.py:410 #, python-format -msgid "Error when signing assertion, reason: %(reason)s" -msgstr "对断言进行签名时出错,原因:%(reason)s" - -#: keystone/contrib/oauth1/core.py:136 -msgid "Cannot retrieve Authorization headers" -msgstr "" +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "无法删除文件%(file_path)r: %(error)s" -#: keystone/openstack/common/loopingcall.py:95 -msgid "in fixed duration looping call" -msgstr "在固定时段内循环调用" +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "发送 %(action)s %(event_type)s 通知失败" -#: keystone/openstack/common/loopingcall.py:138 -msgid "in dynamic looping call" -msgstr "在动态循环调用中" +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "发送%(res_id)s %(event_type)s 通知失败" -#: keystone/openstack/common/service.py:268 -msgid "Unhandled exception" -msgstr "存在未处理的异常" +msgid "Failed to validate token" +msgstr "token验证失败" -#: keystone/resource/core.py:477 #, python-format -msgid "" -"Circular reference or a repeated entry found projects hierarchy - " -"%(project_id)s." -msgstr "" +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "端点 %(url)s 的格式不正确 - 键 %(keyerror)s 未知" -#: keystone/resource/core.py:939 #, python-format msgid "" -"Unexpected results in response for domain config - %(count)s responses, " -"first option is %(option)s, expected option %(expected)s" -msgstr "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "端点 %s 的格式不完整 - (是否缺少了类型通告者?)" -#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 #, python-format msgid "" -"Circular reference or a repeated entry found in projects hierarchy - " -"%(project_id)s." +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" msgstr "" +"端点 '%(url)s' 的格式不正确。在字符串替换时发生以下类型错误:%(typeerror)s" -#: keystone/token/provider.py:292 #, python-format -msgid "Unexpected error or malformed token determining token expiry: %s" -msgstr "" +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "端点 - %(url)r 不是一个字符串" -#: keystone/token/persistence/backends/kvs.py:226 #, python-format msgid "" "Reinitializing revocation list due to error in loading revocation list from " "backend. Expected `list` type got `%(type)s`. Old revocation list data: " "%(list)r" msgstr "" +"由于从后端加载撤销列表出现错误,重新初始化撤销列表。期望“列表”类型是 `" +"%(type)s`。旧的撤销列表数据是: %(list)r" -#: keystone/token/providers/common.py:611 -msgid "Failed to validate token" -msgstr "token验证失败" +msgid "Server error" +msgstr "服务器报错" + +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \"<argname>:<value>\". " +"Skipping unknown format: %s" +msgstr "无法构建缓存配置键值对。期望格式“<参数>:<值>”。跳过未知的格式: %s" -#: keystone/token/providers/pki.py:47 msgid "Unable to sign token" -msgstr "" +msgstr "无法签名令牌" -#: keystone/token/providers/fernet/utils.py:38 #, python-format -msgid "" -"Either [fernet_tokens] key_repository does not exist or Keystone does not " -"have sufficient permission to access it: %s" -msgstr "" +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "决策令牌预计超期时间 :%s 时,出现未知错误或变形的令牌" -#: keystone/token/providers/fernet/utils.py:79 +#, python-format msgid "" -"Failed to create [fernet_tokens] key_repository: either it already exists or " -"you don't have sufficient permissions to create it" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" msgstr "" +"针对域配置- %(count)s 结果,响应中出现不是预期结果,第一参数是%(option)s,期" +"望参数是 %(expected)s 。" diff --git a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po index 0e848ee1..92f06dcb 100644 --- a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po +++ b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po @@ -8,33 +8,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2015-03-09 06:03+0000\n" -"PO-Revision-Date: 2015-03-07 08:47+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" +"PO-Revision-Date: 2015-08-01 06:26+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/keystone/" +"Language-Team: Chinese (China) (http://www.transifex.com/openstack/keystone/" "language/zh_CN/)\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: keystone/assignment/core.py:250 -#, python-format -msgid "Creating the default role %s because it does not exist." -msgstr "正在创建默认角色%s,因为它之前不存在。" - -#: keystone/assignment/core.py:258 -#, python-format -msgid "Creating the default role %s failed because it was already created" -msgstr "" - -#: keystone/auth/controllers.py:64 -msgid "Loading auth-plugins by class-name is deprecated." -msgstr "通过class-name(类名)加载auth-plugins(认证插件)的方式已被弃用。" - -#: keystone/auth/controllers.py:106 #, python-format msgid "" "\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " @@ -43,173 +28,55 @@ msgstr "" "\"expires_at\" 被赋予矛盾的值: %(existing)s 和 %(new)s。将采用时间上较早的那" "个值。" -#: keystone/common/openssl.py:81 -#, python-format -msgid "Running command - %s" -msgstr "正在运行命令 - %s" - -#: keystone/common/wsgi.py:79 -msgid "No bind information present in token" -msgstr "令牌中暂无绑定信息" - -#: keystone/common/wsgi.py:83 -#, python-format -msgid "Named bind mode %s not in bind information" -msgstr "在绑定信息中没有命名绑定模式%s" - -#: keystone/common/wsgi.py:90 -msgid "Kerberos credentials required and not present" -msgstr "没有所需的Kerberos凭证" - -#: keystone/common/wsgi.py:94 -msgid "Kerberos credentials do not match those in bind" -msgstr "在绑定中没有匹配的Kerberos凭证" - -#: keystone/common/wsgi.py:98 -msgid "Kerberos bind authentication successful" -msgstr "Kerberos绑定认证成功" - -#: keystone/common/wsgi.py:105 -#, python-format -msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" -msgstr "不能验证未知绑定: {%(bind_type)s: %(identifier)s}" - -#: keystone/common/environment/eventlet_server.py:103 -#, python-format -msgid "Starting %(arg0)s on %(host)s:%(port)s" -msgstr "正在 %(host)s:%(port)s 上启动 %(arg0)s" - -#: keystone/common/kvs/core.py:138 #, python-format msgid "Adding proxy '%(proxy)s' to KVS %(name)s." msgstr "正在将代理'%(proxy)s'加入KVS %(name)s 中。" -#: keystone/common/kvs/core.py:188 #, python-format -msgid "Using %(func)s as KVS region %(name)s key_mangler" -msgstr "使用 %(func)s 作为KVS域 %(name)s 的key_mangler处理函数" +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "不能验证未知绑定: {%(bind_type)s: %(identifier)s}" -#: keystone/common/kvs/core.py:200 #, python-format -msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" -msgstr "" -"使用默认的dogpile sha1_mangle_key函数作为KVS域 %s 的key_mangler处理函数" +msgid "Creating the default role %s because it does not exist." +msgstr "正在创建默认角色%s,因为它之前不存在。" -#: keystone/common/kvs/core.py:210 #, python-format msgid "KVS region %s key_mangler disabled." msgstr "KVS域 %s 的key_mangler处理函数被禁用。" -#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 -#, python-format -msgid "" -"Received the following notification: service %(service)s, resource_type: " -"%(resource_type)s, operation %(operation)s payload %(payload)s" -msgstr "" - -#: keystone/openstack/common/eventlet_backdoor.py:146 -#, python-format -msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" -msgstr "携程为进程 %(pid)d 在后台监听 %(port)s " - -#: keystone/openstack/common/service.py:173 -#, python-format -msgid "Caught %s, exiting" -msgstr "捕获到 %s,正在退出" - -#: keystone/openstack/common/service.py:231 -msgid "Parent process has died unexpectedly, exiting" -msgstr "父进程已意外终止,正在退出" - -#: keystone/openstack/common/service.py:262 -#, python-format -msgid "Child caught %s, exiting" -msgstr "子代捕获 %s,正在退出" - -#: keystone/openstack/common/service.py:301 -msgid "Forking too fast, sleeping" -msgstr "派生速度太快,正在休眠" +msgid "Kerberos bind authentication successful" +msgstr "Kerberos绑定认证成功" -#: keystone/openstack/common/service.py:320 -#, python-format -msgid "Started child %d" -msgstr "已启动子代 %d" +msgid "Kerberos credentials do not match those in bind" +msgstr "在绑定中没有匹配的Kerberos凭证" -#: keystone/openstack/common/service.py:330 -#, python-format -msgid "Starting %d workers" -msgstr "正在启动 %d 工作程序" +msgid "Kerberos credentials required and not present" +msgstr "没有所需的Kerberos凭证" -#: keystone/openstack/common/service.py:347 #, python-format -msgid "Child %(pid)d killed by signal %(sig)d" -msgstr "信号 %(sig)d 已终止子代 %(pid)d" +msgid "Named bind mode %s not in bind information" +msgstr "在绑定信息中没有命名绑定模式%s" -#: keystone/openstack/common/service.py:351 -#, python-format -msgid "Child %(pid)s exited with status %(code)d" -msgstr "子代 %(pid)s 已退出,状态为 %(code)d" +msgid "No bind information present in token" +msgstr "令牌中暂无绑定信息" -#: keystone/openstack/common/service.py:390 #, python-format -msgid "Caught %s, stopping children" -msgstr "捕获到 %s,正在停止子代" - -#: keystone/openstack/common/service.py:399 -msgid "Wait called after thread killed. Cleaning up." -msgstr "线程结束,正在清理" +msgid "Running command - %s" +msgstr "正在运行命令 - %s" -#: keystone/openstack/common/service.py:415 #, python-format -msgid "Waiting on %d children to exit" -msgstr "正在等待 %d 个子代退出" +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "正在 %(host)s:%(port)s 上启动 %(arg0)s" -#: keystone/token/persistence/backends/sql.py:279 #, python-format msgid "Total expired tokens removed: %d" msgstr "被移除的失效令牌总数:%d" -#: keystone/token/providers/fernet/utils.py:72 -msgid "" -"[fernet_tokens] key_repository does not appear to exist; attempting to " -"create it" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:130 -#, python-format -msgid "Created a new key: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:143 -msgid "Key repository is already initialized; aborting." -msgstr "" - -#: keystone/token/providers/fernet/utils.py:179 -#, python-format -msgid "Starting key rotation with %(count)s key files: %(list)s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:185 -#, python-format -msgid "Current primary key is: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:187 -#, python-format -msgid "Next primary key will be: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:197 -#, python-format -msgid "Promoted key 0 to be the primary: %s" -msgstr "" - -#: keystone/token/providers/fernet/utils.py:213 #, python-format -msgid "Excess keys to purge: %s" -msgstr "" +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "使用 %(func)s 作为KVS域 %(name)s 的key_mangler处理函数" -#: keystone/token/providers/fernet/utils.py:237 #, python-format -msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" msgstr "" +"使用默认的dogpile sha1_mangle_key函数作为KVS域 %s 的key_mangler处理函数" diff --git a/keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po index b0ff57c9..c2e8b9ea 100644 --- a/keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po +++ b/keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po @@ -1,5 +1,5 @@ # Translations template for keystone. -# Copyright (C) 2014 OpenStack Foundation +# Copyright (C) 2015 OpenStack Foundation # This file is distributed under the same license as the keystone project. # # Translators: @@ -7,19 +7,18 @@ msgid "" msgstr "" "Project-Id-Version: Keystone\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" -"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"POT-Creation-Date: 2015-08-06 06:28+0000\n" "PO-Revision-Date: 2014-08-31 15:19+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/" -"keystone/language/zh_TW/)\n" +"Language-Team: Chinese (Taiwan) (http://www.transifex.com/openstack/keystone/" +"language/zh_TW/)\n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" +"Generated-By: Babel 2.0\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: keystone/catalog/backends/templated.py:106 #, python-format msgid "Unable to open template file %s" msgstr "無法開啟範本檔 %s" diff --git a/keystone-moon/keystone/middleware/core.py b/keystone-moon/keystone/middleware/core.py index bf86cd2b..62ff291a 100644 --- a/keystone-moon/keystone/middleware/core.py +++ b/keystone-moon/keystone/middleware/core.py @@ -14,16 +14,16 @@ from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_middleware import sizelimit from oslo_serialization import jsonutils -import six from keystone.common import authorization from keystone.common import wsgi from keystone import exception from keystone.i18n import _LW from keystone.models import token_model -from keystone.openstack.common import versionutils + CONF = cfg.CONF LOG = log.getLogger(__name__) @@ -51,8 +51,7 @@ class TokenAuthMiddleware(wsgi.Middleware): context = request.environ.get(CONTEXT_ENV, {}) context['token_id'] = token if SUBJECT_TOKEN_HEADER in request.headers: - context['subject_token_id'] = ( - request.headers.get(SUBJECT_TOKEN_HEADER)) + context['subject_token_id'] = request.headers[SUBJECT_TOKEN_HEADER] request.environ[CONTEXT_ENV] = context @@ -82,7 +81,7 @@ class PostParamsMiddleware(wsgi.Middleware): def process_request(self, request): params_parsed = request.params params = {} - for k, v in six.iteritems(params_parsed): + for k, v in params_parsed.items(): if k in ('self', 'context'): continue if k.startswith('_'): @@ -132,7 +131,7 @@ class JsonBodyMiddleware(wsgi.Middleware): return wsgi.render_exception(e, request=request) params = {} - for k, v in six.iteritems(params_parsed): + for k, v in params_parsed.items(): if k in ('self', 'context'): continue if k.startswith('_'): @@ -142,35 +141,6 @@ class JsonBodyMiddleware(wsgi.Middleware): request.environ[PARAMS_ENV] = params -class XmlBodyMiddleware(wsgi.Middleware): - """De/serialize XML to/from JSON.""" - - def print_warning(self): - LOG.warning(_LW('XML support has been removed as of the Kilo release ' - 'and should not be referenced or used in deployment. ' - 'Please remove references to XmlBodyMiddleware from ' - 'your configuration. This compatibility stub will be ' - 'removed in the L release')) - - def __init__(self, *args, **kwargs): - super(XmlBodyMiddleware, self).__init__(*args, **kwargs) - self.print_warning() - - -class XmlBodyMiddlewareV2(XmlBodyMiddleware): - """De/serialize XML to/from JSON for v2.0 API.""" - - def __init__(self, *args, **kwargs): - pass - - -class XmlBodyMiddlewareV3(XmlBodyMiddleware): - """De/serialize XML to/from JSON for v3 API.""" - - def __init__(self, *args, **kwargs): - pass - - class NormalizingFilter(wsgi.Middleware): """Middleware filter to handle URL normalization.""" diff --git a/keystone-moon/keystone/models/token_model.py b/keystone-moon/keystone/models/token_model.py index 3be22b96..2032fd19 100644 --- a/keystone-moon/keystone/models/token_model.py +++ b/keystone-moon/keystone/models/token_model.py @@ -17,7 +17,7 @@ from oslo_config import cfg from oslo_utils import timeutils import six -from keystone.contrib import federation +from keystone.contrib.federation import constants as federation_constants from keystone import exception from keystone.i18n import _ @@ -296,7 +296,8 @@ class KeystoneToken(dict): @property def is_federated_user(self): try: - return self.version is V3 and federation.FEDERATION in self['user'] + return (self.version is V3 and + federation_constants.FEDERATION in self['user']) except KeyError: raise exception.UnexpectedError() @@ -305,7 +306,7 @@ class KeystoneToken(dict): if self.is_federated_user: if self.version is V3: try: - groups = self['user'][federation.FEDERATION].get( + groups = self['user'][federation_constants.FEDERATION].get( 'groups', []) return [g['id'] for g in groups] except KeyError: @@ -316,12 +317,15 @@ class KeystoneToken(dict): def federation_idp_id(self): if self.version is not V3 or not self.is_federated_user: return None - return self['user'][federation.FEDERATION]['identity_provider']['id'] + return ( + self['user'][federation_constants.FEDERATION] + ['identity_provider']['id']) @property def federation_protocol_id(self): if self.version is V3 and self.is_federated_user: - return self['user'][federation.FEDERATION]['protocol']['id'] + return (self['user'][federation_constants.FEDERATION]['protocol'] + ['id']) return None @property diff --git a/keystone-moon/keystone/notifications.py b/keystone-moon/keystone/notifications.py index 4a1cd333..801dd737 100644 --- a/keystone-moon/keystone/notifications.py +++ b/keystone-moon/keystone/notifications.py @@ -15,12 +15,14 @@ """Notifications module for OpenStack Identity Service resources""" import collections +import functools import inspect import logging import socket from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils import oslo_messaging import pycadf from pycadf import cadftaxonomy as taxonomy @@ -36,12 +38,12 @@ notifier_opts = [ cfg.StrOpt('default_publisher_id', help='Default publisher_id for outgoing notifications'), cfg.StrOpt('notification_format', default='basic', + choices=['basic', 'cadf'], help='Define the notification format for Identity Service ' 'events. A "basic" notification has information about ' 'the resource being operated on. A "cadf" notification ' 'has the same information, as well as information about ' - 'the initiator of the event. Valid options are: basic ' - 'and cadf'), + 'the initiator of the event.'), ] config_section = None @@ -55,6 +57,7 @@ _ACTIONS = collections.namedtuple( 'created, deleted, disabled, updated, internal') ACTIONS = _ACTIONS(created='created', deleted='deleted', disabled='disabled', updated='updated', internal='internal') +"""The actions on resources.""" CADF_TYPE_MAP = { 'group': taxonomy.SECURITY_GROUP, @@ -291,6 +294,54 @@ def register_event_callback(event, resource_type, callbacks): LOG.debug(msg, {'callback': callback_str, 'event': event_str}) +def listener(cls): + """A class decorator to declare a class to be a notification listener. + + A notification listener must specify the event(s) it is interested in by + defining a ``event_callbacks`` attribute or property. ``event_callbacks`` + is a dictionary where the key is the type of event and the value is a + dictionary containing a mapping of resource types to callback(s). + + :data:`.ACTIONS` contains constants for the currently + supported events. There is currently no single place to find constants for + the resource types. + + Example:: + + @listener + class Something(object): + + def __init__(self): + self.event_callbacks = { + notifications.ACTIONS.created: { + 'user': self._user_created_callback, + }, + notifications.ACTIONS.deleted: { + 'project': [ + self._project_deleted_callback, + self._do_cleanup, + ] + }, + } + + """ + + def init_wrapper(init): + @functools.wraps(init) + def __new_init__(self, *args, **kwargs): + init(self, *args, **kwargs) + _register_event_callbacks(self) + return __new_init__ + + def _register_event_callbacks(self): + for event, resource_types in self.event_callbacks.items(): + for resource_type, callbacks in resource_types.items(): + register_event_callback(event, resource_type, callbacks) + + cls.__init__ = init_wrapper(cls.__init__) + return cls + + def notify_event_callbacks(service, resource_type, operation, payload): """Sends a notification to registered extensions.""" if operation in _SUBSCRIBERS: @@ -524,8 +575,10 @@ class CadfRoleAssignmentNotificationWrapper(object): def __init__(self, operation): self.action = '%s.%s' % (operation, self.ROLE_ASSIGNMENT) - self.event_type = '%s.%s.%s' % (SERVICE, operation, - self.ROLE_ASSIGNMENT) + self.deprecated_event_type = '%s.%s.%s' % (SERVICE, operation, + self.ROLE_ASSIGNMENT) + self.event_type = '%s.%s.%s' % (SERVICE, self.ROLE_ASSIGNMENT, + operation) def __call__(self, f): def wrapper(wrapped_self, role_id, *args, **kwargs): @@ -581,19 +634,30 @@ class CadfRoleAssignmentNotificationWrapper(object): audit_kwargs['inherited_to_projects'] = inherited audit_kwargs['role'] = role_id + # For backward compatibility, send both old and new event_type. + # Deprecate old format and remove it in the next release. + event_types = [self.deprecated_event_type, self.event_type] + versionutils.deprecated( + as_of=versionutils.deprecated.KILO, + remove_in=+1, + what=('sending duplicate %s notification event type' % + self.deprecated_event_type), + in_favor_of='%s notification event type' % self.event_type) try: result = f(wrapped_self, role_id, *args, **kwargs) except Exception: - _send_audit_notification(self.action, initiator, - taxonomy.OUTCOME_FAILURE, - target, self.event_type, - **audit_kwargs) + for event_type in event_types: + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_FAILURE, + target, event_type, + **audit_kwargs) raise else: - _send_audit_notification(self.action, initiator, - taxonomy.OUTCOME_SUCCESS, - target, self.event_type, - **audit_kwargs) + for event_type in event_types: + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_SUCCESS, + target, event_type, + **audit_kwargs) return result return wrapper diff --git a/keystone-moon/keystone/policy/core.py b/keystone-moon/keystone/policy/core.py index 1f02803f..7943b59e 100644 --- a/keystone-moon/keystone/policy/core.py +++ b/keystone-moon/keystone/policy/core.py @@ -36,6 +36,9 @@ class Manager(manager.Manager): dynamically calls the backend. """ + + driver_namespace = 'keystone.policy' + _POLICY = 'policy' def __init__(self): diff --git a/keystone-moon/keystone/resource/backends/ldap.py b/keystone-moon/keystone/resource/backends/ldap.py index 434c2b04..43684035 100644 --- a/keystone-moon/keystone/resource/backends/ldap.py +++ b/keystone-moon/keystone/resource/backends/ldap.py @@ -17,7 +17,7 @@ import uuid from oslo_config import cfg from oslo_log import log -from keystone import clean +from keystone.common import clean from keystone.common import driver_hints from keystone.common import ldap as common_ldap from keystone.common import models @@ -47,7 +47,7 @@ class Resource(resource.Driver): self.project = ProjectApi(CONF) def default_assignment_driver(self): - return 'keystone.assignment.backends.ldap.Assignment' + return 'ldap' def _set_default_parent_project(self, ref): """If the parent project ID has not been set, set it to None.""" @@ -60,6 +60,14 @@ class Resource(resource.Driver): else: raise ValueError(_('Expected dict or list: %s') % type(ref)) + def _set_default_is_domain_project(self, ref): + if isinstance(ref, dict): + return dict(ref, is_domain=False) + elif isinstance(ref, list): + return [self._set_default_is_domain_project(x) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + def _validate_parent_project_is_none(self, ref): """If a parent_id different from None was given, raises InvalidProjectException. @@ -69,8 +77,15 @@ class Resource(resource.Driver): if parent_id is not None: raise exception.InvalidParentProject(parent_id) + def _validate_is_domain_field_is_false(self, ref): + is_domain = ref.pop('is_domain', None) + if is_domain: + raise exception.ValidationError(_('LDAP does not support projects ' + 'with is_domain flag enabled')) + def _set_default_attributes(self, project_ref): project_ref = self._set_default_domain(project_ref) + project_ref = self._set_default_is_domain_project(project_ref) return self._set_default_parent_project(project_ref) def get_project(self, tenant_id): @@ -116,8 +131,8 @@ class Resource(resource.Driver): def create_project(self, tenant_id, tenant): self.project.check_allow_create() - tenant = self._validate_default_domain(tenant) self._validate_parent_project_is_none(tenant) + self._validate_is_domain_field_is_false(tenant) tenant['name'] = clean.project_name(tenant['name']) data = tenant.copy() if 'id' not in data or data['id'] is None: @@ -130,6 +145,7 @@ class Resource(resource.Driver): def update_project(self, tenant_id, tenant): self.project.check_allow_update() tenant = self._validate_default_domain(tenant) + self._validate_is_domain_field_is_false(tenant) if 'name' in tenant: tenant['name'] = clean.project_name(tenant['name']) return self._set_default_attributes( diff --git a/keystone-moon/keystone/resource/backends/sql.py b/keystone-moon/keystone/resource/backends/sql.py index fb117240..3a0d8cea 100644 --- a/keystone-moon/keystone/resource/backends/sql.py +++ b/keystone-moon/keystone/resource/backends/sql.py @@ -13,7 +13,7 @@ from oslo_config import cfg from oslo_log import log -from keystone import clean +from keystone.common import clean from keystone.common import sql from keystone import exception from keystone.i18n import _LE @@ -27,7 +27,7 @@ LOG = log.getLogger(__name__) class Resource(keystone_resource.Driver): def default_assignment_driver(self): - return 'keystone.assignment.backends.sql.Assignment' + return 'sql' def _get_project(self, session, project_id): project_ref = session.query(Project).get(project_id) @@ -91,10 +91,9 @@ class Resource(keystone_resource.Driver): def list_projects_in_subtree(self, project_id): with sql.transaction() as session: - project = self._get_project(session, project_id).to_dict() - children = self._get_children(session, [project['id']]) + children = self._get_children(session, [project_id]) subtree = [] - examined = set(project['id']) + examined = set([project_id]) while children: children_ids = set() for ref in children: @@ -106,7 +105,7 @@ class Resource(keystone_resource.Driver): return children_ids.add(ref['id']) - examined.union(children_ids) + examined.update(children_ids) subtree += children children = self._get_children(session, children_ids) return subtree @@ -246,7 +245,7 @@ class Domain(sql.ModelBase, sql.DictBase): class Project(sql.ModelBase, sql.DictBase): __tablename__ = 'project' attributes = ['id', 'name', 'domain_id', 'description', 'enabled', - 'parent_id'] + 'parent_id', 'is_domain'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), nullable=False) domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), @@ -255,6 +254,7 @@ class Project(sql.ModelBase, sql.DictBase): enabled = sql.Column(sql.Boolean) extra = sql.Column(sql.JsonBlob()) parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id')) + is_domain = sql.Column(sql.Boolean, default=False, nullable=False) # Unique constraint across two columns to create the separation # rather than just only 'name' being unique __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {}) diff --git a/keystone-moon/keystone/resource/controllers.py b/keystone-moon/keystone/resource/controllers.py index 886b5eb1..60c4e025 100644 --- a/keystone-moon/keystone/resource/controllers.py +++ b/keystone-moon/keystone/resource/controllers.py @@ -47,27 +47,37 @@ class Tenant(controller.V2Controller): self.assert_admin(context) tenant_refs = self.resource_api.list_projects_in_domain( CONF.identity.default_domain_id) - for tenant_ref in tenant_refs: - tenant_ref = self.filter_domain_id(tenant_ref) + tenant_refs = [self.v3_to_v2_project(tenant_ref) + for tenant_ref in tenant_refs + if not tenant_ref.get('is_domain')] params = { 'limit': context['query_string'].get('limit'), 'marker': context['query_string'].get('marker'), } return self.format_project_list(tenant_refs, **params) + def _assert_not_is_domain_project(self, project_id, project_ref=None): + # Projects acting as a domain should not be visible via v2 + if not project_ref: + project_ref = self.resource_api.get_project(project_id) + if project_ref.get('is_domain'): + raise exception.ProjectNotFound(project_id) + @controller.v2_deprecated def get_project(self, context, tenant_id): # TODO(termie): this stuff should probably be moved to middleware self.assert_admin(context) ref = self.resource_api.get_project(tenant_id) - return {'tenant': self.filter_domain_id(ref)} + self._assert_not_is_domain_project(tenant_id, ref) + return {'tenant': self.v3_to_v2_project(ref)} @controller.v2_deprecated def get_project_by_name(self, context, tenant_name): self.assert_admin(context) + # Projects acting as a domain should not be visible via v2 ref = self.resource_api.get_project_by_name( tenant_name, CONF.identity.default_domain_id) - return {'tenant': self.filter_domain_id(ref)} + return {'tenant': self.v3_to_v2_project(ref)} # CRUD Extension @controller.v2_deprecated @@ -83,23 +93,25 @@ class Tenant(controller.V2Controller): tenant = self.resource_api.create_project( tenant_ref['id'], self._normalize_domain_id(context, tenant_ref)) - return {'tenant': self.filter_domain_id(tenant)} + return {'tenant': self.v3_to_v2_project(tenant)} @controller.v2_deprecated def update_project(self, context, tenant_id, tenant): self.assert_admin(context) - # Remove domain_id if specified - a v2 api caller should not - # be specifying that + self._assert_not_is_domain_project(tenant_id) + # Remove domain_id and is_domain if specified - a v2 api caller + # should not be specifying that clean_tenant = tenant.copy() clean_tenant.pop('domain_id', None) - + clean_tenant.pop('is_domain', None) tenant_ref = self.resource_api.update_project( tenant_id, clean_tenant) - return {'tenant': tenant_ref} + return {'tenant': self.v3_to_v2_project(tenant_ref)} @controller.v2_deprecated def delete_project(self, context, tenant_id): self.assert_admin(context) + self._assert_not_is_domain_project(tenant_id) self.resource_api.delete_project(tenant_id) @@ -201,9 +213,18 @@ class ProjectV3(controller.V3Controller): def create_project(self, context, project): ref = self._assign_unique_id(self._normalize_dict(project)) ref = self._normalize_domain_id(context, ref) + + if ref.get('is_domain'): + msg = _('The creation of projects acting as domains is not ' + 'allowed yet.') + raise exception.NotImplemented(msg) + initiator = notifications._get_request_audit_info(context) - ref = self.resource_api.create_project(ref['id'], ref, - initiator=initiator) + try: + ref = self.resource_api.create_project(ref['id'], ref, + initiator=initiator) + except exception.DomainNotFound as e: + raise exception.ValidationError(e) return ProjectV3.wrap_member(context, ref) @controller.filterprotected('domain_id', 'enabled', 'name', diff --git a/keystone-moon/keystone/resource/core.py b/keystone-moon/keystone/resource/core.py index 017eb4e7..ca69b729 100644 --- a/keystone-moon/keystone/resource/core.py +++ b/keystone-moon/keystone/resource/core.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Main entry point into the resource service.""" +"""Main entry point into the Resource service.""" import abc @@ -18,12 +18,11 @@ from oslo_config import cfg from oslo_log import log import six -from keystone import clean from keystone.common import cache +from keystone.common import clean from keystone.common import dependency from keystone.common import driver_hints from keystone.common import manager -from keystone.contrib import federation from keystone import exception from keystone.i18n import _, _LE, _LW from keystone import notifications @@ -47,12 +46,15 @@ def calc_default_domain(): @dependency.requires('assignment_api', 'credential_api', 'domain_config_api', 'identity_api', 'revoke_api') class Manager(manager.Manager): - """Default pivot point for the resource backend. + """Default pivot point for the Resource backend. See :mod:`keystone.common.manager.Manager` for more details on how this dynamically calls the backend. """ + + driver_namespace = 'keystone.resource' + _DOMAIN = 'domain' _PROJECT = 'project' @@ -62,9 +64,8 @@ class Manager(manager.Manager): resource_driver = CONF.resource.driver if resource_driver is None: - assignment_driver = ( - dependency.get_provider('assignment_api').driver) - resource_driver = assignment_driver.default_resource_driver() + assignment_manager = dependency.get_provider('assignment_api') + resource_driver = assignment_manager.default_resource_driver() super(Manager, self).__init__(resource_driver) @@ -86,21 +87,23 @@ class Manager(manager.Manager): tenant['enabled'] = clean.project_enabled(tenant['enabled']) tenant.setdefault('description', '') tenant.setdefault('parent_id', None) + tenant.setdefault('is_domain', False) + self.get_domain(tenant.get('domain_id')) if tenant.get('parent_id') is not None: parent_ref = self.get_project(tenant.get('parent_id')) parents_list = self.list_project_parents(parent_ref['id']) parents_list.append(parent_ref) for ref in parents_list: if ref.get('domain_id') != tenant.get('domain_id'): - raise exception.ForbiddenAction( - action=_('cannot create a project within a different ' - 'domain than its parents.')) + raise exception.ValidationError( + message=_('cannot create a project within a different ' + 'domain than its parents.')) if not ref.get('enabled', True): - raise exception.ForbiddenAction( - action=_('cannot create a project in a ' - 'branch containing a disabled ' - 'project: %s') % ref['id']) + raise exception.ValidationError( + message=_('cannot create a project in a ' + 'branch containing a disabled ' + 'project: %s') % ref['id']) self._assert_max_hierarchy_depth(tenant.get('parent_id'), parents_list) @@ -135,14 +138,13 @@ class Manager(manager.Manager): """ # NOTE(marek-denis): We cannot create this attribute in the __init__ as # config values are always initialized to default value. - federated_domain = (CONF.federation.federated_domain_name or - federation.FEDERATED_DOMAIN_KEYWORD).lower() + federated_domain = CONF.federation.federated_domain_name.lower() if (domain.get('name') and domain['name'].lower() == federated_domain): raise AssertionError(_('Domain cannot be named %s') - % federated_domain) + % domain['name']) if (domain_id.lower() == federated_domain): raise AssertionError(_('Domain cannot have ID %s') - % federated_domain) + % domain_id) def assert_project_enabled(self, project_id, project=None): """Assert the project is enabled and its associated domain is enabled. @@ -177,7 +179,7 @@ class Manager(manager.Manager): 'disabled parents') % project_id) def _assert_whole_subtree_is_disabled(self, project_id): - subtree_list = self.driver.list_projects_in_subtree(project_id) + subtree_list = self.list_projects_in_subtree(project_id) for ref in subtree_list: if ref.get('enabled', True): raise exception.ForbiddenAction( @@ -194,6 +196,11 @@ class Manager(manager.Manager): raise exception.ForbiddenAction( action=_('Update of `parent_id` is not allowed.')) + if ('is_domain' in tenant and + tenant['is_domain'] != original_tenant['is_domain']): + raise exception.ValidationError( + message=_('Update of `is_domain` is not allowed.')) + if 'enabled' in tenant: tenant['enabled'] = clean.project_enabled(tenant['enabled']) @@ -241,15 +248,23 @@ class Manager(manager.Manager): user_projects = self.assignment_api.list_projects_for_user(user_id) user_projects_ids = set([proj['id'] for proj in user_projects]) # Keep only the projects present in user_projects - projects_list = [proj for proj in projects_list - if proj['id'] in user_projects_ids] + return [proj for proj in projects_list + if proj['id'] in user_projects_ids] + + def _assert_valid_project_id(self, project_id): + if project_id is None: + msg = _('Project field is required and cannot be empty.') + raise exception.ValidationError(message=msg) + # Check if project_id exists + self.get_project(project_id) def list_project_parents(self, project_id, user_id=None): + self._assert_valid_project_id(project_id) parents = self.driver.list_project_parents(project_id) # If a user_id was provided, the returned list should be filtered # against the projects this user has access to. if user_id: - self._filter_projects_list(parents, user_id) + parents = self._filter_projects_list(parents, user_id) return parents def _build_parents_as_ids_dict(self, project, parents_by_id): @@ -296,11 +311,12 @@ class Manager(manager.Manager): return parents_as_ids def list_projects_in_subtree(self, project_id, user_id=None): + self._assert_valid_project_id(project_id) subtree = self.driver.list_projects_in_subtree(project_id) # If a user_id was provided, the returned list should be filtered # against the projects this user has access to. if user_id: - self._filter_projects_list(subtree, user_id) + subtree = self._filter_projects_list(subtree, user_id) return subtree def _build_subtree_as_ids_dict(self, project_id, subtree_by_parent): @@ -780,6 +796,9 @@ class Driver(object): raise exception.DomainNotFound(domain_id=domain_id) +MEMOIZE_CONFIG = cache.get_memoization_decorator(section='domain_config') + + @dependency.provider('domain_config_api') class DomainConfigManager(manager.Manager): """Default pivot point for the Domain Config backend.""" @@ -793,6 +812,8 @@ class DomainConfigManager(manager.Manager): # Only those options that affect the domain-specific driver support in # the identity manager are supported. + driver_namespace = 'keystone.resource.domain_config' + whitelisted_options = { 'identity': ['driver'], 'ldap': [ @@ -975,6 +996,10 @@ class DomainConfigManager(manager.Manager): self.create_config_option( domain_id, option['group'], option['option'], option['value'], sensitive=True) + # Since we are caching on the full substituted config, we just + # invalidate here, rather than try and create the right result to + # cache. + self.get_config_with_sensitive_info.invalidate(self, domain_id) return self._list_to_config(whitelisted) def get_config(self, domain_id, group=None, option=None): @@ -999,7 +1024,7 @@ class DomainConfigManager(manager.Manager): 'url': 'myurl' 'user_tree_dn': 'OU=myou'}, 'identity': { - 'driver': 'keystone.identity.backends.ldap.Identity'} + 'driver': 'ldap'} } @@ -1077,22 +1102,22 @@ class DomainConfigManager(manager.Manager): 'provided contains group %(group_other)s ' 'instead') % { 'group': group, - 'group_other': config.keys()[0]} + 'group_other': list(config.keys())[0]} raise exception.InvalidDomainConfig(reason=msg) if option and option not in config[group]: msg = _('Trying to update option %(option)s in group ' '%(group)s, but config provided contains option ' '%(option_other)s instead') % { 'group': group, 'option': option, - 'option_other': config[group].keys()[0]} + 'option_other': list(config[group].keys())[0]} raise exception.InvalidDomainConfig(reason=msg) # Finally, we need to check if the group/option specified # already exists in the original config - since if not, to keep # with the semantics of an update, we need to fail with # a DomainConfigNotFound - if not self.get_config_with_sensitive_info(domain_id, - group, option): + if not self._get_config_with_sensitive_info(domain_id, + group, option): if option: msg = _('option %(option)s in group %(group)s') % { 'group': group, 'option': option} @@ -1131,6 +1156,7 @@ class DomainConfigManager(manager.Manager): for new_option in sensitive: _update_or_create(domain_id, new_option, sensitive=True) + self.get_config_with_sensitive_info.invalidate(self, domain_id) return self.get_config(domain_id) def delete_config(self, domain_id, group=None, option=None): @@ -1154,7 +1180,7 @@ class DomainConfigManager(manager.Manager): if group: # As this is a partial delete, then make sure the items requested # are valid and exist in the current config - current_config = self.get_config_with_sensitive_info(domain_id) + current_config = self._get_config_with_sensitive_info(domain_id) # Raise an exception if the group/options specified don't exist in # the current config so that the delete method provides the # correct error semantics. @@ -1171,14 +1197,14 @@ class DomainConfigManager(manager.Manager): self.delete_config_options(domain_id, group, option) self.delete_config_options(domain_id, group, option, sensitive=True) + self.get_config_with_sensitive_info.invalidate(self, domain_id) - def get_config_with_sensitive_info(self, domain_id, group=None, - option=None): - """Get config for a domain with sensitive info included. + def _get_config_with_sensitive_info(self, domain_id, group=None, + option=None): + """Get config for a domain/group/option with sensitive info included. - This method is not exposed via the public API, but is used by the - identity manager to initialize a domain with the fully formed config - options. + This is only used by the methods within this class, which may need to + check individual groups or options. """ whitelisted = self.list_config_options(domain_id, group, option) @@ -1233,6 +1259,17 @@ class DomainConfigManager(manager.Manager): return self._list_to_config(whitelisted, sensitive) + @MEMOIZE_CONFIG + def get_config_with_sensitive_info(self, domain_id): + """Get config for a domain with sensitive info included. + + This method is not exposed via the public API, but is used by the + identity manager to initialize a domain with the fully formed config + options. + + """ + return self._get_config_with_sensitive_info(domain_id) + @six.add_metaclass(abc.ABCMeta) class DomainConfigDriver(object): diff --git a/keystone-moon/keystone/resource/schema.py b/keystone-moon/keystone/resource/schema.py index 0fd59e3f..e26a9c4a 100644 --- a/keystone-moon/keystone/resource/schema.py +++ b/keystone-moon/keystone/resource/schema.py @@ -21,6 +21,7 @@ _project_properties = { # implementation. 'domain_id': parameter_types.id_string, 'enabled': parameter_types.boolean, + 'is_domain': parameter_types.boolean, 'parent_id': validation.nullable(parameter_types.id_string), 'name': { 'type': 'string', diff --git a/keystone-moon/keystone/server/backends.py b/keystone-moon/keystone/server/backends.py new file mode 100644 index 00000000..ebe00a81 --- /dev/null +++ b/keystone-moon/keystone/server/backends.py @@ -0,0 +1,64 @@ +# 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 import assignment +from keystone import auth +from keystone import catalog +from keystone.common import cache +from keystone.contrib import endpoint_filter +from keystone.contrib import federation +from keystone.contrib import oauth1 +from keystone.contrib import revoke +from keystone import credential +from keystone import endpoint_policy +from keystone import identity +from keystone import policy +from keystone import resource +from keystone import token +from keystone import trust + + +def load_backends(): + + # Configure and build the cache + cache.configure_cache_region(cache.REGION) + + # Ensure that the identity driver is created before the assignment manager + # and that the assignment driver is created before the resource manager. + # The default resource driver depends on assignment, which in turn + # depends on identity - hence we need to ensure the chain is available. + _IDENTITY_API = identity.Manager() + _ASSIGNMENT_API = assignment.Manager() + + DRIVERS = dict( + assignment_api=_ASSIGNMENT_API, + catalog_api=catalog.Manager(), + credential_api=credential.Manager(), + domain_config_api=resource.DomainConfigManager(), + endpoint_filter_api=endpoint_filter.Manager(), + endpoint_policy_api=endpoint_policy.Manager(), + federation_api=federation.Manager(), + id_generator_api=identity.generator.Manager(), + id_mapping_api=identity.MappingManager(), + identity_api=_IDENTITY_API, + oauth_api=oauth1.Manager(), + policy_api=policy.Manager(), + resource_api=resource.Manager(), + revoke_api=revoke.Manager(), + role_api=assignment.RoleManager(), + token_api=token.persistence.Manager(), + trust_api=trust.Manager(), + token_provider_api=token.provider.Manager()) + + auth.controllers.load_auth_methods() + + return DRIVERS diff --git a/keystone-moon/keystone/server/common.py b/keystone-moon/keystone/server/common.py index fda44eea..2de6d39e 100644 --- a/keystone-moon/keystone/server/common.py +++ b/keystone-moon/keystone/server/common.py @@ -14,10 +14,10 @@ from oslo_config import cfg -from keystone import backends from keystone.common import dependency from keystone.common import sql from keystone import config +from keystone.server import backends CONF = cfg.CONF diff --git a/keystone-moon/keystone/server/eventlet.py b/keystone-moon/keystone/server/eventlet.py index 5bedaf9b..243f0234 100644 --- a/keystone-moon/keystone/server/eventlet.py +++ b/keystone-moon/keystone/server/eventlet.py @@ -20,6 +20,8 @@ import socket from oslo_concurrency import processutils from oslo_config import cfg import oslo_i18n +from oslo_service import service +from oslo_service import systemd import pbr.version @@ -34,8 +36,6 @@ from keystone.common import environment from keystone.common import utils from keystone import config from keystone.i18n import _ -from keystone.openstack.common import service -from keystone.openstack.common import systemd from keystone.server import common from keystone import service as keystone_service @@ -79,9 +79,9 @@ def serve(*servers): 'Support for keystone under eventlet will be removed in ' 'the "M"-Release.')) if max([server[1].workers for server in servers]) > 1: - launcher = service.ProcessLauncher() + launcher = service.ProcessLauncher(CONF) else: - launcher = service.ServiceLauncher() + launcher = service.ServiceLauncher(CONF) for name, server in servers: try: diff --git a/keystone-moon/keystone/server/wsgi.py b/keystone-moon/keystone/server/wsgi.py index 863f13bc..dbdad326 100644 --- a/keystone-moon/keystone/server/wsgi.py +++ b/keystone-moon/keystone/server/wsgi.py @@ -50,3 +50,11 @@ def initialize_application(name): _unused, application = common.setup_backends( startup_application_fn=loadapp) return application + + +def initialize_admin_application(): + return initialize_application('admin') + + +def initialize_public_application(): + return initialize_application('main') diff --git a/keystone-moon/keystone/service.py b/keystone-moon/keystone/service.py index e9a0748e..35b548fa 100644 --- a/keystone-moon/keystone/service.py +++ b/keystone-moon/keystone/service.py @@ -26,13 +26,14 @@ from keystone import catalog from keystone.common import wsgi from keystone import controllers from keystone import credential +from keystone import endpoint_policy from keystone import identity from keystone import policy from keystone import resource from keystone import routers from keystone import token from keystone import trust -from keystone.contrib import moon as authz + CONF = cfg.CONF LOG = log.getLogger(__name__) @@ -103,11 +104,23 @@ def v3_app_factory(global_conf, **local_conf): sub_routers = [] _routers = [] - router_modules = [assignment, auth, catalog, credential, identity, policy, - resource, authz] + # NOTE(dstanek): Routers should be ordered by their frequency of use in + # a live system. This is due to the routes implementation. The most + # frequently used routers should appear first. + router_modules = [auth, + assignment, + catalog, + credential, + identity, + policy, + resource] + if CONF.trust.enabled: router_modules.append(trust) + if CONF.endpoint_policy.enabled: + router_modules.append(endpoint_policy) + for module in router_modules: routers_instance = module.routers.Routers() _routers.append(routers_instance) diff --git a/keystone-moon/keystone/tests/functional/__init__.py b/keystone-moon/keystone/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/tests/functional/__init__.py diff --git a/keystone-moon/keystone/tests/functional/shared/__init__.py b/keystone-moon/keystone/tests/functional/shared/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/tests/functional/shared/__init__.py diff --git a/keystone-moon/keystone/tests/functional/shared/test_running.py b/keystone-moon/keystone/tests/functional/shared/test_running.py new file mode 100644 index 00000000..aed48ac2 --- /dev/null +++ b/keystone-moon/keystone/tests/functional/shared/test_running.py @@ -0,0 +1,50 @@ +# 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 requests +import testtools.matchers + + +is_multiple_choices = testtools.matchers.Equals( + requests.status_codes.codes.multiple_choices) +is_ok = testtools.matchers.Equals(requests.status_codes.codes.ok) + +admin_url = 'http://localhost:35357' +public_url = 'http://localhost:5000' +versions = ('v2.0', 'v3') + + +class TestServerRunning(testtools.TestCase): + + def test_admin_responds_with_multiple_choices(self): + resp = requests.get(admin_url) + self.assertThat(resp.status_code, is_multiple_choices) + + def test_admin_versions(self): + for version in versions: + resp = requests.get(admin_url + '/' + version) + self.assertThat( + resp.status_code, + testtools.matchers.Annotate( + 'failed for version %s' % version, is_ok)) + + def test_public_responds_with_multiple_choices(self): + resp = requests.get(public_url) + self.assertThat(resp.status_code, is_multiple_choices) + + def test_public_versions(self): + for version in versions: + resp = requests.get(public_url + '/' + version) + self.assertThat( + resp.status_code, + testtools.matchers.Annotate( + 'failed for version %s' % version, is_ok)) diff --git a/keystone-moon/keystone/tests/hacking/__init__.py b/keystone-moon/keystone/tests/hacking/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/tests/hacking/__init__.py diff --git a/keystone-moon/keystone/tests/hacking/checks.py b/keystone-moon/keystone/tests/hacking/checks.py new file mode 100644 index 00000000..17bafff3 --- /dev/null +++ b/keystone-moon/keystone/tests/hacking/checks.py @@ -0,0 +1,434 @@ +# 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. + +"""Keystone's pep8 extensions. + +In order to make the review process faster and easier for core devs we are +adding some Keystone specific pep8 checks. This will catch common errors +so that core devs don't have to. + +There are two types of pep8 extensions. One is a function that takes either +a physical or logical line. The physical or logical line is the first param +in the function definition and can be followed by other parameters supported +by pep8. The second type is a class that parses AST trees. For more info +please see pep8.py. +""" + +import ast +import re + +import six + + +class BaseASTChecker(ast.NodeVisitor): + """Provides a simple framework for writing AST-based checks. + + Subclasses should implement visit_* methods like any other AST visitor + implementation. When they detect an error for a particular node the + method should call ``self.add_error(offending_node)``. Details about + where in the code the error occurred will be pulled from the node + object. + + Subclasses should also provide a class variable named CHECK_DESC to + be used for the human readable error message. + + """ + + def __init__(self, tree, filename): + """This object is created automatically by pep8. + + :param tree: an AST tree + :param filename: name of the file being analyzed + (ignored by our checks) + """ + self._tree = tree + self._errors = [] + + def run(self): + """Called automatically by pep8.""" + self.visit(self._tree) + return self._errors + + def add_error(self, node, message=None): + """Add an error caused by a node to the list of errors for pep8.""" + message = message or self.CHECK_DESC + error = (node.lineno, node.col_offset, message, self.__class__) + self._errors.append(error) + + +class CheckForMutableDefaultArgs(BaseASTChecker): + """Checks for the use of mutable objects as function/method defaults. + + We are only checking for list and dict literals at this time. This means + that a developer could specify an instance of their own and cause a bug. + The fix for this is probably more work than it's worth because it will + get caught during code review. + + """ + + CHECK_DESC = 'K001 Using mutable as a function/method default' + MUTABLES = ( + ast.List, ast.ListComp, + ast.Dict, ast.DictComp, + ast.Set, ast.SetComp, + ast.Call) + + def visit_FunctionDef(self, node): + for arg in node.args.defaults: + if isinstance(arg, self.MUTABLES): + self.add_error(arg) + + super(CheckForMutableDefaultArgs, self).generic_visit(node) + + +def block_comments_begin_with_a_space(physical_line, line_number): + """There should be a space after the # of block comments. + + There is already a check in pep8 that enforces this rule for + inline comments. + + Okay: # this is a comment + Okay: #!/usr/bin/python + Okay: # this is a comment + K002: #this is a comment + + """ + MESSAGE = "K002 block comments should start with '# '" + + # shebangs are OK + if line_number == 1 and physical_line.startswith('#!'): + return + + text = physical_line.strip() + if text.startswith('#'): # look for block comments + if len(text) > 1 and not text[1].isspace(): + return physical_line.index('#'), MESSAGE + + +class CheckForAssertingNoneEquality(BaseASTChecker): + """Ensures that code does not use a None with assert(Not*)Equal.""" + + CHECK_DESC_IS = ('K003 Use self.assertIsNone(...) when comparing ' + 'against None') + CHECK_DESC_ISNOT = ('K004 Use assertIsNotNone(...) when comparing ' + ' against None') + + def visit_Call(self, node): + # NOTE(dstanek): I wrote this in a verbose way to make it easier to + # read for those that have little experience with Python's AST. + + if isinstance(node.func, ast.Attribute): + if node.func.attr == 'assertEqual': + for arg in node.args: + if isinstance(arg, ast.Name) and arg.id == 'None': + self.add_error(node, message=self.CHECK_DESC_IS) + elif node.func.attr == 'assertNotEqual': + for arg in node.args: + if isinstance(arg, ast.Name) and arg.id == 'None': + self.add_error(node, message=self.CHECK_DESC_ISNOT) + + super(CheckForAssertingNoneEquality, self).generic_visit(node) + + +class CheckForLoggingIssues(BaseASTChecker): + + DEBUG_CHECK_DESC = 'K005 Using translated string in debug logging' + NONDEBUG_CHECK_DESC = 'K006 Not using translating helper for logging' + EXCESS_HELPER_CHECK_DESC = 'K007 Using hints when _ is necessary' + LOG_MODULES = ('logging', 'oslo_log.log') + I18N_MODULES = ( + 'keystone.i18n._', + 'keystone.i18n._LI', + 'keystone.i18n._LW', + 'keystone.i18n._LE', + 'keystone.i18n._LC', + ) + TRANS_HELPER_MAP = { + 'debug': None, + 'info': '_LI', + 'warn': '_LW', + 'warning': '_LW', + 'error': '_LE', + 'exception': '_LE', + 'critical': '_LC', + } + + def __init__(self, tree, filename): + super(CheckForLoggingIssues, self).__init__(tree, filename) + + self.logger_names = [] + self.logger_module_names = [] + self.i18n_names = {} + + # NOTE(dstanek): this kinda accounts for scopes when talking + # about only leaf node in the graph + self.assignments = {} + + def generic_visit(self, node): + """Called if no explicit visitor function exists for a node.""" + for field, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if isinstance(item, ast.AST): + item._parent = node + self.visit(item) + elif isinstance(value, ast.AST): + value._parent = node + self.visit(value) + + def _filter_imports(self, module_name, alias): + """Keeps lists of logging and i18n imports + + """ + if module_name in self.LOG_MODULES: + self.logger_module_names.append(alias.asname or alias.name) + elif module_name in self.I18N_MODULES: + self.i18n_names[alias.asname or alias.name] = alias.name + + def visit_Import(self, node): + for alias in node.names: + self._filter_imports(alias.name, alias) + return super(CheckForLoggingIssues, self).generic_visit(node) + + def visit_ImportFrom(self, node): + for alias in node.names: + full_name = '%s.%s' % (node.module, alias.name) + self._filter_imports(full_name, alias) + return super(CheckForLoggingIssues, self).generic_visit(node) + + def _find_name(self, node): + """Return the fully qualified name or a Name or Attribute.""" + if isinstance(node, ast.Name): + return node.id + elif (isinstance(node, ast.Attribute) + and isinstance(node.value, (ast.Name, ast.Attribute))): + method_name = node.attr + obj_name = self._find_name(node.value) + if obj_name is None: + return None + return obj_name + '.' + method_name + elif isinstance(node, six.string_types): + return node + else: # could be Subscript, Call or many more + return None + + def visit_Assign(self, node): + """Look for 'LOG = logging.getLogger' + + This handles the simple case: + name = [logging_module].getLogger(...) + + - or - + + name = [i18n_name](...) + + And some much more comple ones: + name = [i18n_name](...) % X + + - or - + + self.name = [i18n_name](...) % X + + """ + attr_node_types = (ast.Name, ast.Attribute) + + if (len(node.targets) != 1 + or not isinstance(node.targets[0], attr_node_types)): + # say no to: "x, y = ..." + return super(CheckForLoggingIssues, self).generic_visit(node) + + target_name = self._find_name(node.targets[0]) + + if (isinstance(node.value, ast.BinOp) and + isinstance(node.value.op, ast.Mod)): + if (isinstance(node.value.left, ast.Call) and + isinstance(node.value.left.func, ast.Name) and + node.value.left.func.id in self.i18n_names): + # NOTE(dstanek): this is done to match cases like: + # `msg = _('something %s') % x` + node = ast.Assign(value=node.value.left) + + if not isinstance(node.value, ast.Call): + # node.value must be a call to getLogger + self.assignments.pop(target_name, None) + return super(CheckForLoggingIssues, self).generic_visit(node) + + # is this a call to an i18n function? + if (isinstance(node.value.func, ast.Name) + and node.value.func.id in self.i18n_names): + self.assignments[target_name] = node.value.func.id + return super(CheckForLoggingIssues, self).generic_visit(node) + + if (not isinstance(node.value.func, ast.Attribute) + or not isinstance(node.value.func.value, attr_node_types)): + # function must be an attribute on an object like + # logging.getLogger + return super(CheckForLoggingIssues, self).generic_visit(node) + + object_name = self._find_name(node.value.func.value) + func_name = node.value.func.attr + + if (object_name in self.logger_module_names + and func_name == 'getLogger'): + self.logger_names.append(target_name) + + return super(CheckForLoggingIssues, self).generic_visit(node) + + def visit_Call(self, node): + """Look for the 'LOG.*' calls. + + """ + + # obj.method + if isinstance(node.func, ast.Attribute): + obj_name = self._find_name(node.func.value) + if isinstance(node.func.value, ast.Name): + method_name = node.func.attr + elif isinstance(node.func.value, ast.Attribute): + obj_name = self._find_name(node.func.value) + method_name = node.func.attr + else: # could be Subscript, Call or many more + return super(CheckForLoggingIssues, self).generic_visit(node) + + # must be a logger instance and one of the support logging methods + if (obj_name not in self.logger_names + or method_name not in self.TRANS_HELPER_MAP): + return super(CheckForLoggingIssues, self).generic_visit(node) + + # the call must have arguments + if not len(node.args): + return super(CheckForLoggingIssues, self).generic_visit(node) + + if method_name == 'debug': + self._process_debug(node) + elif method_name in self.TRANS_HELPER_MAP: + self._process_non_debug(node, method_name) + + return super(CheckForLoggingIssues, self).generic_visit(node) + + def _process_debug(self, node): + msg = node.args[0] # first arg to a logging method is the msg + + # if first arg is a call to a i18n name + if (isinstance(msg, ast.Call) + and isinstance(msg.func, ast.Name) + and msg.func.id in self.i18n_names): + self.add_error(msg, message=self.DEBUG_CHECK_DESC) + + # if the first arg is a reference to a i18n call + elif (isinstance(msg, ast.Name) + and msg.id in self.assignments + and not self._is_raised_later(node, msg.id)): + self.add_error(msg, message=self.DEBUG_CHECK_DESC) + + def _process_non_debug(self, node, method_name): + msg = node.args[0] # first arg to a logging method is the msg + + # if first arg is a call to a i18n name + if isinstance(msg, ast.Call): + try: + func_name = msg.func.id + except AttributeError: + # in the case of logging only an exception, the msg function + # will not have an id associated with it, for instance: + # LOG.warning(six.text_type(e)) + return + + # the function name is the correct translation helper + # for the logging method + if func_name == self.TRANS_HELPER_MAP[method_name]: + return + + # the function name is an alias for the correct translation + # helper for the loggine method + if (self.i18n_names[func_name] == + self.TRANS_HELPER_MAP[method_name]): + return + + self.add_error(msg, message=self.NONDEBUG_CHECK_DESC) + + # if the first arg is not a reference to the correct i18n hint + elif isinstance(msg, ast.Name): + + # FIXME(dstanek): to make sure more robust we should be checking + # all names passed into a logging method. we can't right now + # because: + # 1. We have code like this that we'll fix when dealing with the %: + # msg = _('....') % {} + # LOG.warn(msg) + # 2. We also do LOG.exception(e) in several places. I'm not sure + # exactly what we should be doing about that. + if msg.id not in self.assignments: + return + + helper_method_name = self.TRANS_HELPER_MAP[method_name] + if (self.assignments[msg.id] != helper_method_name + and not self._is_raised_later(node, msg.id)): + self.add_error(msg, message=self.NONDEBUG_CHECK_DESC) + elif (self.assignments[msg.id] == helper_method_name + and self._is_raised_later(node, msg.id)): + self.add_error(msg, message=self.EXCESS_HELPER_CHECK_DESC) + + def _is_raised_later(self, node, name): + + def find_peers(node): + node_for_line = node._parent + for _field, value in ast.iter_fields(node._parent._parent): + if isinstance(value, list) and node_for_line in value: + return value[value.index(node_for_line) + 1:] + continue + return [] + + peers = find_peers(node) + for peer in peers: + if isinstance(peer, ast.Raise): + if (isinstance(peer.type, ast.Call) and + len(peer.type.args) > 0 and + isinstance(peer.type.args[0], ast.Name) and + name in (a.id for a in peer.type.args)): + return True + else: + return False + elif isinstance(peer, ast.Assign): + if name in (t.id for t in peer.targets): + return False + + +def dict_constructor_with_sequence_copy(logical_line): + """Should use a dict comprehension instead of a dict constructor. + + PEP-0274 introduced dict comprehension with performance enhancement + and it also makes code more readable. + + Okay: lower_res = {k.lower(): v for k, v in six.iteritems(res[1])} + Okay: fool = dict(a='a', b='b') + K008: lower_res = dict((k.lower(), v) for k, v in six.iteritems(res[1])) + K008: attrs = dict([(k, _from_json(v)) + K008: dict([[i,i] for i in range(3)]) + + """ + MESSAGE = ("K008 Must use a dict comprehension instead of a dict" + " constructor with a sequence of key-value pairs.") + + dict_constructor_with_sequence_re = ( + re.compile(r".*\bdict\((\[)?(\(|\[)(?!\{)")) + + if dict_constructor_with_sequence_re.match(logical_line): + yield (0, MESSAGE) + + +def factory(register): + register(CheckForMutableDefaultArgs) + register(block_comments_begin_with_a_space) + register(CheckForAssertingNoneEquality) + register(CheckForLoggingIssues) + register(dict_constructor_with_sequence_copy) diff --git a/keystone-moon/keystone/tests/moon/unit/test_unit_core_configuration.py b/keystone-moon/keystone/tests/moon/unit/test_unit_core_configuration.py index 0be52c18..83606ff3 100644 --- a/keystone-moon/keystone/tests/moon/unit/test_unit_core_configuration.py +++ b/keystone-moon/keystone/tests/moon/unit/test_unit_core_configuration.py @@ -14,12 +14,13 @@ from keystone.contrib.moon.exception import * from keystone.tests.unit import default_fixtures from keystone.contrib.moon.core import LogManager from keystone.contrib.moon.core import IntraExtensionAdminManager +from keystone.contrib.moon.core import IntraExtensionRootManager from keystone.tests.moon.unit import * CONF = cfg.CONF -@dependency.requires('admin_api', 'authz_api', 'tenant_api', 'configuration_api', 'moonlog_api') +# @dependency.requires('admin_api', 'authz_api', 'tenant_api', 'configuration_api', 'moonlog_api') class TestConfigurationManager(tests.TestCase): def setUp(self): @@ -41,7 +42,8 @@ class TestConfigurationManager(tests.TestCase): def load_extra_backends(self): return { "moonlog_api": LogManager(), - "admin_api": IntraExtensionAdminManager() + "admin_api": IntraExtensionAdminManager(), + "root_api": IntraExtensionRootManager() } def config_overrides(self): diff --git a/keystone-moon/keystone/tests/unit/__init__.py b/keystone-moon/keystone/tests/unit/__init__.py index c97ce253..837afe69 100644 --- a/keystone-moon/keystone/tests/unit/__init__.py +++ b/keystone-moon/keystone/tests/unit/__init__.py @@ -25,11 +25,9 @@ if six.PY3: import sys from unittest import mock # noqa: our import detection is naive? - sys.modules['eventlet'] = mock.Mock() - sys.modules['eventlet.green'] = mock.Mock() - sys.modules['eventlet.wsgi'] = mock.Mock() - sys.modules['oslo'].messaging = mock.Mock() - sys.modules['pycadf'] = mock.Mock() + sys.modules['ldappool'] = mock.Mock() + sys.modules['memcache'] = mock.Mock() + sys.modules['oslo_messaging'] = mock.Mock() sys.modules['paste'] = mock.Mock() # NOTE(dstanek): oslo_i18n.enable_lazy() must be called before diff --git a/keystone-moon/keystone/tests/unit/auth/__init__.py b/keystone-moon/keystone/tests/unit/auth/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/tests/unit/auth/__init__.py diff --git a/keystone-moon/keystone/tests/unit/auth/test_controllers.py b/keystone-moon/keystone/tests/unit/auth/test_controllers.py new file mode 100644 index 00000000..76f2776a --- /dev/null +++ b/keystone-moon/keystone/tests/unit/auth/test_controllers.py @@ -0,0 +1,98 @@ +# Copyright 2015 IBM Corp. + +# 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 uuid + +import mock +from oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslo_utils import importutils +from oslotest import mockpatch +import stevedore +from stevedore import extension + +from keystone.auth import controllers +from keystone.tests import unit + + +class TestLoadAuthMethod(unit.BaseTestCase): + def test_entrypoint_works(self): + method = uuid.uuid4().hex + plugin_name = self.getUniqueString() + + # Register the method using the given plugin + cf = self.useFixture(config_fixture.Config()) + cf.register_opt(cfg.StrOpt(method), group='auth') + cf.config(group='auth', **{method: plugin_name}) + + # Setup stevedore.DriverManager to return a driver for the plugin + extension_ = extension.Extension( + plugin_name, entry_point=mock.sentinel.entry_point, + plugin=mock.sentinel.plugin, + obj=mock.sentinel.driver) + auth_plugin_namespace = 'keystone.auth.%s' % method + fake_driver_manager = stevedore.DriverManager.make_test_instance( + extension_, namespace=auth_plugin_namespace) + + driver_manager_mock = self.useFixture(mockpatch.PatchObject( + stevedore, 'DriverManager', return_value=fake_driver_manager)).mock + + driver = controllers.load_auth_method(method) + + self.assertEqual(auth_plugin_namespace, fake_driver_manager.namespace) + driver_manager_mock.assert_called_once_with( + auth_plugin_namespace, plugin_name, invoke_on_load=True) + self.assertIs(driver, mock.sentinel.driver) + + def test_entrypoint_fails_import_works(self): + method = uuid.uuid4().hex + plugin_name = self.getUniqueString() + + # Register the method using the given plugin + cf = self.useFixture(config_fixture.Config()) + cf.register_opt(cfg.StrOpt(method), group='auth') + cf.config(group='auth', **{method: plugin_name}) + + # stevedore.DriverManager raises RuntimeError if it can't load the + # driver. + self.useFixture(mockpatch.PatchObject( + stevedore, 'DriverManager', side_effect=RuntimeError)) + + self.useFixture(mockpatch.PatchObject( + importutils, 'import_object', return_value=mock.sentinel.driver)) + + driver = controllers.load_auth_method(method) + self.assertIs(driver, mock.sentinel.driver) + + def test_entrypoint_fails_import_fails(self): + method = uuid.uuid4().hex + plugin_name = self.getUniqueString() + + # Register the method using the given plugin + cf = self.useFixture(config_fixture.Config()) + cf.register_opt(cfg.StrOpt(method), group='auth') + cf.config(group='auth', **{method: plugin_name}) + + # stevedore.DriverManager raises RuntimeError if it can't load the + # driver. + self.useFixture(mockpatch.PatchObject( + stevedore, 'DriverManager', side_effect=RuntimeError)) + + class TestException(Exception): + pass + + self.useFixture(mockpatch.PatchObject( + importutils, 'import_object', side_effect=TestException)) + + self.assertRaises(TestException, controllers.load_auth_method, method) diff --git a/keystone-moon/keystone/tests/unit/backend/core_ldap.py b/keystone-moon/keystone/tests/unit/backend/core_ldap.py index 9d6b23e1..a6cd0802 100644 --- a/keystone-moon/keystone/tests/unit/backend/core_ldap.py +++ b/keystone-moon/keystone/tests/unit/backend/core_ldap.py @@ -17,7 +17,6 @@ from oslo_config import cfg from keystone.common import cache from keystone.common import ldap as common_ldap from keystone.common.ldap import core as common_ldap_core -from keystone.common import sql from keystone.tests import unit as tests from keystone.tests.unit import default_fixtures from keystone.tests.unit import fakeldap @@ -57,19 +56,13 @@ class BaseBackendLdapCommon(object): for shelf in fakeldap.FakeShelves: fakeldap.FakeShelves[shelf].clear() - def reload_backends(self, domain_id): - # Only one backend unless we are using separate domain backends - self.load_backends() - def get_config(self, domain_id): # Only one conf structure unless we are using separate domain backends return CONF def config_overrides(self): super(BaseBackendLdapCommon, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config(group='identity', driver='ldap') def config_files(self): config_files = super(BaseBackendLdapCommon, self).config_files() @@ -116,17 +109,13 @@ class BaseBackendLdapIdentitySqlEverythingElse(tests.SQLDriverOverrides): return config_files def setUp(self): - self.useFixture(database.Database()) + sqldb = self.useFixture(database.Database()) super(BaseBackendLdapIdentitySqlEverythingElse, self).setUp() self.clear_database() self.load_backends() cache.configure_cache_region(cache.REGION) - self.engine = sql.get_engine() - self.addCleanup(sql.cleanup) - - sql.ModelBase.metadata.create_all(bind=self.engine) - self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + sqldb.recreate() self.load_fixtures(default_fixtures) # defaulted by the data load self.user_foo['enabled'] = True @@ -134,15 +123,9 @@ class BaseBackendLdapIdentitySqlEverythingElse(tests.SQLDriverOverrides): def config_overrides(self): super(BaseBackendLdapIdentitySqlEverythingElse, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - self.config_fixture.config( - group='resource', - driver='keystone.resource.backends.sql.Resource') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') + self.config_fixture.config(group='identity', driver='ldap') + self.config_fixture.config(group='resource', driver='sql') + self.config_fixture.config(group='assignment', driver='sql') class BaseBackendLdapIdentitySqlEverythingElseWithMapping(object): diff --git a/keystone-moon/keystone/tests/unit/backend/domain_config/core.py b/keystone-moon/keystone/tests/unit/backend/domain_config/core.py index da2e9bd9..c53d99b7 100644 --- a/keystone-moon/keystone/tests/unit/backend/domain_config/core.py +++ b/keystone-moon/keystone/tests/unit/backend/domain_config/core.py @@ -17,6 +17,7 @@ import mock from testtools import matchers from keystone import exception +from keystone.tests import unit as tests class DomainConfigTests(object): @@ -521,3 +522,30 @@ class DomainConfigTests(object): self.assertFalse(mock_log.warn.called) # The escaping '%' should have been removed self.assertEqual('my_url/%(password)s', res['ldap']['url']) + + @tests.skip_if_cache_disabled('domain_config') + def test_cache_layer_get_sensitive_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + # cache the result + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(config, res) + + # delete, bypassing domain config manager api + self.domain_config_api.delete_config_options(self.domain['id']) + self.domain_config_api.delete_config_options(self.domain['id'], + sensitive=True) + + self.assertDictEqual( + res, self.domain_config_api.get_config_with_sensitive_info( + self.domain['id'])) + self.domain_config_api.get_config_with_sensitive_info.invalidate( + self.domain_config_api, self.domain['id']) + self.assertDictEqual( + {}, + self.domain_config_api.get_config_with_sensitive_info( + self.domain['id'])) diff --git a/keystone-moon/keystone/tests/unit/catalog/test_core.py b/keystone-moon/keystone/tests/unit/catalog/test_core.py index 99a34280..2f334bb6 100644 --- a/keystone-moon/keystone/tests/unit/catalog/test_core.py +++ b/keystone-moon/keystone/tests/unit/catalog/test_core.py @@ -11,16 +11,16 @@ # under the License. from oslo_config import cfg -import testtools from keystone.catalog import core from keystone import exception +from keystone.tests import unit CONF = cfg.CONF -class FormatUrlTests(testtools.TestCase): +class FormatUrlTests(unit.BaseTestCase): def test_successful_formatting(self): url_template = ('http://$(public_bind_host)s:$(admin_port)d/' @@ -72,3 +72,17 @@ class FormatUrlTests(testtools.TestCase): core.format_url, url_template, values) + + def test_substitution_with_allowed_keyerror(self): + # No value of 'tenant_id' is passed into url_template. + # mod: format_url will return None instead of raising + # "MalformedEndpoint" exception. + # This is intentional behavior since we don't want to skip + # all the later endpoints once there is an URL of endpoint + # trying to replace 'tenant_id' with None. + url_template = ('http://$(public_bind_host)s:$(admin_port)d/' + '$(tenant_id)s/$(user_id)s') + values = {'public_bind_host': 'server', 'admin_port': 9090, + 'user_id': 'B'} + self.assertIsNone(core.format_url(url_template, values, + silent_keyerror_failures=['tenant_id'])) diff --git a/keystone-moon/keystone/tests/unit/common/test_connection_pool.py b/keystone-moon/keystone/tests/unit/common/test_connection_pool.py index 74d0420c..3813e033 100644 --- a/keystone-moon/keystone/tests/unit/common/test_connection_pool.py +++ b/keystone-moon/keystone/tests/unit/common/test_connection_pool.py @@ -10,9 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import threading import time import mock +import six from six.moves import queue import testtools from testtools import matchers @@ -117,3 +119,17 @@ class TestConnectionPool(core.TestCase): # after it is available. connection_pool.put_nowait(conn) _acquire_connection() + + +class TestMemcacheClientOverrides(core.BaseTestCase): + + def test_client_stripped_of_threading_local(self): + """threading.local overrides are restored for _MemcacheClient""" + client_class = _memcache_pool._MemcacheClient + # get the genuine thread._local from MRO + thread_local = client_class.__mro__[2] + self.assertTrue(thread_local is threading.local) + for field in six.iterkeys(thread_local.__dict__): + if field not in ('__dict__', '__weakref__'): + self.assertNotEqual(id(getattr(thread_local, field, None)), + id(getattr(client_class, field, None))) diff --git a/keystone-moon/keystone/tests/unit/common/test_injection.py b/keystone-moon/keystone/tests/unit/common/test_injection.py index 86bb3c24..b4c23a84 100644 --- a/keystone-moon/keystone/tests/unit/common/test_injection.py +++ b/keystone-moon/keystone/tests/unit/common/test_injection.py @@ -21,6 +21,7 @@ from keystone.tests import unit as tests class TestDependencyInjection(tests.BaseTestCase): def setUp(self): super(TestDependencyInjection, self).setUp() + dependency.reset() self.addCleanup(dependency.reset) def test_dependency_injection(self): @@ -210,62 +211,6 @@ class TestDependencyInjection(tests.BaseTestCase): self.assertFalse(dependency._REGISTRY) - def test_optional_dependency_not_provided(self): - requirement_name = uuid.uuid4().hex - - @dependency.optional(requirement_name) - class C1(object): - pass - - c1_inst = C1() - - dependency.resolve_future_dependencies() - - self.assertIsNone(getattr(c1_inst, requirement_name)) - - def test_optional_dependency_provided(self): - requirement_name = uuid.uuid4().hex - - @dependency.optional(requirement_name) - class C1(object): - pass - - @dependency.provider(requirement_name) - class P1(object): - pass - - c1_inst = C1() - p1_inst = P1() - - dependency.resolve_future_dependencies() - - self.assertIs(getattr(c1_inst, requirement_name), p1_inst) - - def test_optional_and_required(self): - p1_name = uuid.uuid4().hex - p2_name = uuid.uuid4().hex - optional_name = uuid.uuid4().hex - - @dependency.provider(p1_name) - @dependency.requires(p2_name) - @dependency.optional(optional_name) - class P1(object): - pass - - @dependency.provider(p2_name) - @dependency.requires(p1_name) - class P2(object): - pass - - p1 = P1() - p2 = P2() - - dependency.resolve_future_dependencies() - - self.assertIs(getattr(p1, p2_name), p2) - self.assertIs(getattr(p2, p1_name), p1) - self.assertIsNone(getattr(p1, optional_name)) - def test_get_provider(self): # Can get the instance of a provider using get_provider diff --git a/keystone-moon/keystone/tests/unit/common/test_ldap.py b/keystone-moon/keystone/tests/unit/common/test_ldap.py index 41568890..d3ce8cd2 100644 --- a/keystone-moon/keystone/tests/unit/common/test_ldap.py +++ b/keystone-moon/keystone/tests/unit/common/test_ldap.py @@ -11,23 +11,24 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import tempfile import uuid +import fixtures import ldap.dn import mock from oslo_config import cfg from testtools import matchers -import os -import shutil -import tempfile - +from keystone.common import driver_hints from keystone.common import ldap as ks_ldap from keystone.common.ldap import core as common_ldap_core from keystone.tests import unit as tests from keystone.tests.unit import default_fixtures from keystone.tests.unit import fakeldap + CONF = cfg.CONF @@ -218,9 +219,7 @@ class LDAPDeleteTreeTest(tests.TestCase): def config_overrides(self): super(LDAPDeleteTreeTest, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config(group='identity', driver='ldap') def config_files(self): config_files = super(LDAPDeleteTreeTest, self).config_files() @@ -311,8 +310,7 @@ class SslTlsTest(tests.TestCase): def test_certdir_trust_tls(self): # We need this to actually exist, so we create a tempdir. - certdir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, certdir) + certdir = self.useFixture(fixtures.TempDir()).path self.config_fixture.config(group='ldap', url='ldap://localhost', use_tls=True, @@ -340,8 +338,7 @@ class SslTlsTest(tests.TestCase): def test_certdir_trust_ldaps(self): # We need this to actually exist, so we create a tempdir. - certdir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, certdir) + certdir = self.useFixture(fixtures.TempDir()).path self.config_fixture.config(group='ldap', url='ldaps://localhost', use_tls=False, @@ -372,9 +369,7 @@ class LDAPPagedResultsTest(tests.TestCase): def config_overrides(self): super(LDAPPagedResultsTest, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config(group='identity', driver='ldap') def config_files(self): config_files = super(LDAPPagedResultsTest, self).config_files() @@ -500,3 +495,68 @@ class CommonLdapTestCase(tests.BaseTestCase): py_result = ks_ldap.convert_ldap_result(result) # The user name should still be a string value. self.assertEqual(user_name, py_result[0][1]['user_name'][0]) + + +class LDAPFilterQueryCompositionTest(tests.TestCase): + """These test cases test LDAP filter generation.""" + + def setUp(self): + super(LDAPFilterQueryCompositionTest, self).setUp() + + self.base_ldap = ks_ldap.BaseLdap(self.config_fixture.conf) + + # The tests need an attribute mapping to use. + self.attribute_name = uuid.uuid4().hex + self.filter_attribute_name = uuid.uuid4().hex + self.base_ldap.attribute_mapping = { + self.attribute_name: self.filter_attribute_name + } + + def test_return_query_with_no_hints(self): + hints = driver_hints.Hints() + # NOTE: doesn't have to be a real query, we just need to make sure the + # same string is returned if there are no hints. + query = uuid.uuid4().hex + self.assertEqual(query, + self.base_ldap.filter_query(hints=hints, query=query)) + + # make sure the default query is an empty string + self.assertEqual('', self.base_ldap.filter_query(hints=hints)) + + def test_filter_with_empty_query_and_hints_set(self): + hints = driver_hints.Hints() + username = uuid.uuid4().hex + hints.add_filter(name=self.attribute_name, + value=username, + comparator='equals', + case_sensitive=False) + expected_ldap_filter = '(&(%s=%s))' % ( + self.filter_attribute_name, username) + self.assertEqual(expected_ldap_filter, + self.base_ldap.filter_query(hints=hints)) + + def test_filter_with_both_query_and_hints_set(self): + hints = driver_hints.Hints() + # NOTE: doesn't have to be a real query, we just need to make sure the + # filter string is concatenated correctly + query = uuid.uuid4().hex + username = uuid.uuid4().hex + expected_result = '(&%(query)s(%(user_name_attr)s=%(username)s))' % ( + {'query': query, + 'user_name_attr': self.filter_attribute_name, + 'username': username}) + hints.add_filter(self.attribute_name, username) + self.assertEqual(expected_result, + self.base_ldap.filter_query(hints=hints, query=query)) + + def test_filter_with_hints_and_query_is_none(self): + hints = driver_hints.Hints() + username = uuid.uuid4().hex + hints.add_filter(name=self.attribute_name, + value=username, + comparator='equals', + case_sensitive=False) + expected_ldap_filter = '(&(%s=%s))' % ( + self.filter_attribute_name, username) + self.assertEqual(expected_ldap_filter, + self.base_ldap.filter_query(hints=hints, query=None)) diff --git a/keystone-moon/keystone/tests/unit/common/test_notifications.py b/keystone-moon/keystone/tests/unit/common/test_notifications.py index 55dd556d..2d872733 100644 --- a/keystone-moon/keystone/tests/unit/common/test_notifications.py +++ b/keystone-moon/keystone/tests/unit/common/test_notifications.py @@ -23,10 +23,9 @@ from pycadf import cadftaxonomy from pycadf import cadftype from pycadf import eventfactory from pycadf import resource as cadfresource -import testtools -from keystone.common import dependency from keystone import notifications +from keystone.tests import unit from keystone.tests.unit import test_v3 @@ -53,7 +52,7 @@ def register_callback(operation, resource_type=EXP_RESOURCE_TYPE): return callback -class AuditNotificationsTestCase(testtools.TestCase): +class AuditNotificationsTestCase(unit.BaseTestCase): def setUp(self): super(AuditNotificationsTestCase, self).setUp() self.config_fixture = self.useFixture(config_fixture.Config(CONF)) @@ -96,7 +95,7 @@ class AuditNotificationsTestCase(testtools.TestCase): DISABLED_OPERATION) -class NotificationsWrapperTestCase(testtools.TestCase): +class NotificationsWrapperTestCase(unit.BaseTestCase): def create_fake_ref(self): resource_id = uuid.uuid4().hex return resource_id, { @@ -174,14 +173,7 @@ class NotificationsWrapperTestCase(testtools.TestCase): self.assertFalse(callback.called) -class NotificationsTestCase(testtools.TestCase): - def setUp(self): - super(NotificationsTestCase, self).setUp() - - # these should use self.config_fixture.config(), but they haven't - # been registered yet - CONF.rpc_backend = 'fake' - CONF.notification_driver = ['fake'] +class NotificationsTestCase(unit.BaseTestCase): def test_send_notification(self): """Test the private method _send_notification to ensure event_type, @@ -324,7 +316,7 @@ class NotificationsForEntities(BaseNotificationTest): def test_create_project(self): project_ref = self.new_project_ref(domain_id=self.domain_id) - self.assignment_api.create_project(project_ref['id'], project_ref) + self.resource_api.create_project(project_ref['id'], project_ref) self._assert_last_note( project_ref['id'], CREATED_OPERATION, 'project') self._assert_last_audit(project_ref['id'], CREATED_OPERATION, @@ -371,8 +363,8 @@ class NotificationsForEntities(BaseNotificationTest): def test_delete_project(self): project_ref = self.new_project_ref(domain_id=self.domain_id) - self.assignment_api.create_project(project_ref['id'], project_ref) - self.assignment_api.delete_project(project_ref['id']) + self.resource_api.create_project(project_ref['id'], project_ref) + self.resource_api.delete_project(project_ref['id']) self._assert_last_note( project_ref['id'], DELETED_OPERATION, 'project') self._assert_last_audit(project_ref['id'], DELETED_OPERATION, @@ -403,19 +395,19 @@ class NotificationsForEntities(BaseNotificationTest): def test_update_domain(self): domain_ref = self.new_domain_ref() - self.assignment_api.create_domain(domain_ref['id'], domain_ref) + self.resource_api.create_domain(domain_ref['id'], domain_ref) domain_ref['description'] = uuid.uuid4().hex - self.assignment_api.update_domain(domain_ref['id'], domain_ref) + self.resource_api.update_domain(domain_ref['id'], domain_ref) self._assert_last_note(domain_ref['id'], UPDATED_OPERATION, 'domain') self._assert_last_audit(domain_ref['id'], UPDATED_OPERATION, 'domain', cadftaxonomy.SECURITY_DOMAIN) def test_delete_domain(self): domain_ref = self.new_domain_ref() - self.assignment_api.create_domain(domain_ref['id'], domain_ref) + self.resource_api.create_domain(domain_ref['id'], domain_ref) domain_ref['enabled'] = False - self.assignment_api.update_domain(domain_ref['id'], domain_ref) - self.assignment_api.delete_domain(domain_ref['id']) + self.resource_api.update_domain(domain_ref['id'], domain_ref) + self.resource_api.delete_domain(domain_ref['id']) self._assert_last_note(domain_ref['id'], DELETED_OPERATION, 'domain') self._assert_last_audit(domain_ref['id'], DELETED_OPERATION, 'domain', cadftaxonomy.SECURITY_DOMAIN) @@ -542,19 +534,19 @@ class NotificationsForEntities(BaseNotificationTest): def test_disable_domain(self): domain_ref = self.new_domain_ref() - self.assignment_api.create_domain(domain_ref['id'], domain_ref) + self.resource_api.create_domain(domain_ref['id'], domain_ref) domain_ref['enabled'] = False - self.assignment_api.update_domain(domain_ref['id'], domain_ref) + self.resource_api.update_domain(domain_ref['id'], domain_ref) self._assert_notify_sent(domain_ref['id'], 'disabled', 'domain', public=False) def test_disable_of_disabled_domain_does_not_notify(self): domain_ref = self.new_domain_ref() domain_ref['enabled'] = False - self.assignment_api.create_domain(domain_ref['id'], domain_ref) + self.resource_api.create_domain(domain_ref['id'], domain_ref) # The domain_ref above is not changed during the create process. We # can use the same ref to perform the update. - self.assignment_api.update_domain(domain_ref['id'], domain_ref) + self.resource_api.update_domain(domain_ref['id'], domain_ref) self._assert_notify_not_sent(domain_ref['id'], 'disabled', 'domain', public=False) @@ -568,8 +560,8 @@ class NotificationsForEntities(BaseNotificationTest): def test_update_project(self): project_ref = self.new_project_ref(domain_id=self.domain_id) - self.assignment_api.create_project(project_ref['id'], project_ref) - self.assignment_api.update_project(project_ref['id'], project_ref) + self.resource_api.create_project(project_ref['id'], project_ref) + self.resource_api.update_project(project_ref['id'], project_ref) self._assert_notify_sent( project_ref['id'], UPDATED_OPERATION, 'project', public=True) self._assert_last_audit(project_ref['id'], UPDATED_OPERATION, @@ -577,27 +569,27 @@ class NotificationsForEntities(BaseNotificationTest): def test_disable_project(self): project_ref = self.new_project_ref(domain_id=self.domain_id) - self.assignment_api.create_project(project_ref['id'], project_ref) + self.resource_api.create_project(project_ref['id'], project_ref) project_ref['enabled'] = False - self.assignment_api.update_project(project_ref['id'], project_ref) + self.resource_api.update_project(project_ref['id'], project_ref) self._assert_notify_sent(project_ref['id'], 'disabled', 'project', public=False) def test_disable_of_disabled_project_does_not_notify(self): project_ref = self.new_project_ref(domain_id=self.domain_id) project_ref['enabled'] = False - self.assignment_api.create_project(project_ref['id'], project_ref) + self.resource_api.create_project(project_ref['id'], project_ref) # The project_ref above is not changed during the create process. We # can use the same ref to perform the update. - self.assignment_api.update_project(project_ref['id'], project_ref) + self.resource_api.update_project(project_ref['id'], project_ref) self._assert_notify_not_sent(project_ref['id'], 'disabled', 'project', public=False) def test_update_project_does_not_send_disable(self): project_ref = self.new_project_ref(domain_id=self.domain_id) - self.assignment_api.create_project(project_ref['id'], project_ref) + self.resource_api.create_project(project_ref['id'], project_ref) project_ref['enabled'] = True - self.assignment_api.update_project(project_ref['id'], project_ref) + self.resource_api.update_project(project_ref['id'], project_ref) self._assert_last_note( project_ref['id'], UPDATED_OPERATION, 'project') self._assert_notify_not_sent(project_ref['id'], 'disabled', 'project') @@ -665,7 +657,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase): def test_notification_received(self): callback = register_callback(CREATED_OPERATION, 'project') project_ref = self.new_project_ref(domain_id=self.domain_id) - self.assignment_api.create_project(project_ref['id'], project_ref) + self.resource_api.create_project(project_ref['id'], project_ref) self.assertTrue(callback.called) def test_notification_method_not_callable(self): @@ -694,14 +686,14 @@ class TestEventCallbacks(test_v3.RestfulTestCase): resource_type, self._project_deleted_callback) - def test_provider_event_callbacks_subscription(self): + def test_provider_event_callback_subscription(self): callback_called = [] - @dependency.provider('foo_api') + @notifications.listener class Foo(object): def __init__(self): self.event_callbacks = { - CREATED_OPERATION: {'project': [self.foo_callback]}} + CREATED_OPERATION: {'project': self.foo_callback}} def foo_callback(self, service, resource_type, operation, payload): @@ -710,24 +702,73 @@ class TestEventCallbacks(test_v3.RestfulTestCase): Foo() project_ref = self.new_project_ref(domain_id=self.domain_id) - self.assignment_api.create_project(project_ref['id'], project_ref) + self.resource_api.create_project(project_ref['id'], project_ref) self.assertEqual([True], callback_called) + def test_provider_event_callbacks_subscription(self): + callback_called = [] + + @notifications.listener + class Foo(object): + def __init__(self): + self.event_callbacks = { + CREATED_OPERATION: { + 'project': [self.callback_0, self.callback_1]}} + + def callback_0(self, service, resource_type, operation, payload): + # uses callback_called from the closure + callback_called.append('cb0') + + def callback_1(self, service, resource_type, operation, payload): + # uses callback_called from the closure + callback_called.append('cb1') + + Foo() + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.resource_api.create_project(project_ref['id'], project_ref) + self.assertItemsEqual(['cb1', 'cb0'], callback_called) + def test_invalid_event_callbacks(self): - @dependency.provider('foo_api') + @notifications.listener class Foo(object): def __init__(self): self.event_callbacks = 'bogus' - self.assertRaises(ValueError, Foo) + self.assertRaises(AttributeError, Foo) def test_invalid_event_callbacks_event(self): - @dependency.provider('foo_api') + @notifications.listener class Foo(object): def __init__(self): self.event_callbacks = {CREATED_OPERATION: 'bogus'} - self.assertRaises(ValueError, Foo) + self.assertRaises(AttributeError, Foo) + + def test_using_an_unbound_method_as_a_callback_fails(self): + # NOTE(dstanek): An unbound method is when you reference a method + # from a class object. You'll get a method that isn't bound to a + # particular instance so there is no magic 'self'. You can call it, + # but you have to pass in the instance manually like: C.m(C()). + # If you reference the method from an instance then you get a method + # that effectively curries the self argument for you + # (think functools.partial). Obviously is we don't have an + # instance then we can't call the method. + @notifications.listener + class Foo(object): + def __init__(self): + self.event_callbacks = {CREATED_OPERATION: + {'project': Foo.callback}} + + def callback(self, *args): + pass + + # TODO(dstanek): it would probably be nice to fail early using + # something like: + # self.assertRaises(TypeError, Foo) + Foo() + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assertRaises(TypeError, self.resource_api.create_project, + project_ref['id'], project_ref) class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): @@ -759,13 +800,14 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): 'action': action, 'initiator': initiator, 'event': event, + 'event_type': event_type, 'send_notification_called': True} self._notifications.append(note) self.useFixture(mockpatch.PatchObject( notifications, '_send_audit_notification', fake_notify)) - def _assert_last_note(self, action, user_id): + def _assert_last_note(self, action, user_id, event_type=None): self.assertTrue(self._notifications) note = self._notifications[-1] self.assertEqual(note['action'], action) @@ -773,6 +815,8 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): self.assertEqual(initiator.id, user_id) self.assertEqual(initiator.host.address, self.LOCAL_HOST) self.assertTrue(note['send_notification_called']) + if event_type: + self.assertEqual(note['event_type'], event_type) def _assert_event(self, role_id, project=None, domain=None, user=None, group=None, inherit=False): @@ -816,10 +860,10 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): self.assertEqual(project, event.project) if domain: self.assertEqual(domain, event.domain) - if user: - self.assertEqual(user, event.user) if group: self.assertEqual(group, event.group) + elif user: + self.assertEqual(user, event.user) self.assertEqual(role_id, event.role) self.assertEqual(inherit, event.inherited_to_projects) @@ -857,12 +901,16 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): user=None, group=None): self.put(url) action = "%s.%s" % (CREATED_OPERATION, self.ROLE_ASSIGNMENT) - self._assert_last_note(action, self.user_id) + event_type = '%s.%s.%s' % (notifications.SERVICE, + self.ROLE_ASSIGNMENT, CREATED_OPERATION) + self._assert_last_note(action, self.user_id, event_type) self._assert_event(role, project, domain, user, group) self.delete(url) action = "%s.%s" % (DELETED_OPERATION, self.ROLE_ASSIGNMENT) - self._assert_last_note(action, self.user_id) - self._assert_event(role, project, domain, user, group) + event_type = '%s.%s.%s' % (notifications.SERVICE, + self.ROLE_ASSIGNMENT, DELETED_OPERATION) + self._assert_last_note(action, self.user_id, event_type) + self._assert_event(role, project, domain, user, None) def test_user_project_grant(self): url = ('/projects/%s/users/%s/roles/%s' % @@ -874,14 +922,50 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): def test_group_domain_grant(self): group_ref = self.new_group_ref(domain_id=self.domain_id) group = self.identity_api.create_group(group_ref) + self.identity_api.add_user_to_group(self.user_id, group['id']) url = ('/domains/%s/groups/%s/roles/%s' % (self.domain_id, group['id'], self.role_id)) self._test_role_assignment(url, self.role_id, domain=self.domain_id, + user=self.user_id, group=group['id']) + def test_add_role_to_user_and_project(self): + # A notification is sent when add_role_to_user_and_project is called on + # the assignment manager. + + project_ref = self.new_project_ref(self.domain_id) + project = self.resource_api.create_project( + project_ref['id'], project_ref) + tenant_id = project['id'] + + self.assignment_api.add_role_to_user_and_project( + self.user_id, tenant_id, self.role_id) + + self.assertTrue(self._notifications) + note = self._notifications[-1] + self.assertEqual(note['action'], 'created.role_assignment') + self.assertTrue(note['send_notification_called']) + + self._assert_event(self.role_id, project=tenant_id, user=self.user_id) + + def test_remove_role_from_user_and_project(self): + # A notification is sent when remove_role_from_user_and_project is + # called on the assignment manager. + + self.assignment_api.remove_role_from_user_and_project( + self.user_id, self.project_id, self.role_id) + + self.assertTrue(self._notifications) + note = self._notifications[-1] + self.assertEqual(note['action'], 'deleted.role_assignment') + self.assertTrue(note['send_notification_called']) + + self._assert_event(self.role_id, project=self.project_id, + user=self.user_id) + -class TestCallbackRegistration(testtools.TestCase): +class TestCallbackRegistration(unit.BaseTestCase): def setUp(self): super(TestCallbackRegistration, self).setUp() self.mock_log = mock.Mock() diff --git a/keystone-moon/keystone/tests/unit/common/test_utils.py b/keystone-moon/keystone/tests/unit/common/test_utils.py index 184c8141..e8bac3c0 100644 --- a/keystone-moon/keystone/tests/unit/common/test_utils.py +++ b/keystone-moon/keystone/tests/unit/common/test_utils.py @@ -150,7 +150,7 @@ class UtilsTestCase(tests.BaseTestCase): def test_pki_encoder(self): data = {'field': 'value'} json = jsonutils.dumps(data, cls=common_utils.PKIEncoder) - expected_json = b'{"field":"value"}' + expected_json = '{"field":"value"}' self.assertEqual(expected_json, json) diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf index 8a06f2f9..2097b68b 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf @@ -2,7 +2,7 @@ #For a specific location file based sqlite use: #connection = sqlite:////tmp/keystone.db #To Test MySQL: -#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#connection = mysql+pymysql://keystone:keystone@localhost/keystone?charset=utf8 #To Test PostgreSQL: #connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 idle_timeout = 200 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf index 2d04d83d..5185770b 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf @@ -3,7 +3,7 @@ connection = sqlite:// #For a file based sqlite use #connection = sqlite:////tmp/keystone.db #To Test MySQL: -#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#connection = mysql+pymysql://keystone:keystone@localhost/keystone?charset=utf8 #To Test PostgreSQL: #connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 idle_timeout = 200 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf index d612f729..142ca203 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf @@ -1,4 +1,4 @@ #Used for running the Migrate tests against a live Mysql Server #See _sql_livetest.py [database] -connection = mysql://keystone:keystone@localhost/keystone_test?charset=utf8 +connection = mysql+pymysql://keystone:keystone@localhost/keystone_test?charset=utf8 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf index 9d401af3..063177bd 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf @@ -2,7 +2,7 @@ #For a specific location file based sqlite use: #connection = sqlite:////tmp/keystone.db #To Test MySQL: -#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#connection = mysql+pymysql://keystone:keystone@localhost/keystone?charset=utf8 #To Test PostgreSQL: #connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 idle_timeout = 200 diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf index a4492a67..fecc7bea 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf @@ -2,4 +2,4 @@ # 'domain1' for use with unit tests. [identity] -driver = keystone.identity.backends.sql.Identity
\ No newline at end of file +driver = sql
\ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf index 7049afed..2dd86c25 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf @@ -11,4 +11,4 @@ password = password suffix = cn=example,cn=com [identity] -driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file +driver = ldap
\ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf index 6b7e2488..ba22cdf9 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf @@ -8,4 +8,4 @@ password = password suffix = cn=example,cn=com [identity] -driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file +driver = ldap
\ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf index 0ed68eb9..a14179e3 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf @@ -10,4 +10,4 @@ group_tree_dn = ou=UserGroups,dc=myroot,dc=org user_tree_dn = ou=Users,dc=myroot,dc=org [identity] -driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file +driver = ldap
\ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf index 81b44462..925b26f2 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf @@ -2,4 +2,4 @@ # 'domain2' for use with unit tests. [identity] -driver = keystone.identity.backends.sql.Identity
\ No newline at end of file +driver = sql
\ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf index 7049afed..2dd86c25 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf @@ -11,4 +11,4 @@ password = password suffix = cn=example,cn=com [identity] -driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file +driver = ldap
\ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf index a4492a67..fecc7bea 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf @@ -2,4 +2,4 @@ # 'domain1' for use with unit tests. [identity] -driver = keystone.identity.backends.sql.Identity
\ No newline at end of file +driver = sql
\ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf b/keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf index abcc43ba..4a9e87d5 100644 --- a/keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf +++ b/keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf @@ -1,7 +1,4 @@ [auth] methods = external,password,token,simple_challenge_response,saml2,openid,x509 simple_challenge_response = keystone.tests.unit.test_auth_plugin.SimpleChallengeResponse -saml2 = keystone.auth.plugins.mapped.Mapped -openid = keystone.auth.plugins.mapped.Mapped -x509 = keystone.auth.plugins.mapped.Mapped diff --git a/keystone-moon/keystone/tests/unit/contrib/__init__.py b/keystone-moon/keystone/tests/unit/contrib/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/tests/unit/contrib/__init__.py diff --git a/keystone-moon/keystone/tests/unit/contrib/federation/__init__.py b/keystone-moon/keystone/tests/unit/contrib/federation/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keystone-moon/keystone/tests/unit/contrib/federation/__init__.py diff --git a/keystone-moon/keystone/tests/unit/contrib/federation/test_utils.py b/keystone-moon/keystone/tests/unit/contrib/federation/test_utils.py new file mode 100644 index 00000000..a8b4ae76 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/contrib/federation/test_utils.py @@ -0,0 +1,611 @@ +# 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.auth.plugins import mapped +from keystone.contrib.federation import utils as mapping_utils +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit import mapping_fixtures + + +class MappingRuleEngineTests(unit.BaseTestCase): + """A class for testing the mapping rule engine.""" + + def assertValidMappedUserObject(self, mapped_properties, + user_type='ephemeral', + domain_id=None): + """Check whether mapped properties object has 'user' within. + + According to today's rules, RuleProcessor does not have to issue user's + id or name. What's actually required is user's type and for ephemeral + users that would be service domain named 'Federated'. + """ + self.assertIn('user', mapped_properties, + message='Missing user object in mapped properties') + user = mapped_properties['user'] + self.assertIn('type', user) + self.assertEqual(user_type, user['type']) + self.assertIn('domain', user) + domain = user['domain'] + domain_name_or_id = domain.get('id') or domain.get('name') + domain_ref = domain_id or 'Federated' + self.assertEqual(domain_ref, domain_name_or_id) + + def test_rule_engine_any_one_of_and_direct_mapping(self): + """Should return user's name and group id EMPLOYEE_GROUP_ID. + + The ADMIN_ASSERTION should successfully have a match in MAPPING_LARGE. + They will test the case where `any_one_of` is valid, and there is + a direct mapping for the users name. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + assertion = mapping_fixtures.ADMIN_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + fn = assertion.get('FirstName') + ln = assertion.get('LastName') + full_name = '%s %s' % (fn, ln) + group_ids = values.get('group_ids') + user_name = values.get('user', {}).get('name') + + self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) + self.assertEqual(full_name, user_name) + + def test_rule_engine_no_regex_match(self): + """Should deny authorization, the email of the tester won't match. + + This will not match since the email in the assertion will fail + the regex test. It is set to match any @example.com address. + But the incoming value is set to eviltester@example.org. + RuleProcessor should return list of empty group_ids. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + assertion = mapping_fixtures.BAD_TESTER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) + self.assertListEqual(list(), mapped_properties['group_ids']) + + def test_rule_engine_regex_many_groups(self): + """Should return group CONTRACTOR_GROUP_ID. + + The TESTER_ASSERTION should successfully have a match in + MAPPING_TESTER_REGEX. This will test the case where many groups + are in the assertion, and a regex value is used to try and find + a match. + + """ + + mapping = mapping_fixtures.MAPPING_TESTER_REGEX + assertion = mapping_fixtures.TESTER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) + + def test_rule_engine_any_one_of_many_rules(self): + """Should return group CONTRACTOR_GROUP_ID. + + The CONTRACTOR_ASSERTION should successfully have a match in + MAPPING_SMALL. This will test the case where many rules + must be matched, including an `any_one_of`, and a direct + mapping. + + """ + + mapping = mapping_fixtures.MAPPING_SMALL + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.CONTRACTOR_GROUP_ID, group_ids) + + def test_rule_engine_not_any_of_and_direct_mapping(self): + """Should return user's name and email. + + The CUSTOMER_ASSERTION should successfully have a match in + MAPPING_LARGE. This will test the case where a requirement + has `not_any_of`, and direct mapping to a username, no group. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + assertion = mapping_fixtures.CUSTOMER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertEqual([], group_ids,) + + def test_rule_engine_not_any_of_many_rules(self): + """Should return group EMPLOYEE_GROUP_ID. + + The EMPLOYEE_ASSERTION should successfully have a match in + MAPPING_SMALL. This will test the case where many remote + rules must be matched, including a `not_any_of`. + + """ + + mapping = mapping_fixtures.MAPPING_SMALL + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) + + def test_rule_engine_not_any_of_regex_verify_pass(self): + """Should return group DEVELOPER_GROUP_ID. + + The DEVELOPER_ASSERTION should successfully have a match in + MAPPING_DEVELOPER_REGEX. This will test the case where many + remote rules must be matched, including a `not_any_of`, with + regex set to True. + + """ + + mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX + assertion = mapping_fixtures.DEVELOPER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) + + def test_rule_engine_not_any_of_regex_verify_fail(self): + """Should deny authorization. + + The email in the assertion will fail the regex test. + It is set to reject any @example.org address, but the + incoming value is set to evildeveloper@example.org. + RuleProcessor should return list of empty group_ids. + + """ + + mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX + assertion = mapping_fixtures.BAD_DEVELOPER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) + self.assertListEqual(list(), mapped_properties['group_ids']) + + def _rule_engine_regex_match_and_many_groups(self, assertion): + """Should return group DEVELOPER_GROUP_ID and TESTER_GROUP_ID. + + A helper function injecting assertion passed as an argument. + Expect DEVELOPER_GROUP_ID and TESTER_GROUP_ID in the results. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertValidMappedUserObject(values) + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) + self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) + + def test_rule_engine_regex_match_and_many_groups(self): + """Should return group DEVELOPER_GROUP_ID and TESTER_GROUP_ID. + + The TESTER_ASSERTION should successfully have a match in + MAPPING_LARGE. This will test a successful regex match + for an `any_one_of` evaluation type, and will have many + groups returned. + + """ + self._rule_engine_regex_match_and_many_groups( + mapping_fixtures.TESTER_ASSERTION) + + def test_rule_engine_discards_nonstring_objects(self): + """Check whether RuleProcessor discards non string objects. + + Despite the fact that assertion is malformed and contains + non string objects, RuleProcessor should correctly discard them and + successfully have a match in MAPPING_LARGE. + + """ + self._rule_engine_regex_match_and_many_groups( + mapping_fixtures.MALFORMED_TESTER_ASSERTION) + + def test_rule_engine_fails_after_discarding_nonstring(self): + """Check whether RuleProcessor discards non string objects. + + Expect RuleProcessor to discard non string object, which + is required for a correct rule match. RuleProcessor will result with + empty list of groups. + + """ + mapping = mapping_fixtures.MAPPING_SMALL + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_MALFORMED_ASSERTION + mapped_properties = rp.process(assertion) + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) + self.assertListEqual(list(), mapped_properties['group_ids']) + + def test_rule_engine_returns_group_names(self): + """Check whether RuleProcessor returns group names with their domains. + + RuleProcessor should return 'group_names' entry with a list of + dictionaries with two entries 'name' and 'domain' identifying group by + its name and domain. + + """ + mapping = mapping_fixtures.MAPPING_GROUP_NAMES + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + reference = { + mapping_fixtures.DEVELOPER_GROUP_NAME: + { + "name": mapping_fixtures.DEVELOPER_GROUP_NAME, + "domain": { + "name": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_NAME + } + }, + mapping_fixtures.TESTER_GROUP_NAME: + { + "name": mapping_fixtures.TESTER_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + + def test_rule_engine_whitelist_and_direct_groups_mapping(self): + """Should return user's groups Developer and Contractor. + + The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match + in MAPPING_GROUPS_WHITELIST. It will test the case where 'whitelist' + correctly filters out Manager and only allows Developer and Contractor. + + """ + + mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + + reference = { + mapping_fixtures.DEVELOPER_GROUP_NAME: + { + "name": mapping_fixtures.DEVELOPER_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + }, + mapping_fixtures.CONTRACTOR_GROUP_NAME: + { + "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual([], mapped_properties['group_ids']) + + def test_rule_engine_blacklist_and_direct_groups_mapping(self): + """Should return user's group Developer. + + The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match + in MAPPING_GROUPS_BLACKLIST. It will test the case where 'blacklist' + correctly filters out Manager and Developer and only allows Contractor. + + """ + + mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + + reference = { + mapping_fixtures.CONTRACTOR_GROUP_NAME: + { + "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual([], mapped_properties['group_ids']) + + def test_rule_engine_blacklist_and_direct_groups_mapping_multiples(self): + """Tests matching multiple values before the blacklist. + + Verifies that the local indexes are correct when matching multiple + remote values for a field when the field occurs before the blacklist + entry in the remote rules. + + """ + + mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MULTIPLES + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + + reference = { + mapping_fixtures.CONTRACTOR_GROUP_NAME: + { + "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual([], mapped_properties['group_ids']) + + def test_rule_engine_whitelist_direct_group_mapping_missing_domain(self): + """Test if the local rule is rejected upon missing domain value + + This is a variation with a ``whitelist`` filter. + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_MISSING_DOMAIN + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + self.assertRaises(exception.ValidationError, rp.process, assertion) + + def test_rule_engine_blacklist_direct_group_mapping_missing_domain(self): + """Test if the local rule is rejected upon missing domain value + + This is a variation with a ``blacklist`` filter. + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MISSING_DOMAIN + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + self.assertRaises(exception.ValidationError, rp.process, assertion) + + def test_rule_engine_no_groups_allowed(self): + """Should return user mapped to no groups. + + The EMPLOYEE_ASSERTION should successfully have a match + in MAPPING_GROUPS_WHITELIST, but 'whitelist' should filter out + the group values from the assertion and thus map to no groups. + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertListEqual(mapped_properties['group_names'], []) + self.assertListEqual(mapped_properties['group_ids'], []) + self.assertEqual('tbo', mapped_properties['user']['name']) + + def test_mapping_federated_domain_specified(self): + """Test mapping engine when domain 'ephemeral' is explicitely set. + + For that, we use mapping rule MAPPING_EPHEMERAL_USER and assertion + EMPLOYEE_ASSERTION + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + def test_create_user_object_with_bad_mapping(self): + """Test if user object is created even with bad mapping. + + User objects will be created by mapping engine always as long as there + is corresponding local rule. This test shows, that even with assertion + where no group names nor ids are matched, but there is 'blind' rule for + mapping user, such object will be created. + + In this test MAPPING_EHPEMERAL_USER expects UserName set to jsmith + whereas value from assertion is 'tbo'. + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + self.assertNotIn('id', mapped_properties['user']) + self.assertNotIn('name', mapped_properties['user']) + + def test_set_ephemeral_domain_to_ephemeral_users(self): + """Test auto assigning service domain to ephemeral users. + + Test that ephemeral users will always become members of federated + service domain. The check depends on ``type`` value which must be set + to ``ephemeral`` in case of ephemeral user. + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + def test_local_user_local_domain(self): + """Test that local users can have non-service domains assigned.""" + mapping = mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject( + mapped_properties, user_type='local', + domain_id=mapping_fixtures.LOCAL_DOMAIN) + + def test_user_identifications_name(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has property type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and equal to name, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username({}, mapped_properties) + self.assertEqual('jsmith', mapped_properties['user']['id']) + self.assertEqual('jsmith', mapped_properties['user']['name']) + + def test_user_identifications_name_and_federated_domain(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has propert type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and equal to name, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username({}, mapped_properties) + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual('abc123%40example.com', + mapped_properties['user']['id']) + + def test_user_identification_id(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has propert type set ('ephemeral') + - Check if user's id is properly mapped from the assertion + - Check if user's name is properly set and equal to id, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.ADMIN_ASSERTION + mapped_properties = rp.process(assertion) + context = {'environment': {}} + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username(context, mapped_properties) + self.assertEqual('bob', mapped_properties['user']['name']) + self.assertEqual('bob', mapped_properties['user']['id']) + + def test_user_identification_id_and_name(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has proper type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and and equal to value hardcoded + in the mapping + + This test does two iterations with different assertions used as input + for the Mapping Engine. Different assertions will be matched with + different rules in the ruleset, effectively issuing different user_id + (hardcoded values). In the first iteration, the hardcoded user_id is + not url-safe and we expect Keystone to make it url safe. In the latter + iteration, provided user_id is already url-safe and we expect server + not to change it. + + """ + testcases = [(mapping_fixtures.CUSTOMER_ASSERTION, 'bwilliams'), + (mapping_fixtures.EMPLOYEE_ASSERTION, 'tbo')] + for assertion, exp_user_name in testcases: + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + context = {'environment': {}} + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username(context, mapped_properties) + self.assertEqual(exp_user_name, mapped_properties['user']['name']) + self.assertEqual('abc123%40example.com', + mapped_properties['user']['id']) diff --git a/keystone-moon/keystone/tests/unit/core.py b/keystone-moon/keystone/tests/unit/core.py index caca7dbd..e999b641 100644 --- a/keystone-moon/keystone/tests/unit/core.py +++ b/keystone-moon/keystone/tests/unit/core.py @@ -45,6 +45,7 @@ from keystone.common import config as common_cfg from keystone.common import dependency from keystone.common import kvs from keystone.common.kvs import core as kvs_core +from keystone.common import sql from keystone import config from keystone import controllers from keystone import exception @@ -145,8 +146,9 @@ def remove_generated_paste_config(extension_name): def skip_if_cache_disabled(*sections): - """This decorator is used to skip a test if caching is disabled either - globally or for the specific section. + """This decorator is used to skip a test if caching is disabled. + + Caching can be disabled either globally or for a specific section. In the code fragment:: @@ -163,6 +165,7 @@ def skip_if_cache_disabled(*sections): If a specified configuration section does not define the `caching` option, this decorator makes the same assumption as the `should_cache_fn` in keystone.common.cache that caching should be enabled. + """ def wrapper(f): @functools.wraps(f) @@ -180,9 +183,7 @@ def skip_if_cache_disabled(*sections): def skip_if_no_multiple_domains_support(f): - """This decorator is used to skip a test if an identity driver - does not support multiple domains. - """ + """Decorator to skip tests for identity drivers limited to one domain.""" @functools.wraps(f) def wrapper(*args, **kwargs): test_obj = args[0] @@ -215,7 +216,7 @@ class TestClient(object): req = webob.Request.blank(path) req.method = method - for k, v in six.iteritems(headers): + for k, v in headers.items(): req.headers[k] = v if body: req.body = body @@ -244,6 +245,13 @@ class BaseTestCase(oslotest.BaseTestCase): super(BaseTestCase, self).setUp() self.useFixture(mockpatch.PatchObject(sys, 'exit', side_effect=UnexpectedExit)) + self.useFixture(mockpatch.PatchObject(logging.Handler, 'handleError', + side_effect=BadLog)) + + warnings.filterwarnings('error', category=DeprecationWarning, + module='^keystone\\.') + warnings.simplefilter('error', exc.SAWarning) + self.addCleanup(warnings.resetwarnings) def cleanup_instance(self, *names): """Create a function suitable for use with self.addCleanup. @@ -261,13 +269,17 @@ class BaseTestCase(oslotest.BaseTestCase): return cleanup -@dependency.requires('revoke_api') class TestCase(BaseTestCase): def config_files(self): return [] def config_overrides(self): + # NOTE(morganfainberg): enforce config_overrides can only ever be + # called a single time. + assert self.__config_overrides_called is False + self.__config_overrides_called = True + signing_certfile = 'examples/pki/certs/signing_cert.pem' signing_keyfile = 'examples/pki/private/signing_key.pem' self.config_fixture.config(group='oslo_policy', @@ -281,30 +293,20 @@ class TestCase(BaseTestCase): proxies=['keystone.tests.unit.test_cache.CacheIsolatingProxy']) self.config_fixture.config( group='catalog', - driver='keystone.catalog.backends.templated.Catalog', + driver='templated', template_file=dirs.tests('default_catalog.templates')) self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.sql.Identity') - self.config_fixture.config( group='kvs', backends=[ ('keystone.tests.unit.test_kvs.' 'KVSBackendForcedKeyMangleFixture'), 'keystone.tests.unit.test_kvs.KVSBackendFixture']) - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='signing', certfile=signing_certfile, keyfile=signing_keyfile, ca_certs='examples/pki/certs/cacert.pem') - self.config_fixture.config( - group='token', - driver='keystone.token.persistence.backends.kvs.Token') - self.config_fixture.config( - group='trust', - driver='keystone.trust.backends.sql.Trust') + self.config_fixture.config(group='token', driver='kvs') self.config_fixture.config( group='saml', certfile=signing_certfile, keyfile=signing_keyfile) self.config_fixture.config( @@ -327,28 +329,21 @@ class TestCase(BaseTestCase): self.auth_plugin_config_override() def auth_plugin_config_override(self, methods=None, **method_classes): - if methods is None: - methods = ['external', 'password', 'token', ] - if not method_classes: - method_classes = dict( - external='keystone.auth.plugins.external.DefaultDomain', - password='keystone.auth.plugins.password.Password', - token='keystone.auth.plugins.token.Token', - ) - self.config_fixture.config(group='auth', methods=methods) - common_cfg.setup_authentication() + if methods is not None: + self.config_fixture.config(group='auth', methods=methods) + common_cfg.setup_authentication() if method_classes: self.config_fixture.config(group='auth', **method_classes) + def _assert_config_overrides_called(self): + assert self.__config_overrides_called is True + def setUp(self): super(TestCase, self).setUp() - self.addCleanup(self.cleanup_instance('config_fixture', 'logger')) - + self.__config_overrides_called = False self.addCleanup(CONF.reset) - - self.useFixture(mockpatch.PatchObject(logging.Handler, 'handleError', - side_effect=BadLog)) self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + self.addCleanup(delattr, self, 'config_fixture') self.config(self.config_files()) # NOTE(morganfainberg): mock the auth plugin setup to use the config @@ -356,13 +351,15 @@ class TestCase(BaseTestCase): # cleanup. def mocked_register_auth_plugin_opt(conf, opt): self.config_fixture.register_opt(opt, group='auth') - self.register_auth_plugin_opt_patch = self.useFixture( - mockpatch.PatchObject(common_cfg, '_register_auth_plugin_opt', - new=mocked_register_auth_plugin_opt)) + self.useFixture(mockpatch.PatchObject( + common_cfg, '_register_auth_plugin_opt', + new=mocked_register_auth_plugin_opt)) self.config_overrides() + # NOTE(morganfainberg): ensure config_overrides has been called. + self.addCleanup(self._assert_config_overrides_called) - self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) # NOTE(morganfainberg): This code is a copy from the oslo-incubator # log module. This is not in a function or otherwise available to use @@ -374,11 +371,6 @@ class TestCase(BaseTestCase): logger = logging.getLogger(mod) logger.setLevel(level_name) - warnings.filterwarnings('error', category=DeprecationWarning, - module='^keystone\\.') - warnings.simplefilter('error', exc.SAWarning) - self.addCleanup(warnings.resetwarnings) - self.useFixture(ksfixtures.Cache()) # Clear the registry of providers so that providers from previous @@ -397,6 +389,7 @@ class TestCase(BaseTestCase): self.addCleanup(setattr, controllers, '_VERSIONS', []) def config(self, config_files): + sql.initialize() CONF(args=[], project='keystone', default_config_files=config_files) def load_backends(self): @@ -417,9 +410,9 @@ class TestCase(BaseTestCase): drivers, _unused = common.setup_backends( load_extra_backends_fn=self.load_extra_backends) - for manager_name, manager in six.iteritems(drivers): + for manager_name, manager in drivers.items(): setattr(self, manager_name, manager) - self.addCleanup(self.cleanup_instance(*drivers.keys())) + self.addCleanup(self.cleanup_instance(*list(drivers.keys()))) def load_extra_backends(self): """Override to load managers that aren't loaded by default. @@ -541,15 +534,9 @@ class TestCase(BaseTestCase): def assertNotEmpty(self, l): self.assertTrue(len(l)) - def assertDictEqual(self, d1, d2, msg=None): - self.assertIsInstance(d1, dict) - self.assertIsInstance(d2, dict) - self.assertEqual(d1, d2, msg) - def assertRaisesRegexp(self, expected_exception, expected_regexp, callable_obj, *args, **kwargs): - """Asserts that the message in a raised exception matches a regexp. - """ + """Asserts that the message in a raised exception matches a regexp.""" try: callable_obj(*args, **kwargs) except expected_exception as exc_value: @@ -573,43 +560,6 @@ class TestCase(BaseTestCase): excName = str(expected_exception) raise self.failureException("%s not raised" % excName) - def assertDictContainsSubset(self, expected, actual, msg=None): - """Checks whether actual is a superset of expected.""" - - def safe_repr(obj, short=False): - _MAX_LENGTH = 80 - try: - result = repr(obj) - except Exception: - result = object.__repr__(obj) - if not short or len(result) < _MAX_LENGTH: - return result - return result[:_MAX_LENGTH] + ' [truncated]...' - - missing = [] - mismatched = [] - for key, value in six.iteritems(expected): - if key not in actual: - missing.append(key) - elif value != actual[key]: - mismatched.append('%s, expected: %s, actual: %s' % - (safe_repr(key), safe_repr(value), - safe_repr(actual[key]))) - - if not (missing or mismatched): - return - - standardMsg = '' - if missing: - standardMsg = 'Missing: %s' % ','.join(safe_repr(m) for m in - missing) - if mismatched: - if standardMsg: - standardMsg += '; ' - standardMsg += 'Mismatched values: %s' % ','.join(mismatched) - - self.fail(self._formatMessage(msg, standardMsg)) - @property def ipv6_enabled(self): if socket.has_ipv6: @@ -640,21 +590,9 @@ class SQLDriverOverrides(object): def config_overrides(self): super(SQLDriverOverrides, self).config_overrides() # SQL specific driver overrides - self.config_fixture.config( - group='catalog', - driver='keystone.catalog.backends.sql.Catalog') - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.sql.Identity') - self.config_fixture.config( - group='policy', - driver='keystone.policy.backends.sql.Policy') - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.sql.Revoke') - self.config_fixture.config( - group='token', - driver='keystone.token.persistence.backends.sql.Token') - self.config_fixture.config( - group='trust', - driver='keystone.trust.backends.sql.Trust') + self.config_fixture.config(group='catalog', driver='sql') + self.config_fixture.config(group='identity', driver='sql') + self.config_fixture.config(group='policy', driver='sql') + self.config_fixture.config(group='revoke', driver='sql') + self.config_fixture.config(group='token', driver='sql') + self.config_fixture.config(group='trust', driver='sql') diff --git a/keystone-moon/keystone/tests/unit/default_fixtures.py b/keystone-moon/keystone/tests/unit/default_fixtures.py index f7e2064f..80b0665f 100644 --- a/keystone-moon/keystone/tests/unit/default_fixtures.py +++ b/keystone-moon/keystone/tests/unit/default_fixtures.py @@ -25,6 +25,7 @@ TENANTS = [ 'description': 'description', 'enabled': True, 'parent_id': None, + 'is_domain': False, }, { 'id': 'baz', 'name': 'BAZ', @@ -32,6 +33,7 @@ TENANTS = [ 'description': 'description', 'enabled': True, 'parent_id': None, + 'is_domain': False, }, { 'id': 'mtu', 'name': 'MTU', @@ -39,6 +41,7 @@ TENANTS = [ 'enabled': True, 'domain_id': DEFAULT_DOMAIN_ID, 'parent_id': None, + 'is_domain': False, }, { 'id': 'service', 'name': 'service', @@ -46,6 +49,7 @@ TENANTS = [ 'enabled': True, 'domain_id': DEFAULT_DOMAIN_ID, 'parent_id': None, + 'is_domain': False, } ] diff --git a/keystone-moon/keystone/tests/unit/fakeldap.py b/keystone-moon/keystone/tests/unit/fakeldap.py index 85aaadfe..2f1ebe57 100644 --- a/keystone-moon/keystone/tests/unit/fakeldap.py +++ b/keystone-moon/keystone/tests/unit/fakeldap.py @@ -87,7 +87,7 @@ def _internal_attr(attr_name, value_or_values): return [attr_fn(value_or_values)] -def _match_query(query, attrs): +def _match_query(query, attrs, attrs_checked): """Match an ldap query to an attribute dictionary. The characters &, |, and ! are supported in the query. No syntax checking @@ -102,12 +102,14 @@ def _match_query(query, attrs): matchfn = any # cut off the & or | groups = _paren_groups(inner[1:]) - return matchfn(_match_query(group, attrs) for group in groups) + return matchfn(_match_query(group, attrs, attrs_checked) + for group in groups) if inner.startswith('!'): # cut off the ! and the nested parentheses - return not _match_query(query[2:-1], attrs) + return not _match_query(query[2:-1], attrs, attrs_checked) (k, _sep, v) = inner.partition('=') + attrs_checked.add(k.lower()) return _match(k, v, attrs) @@ -210,7 +212,7 @@ FakeShelves = {} class FakeLdap(core.LDAPHandler): - '''Emulate the python-ldap API. + """Emulate the python-ldap API. The python-ldap API requires all strings to be UTF-8 encoded. This is assured by the caller of this interface @@ -223,7 +225,8 @@ class FakeLdap(core.LDAPHandler): strings, decodes them to unicode for operations internal to this emulation, and encodes them back to UTF-8 when returning values from the emulation. - ''' + + """ __prefix = 'ldap:' @@ -254,7 +257,7 @@ class FakeLdap(core.LDAPHandler): ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile) elif tls_cacertdir: ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir) - if tls_req_cert in core.LDAP_TLS_CERTS.values(): + if tls_req_cert in list(core.LDAP_TLS_CERTS.values()): ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert) else: raise ValueError("invalid TLS_REQUIRE_CERT tls_req_cert=%s", @@ -356,7 +359,7 @@ class FakeLdap(core.LDAPHandler): return self.delete_ext_s(dn, serverctrls=[]) def _getChildren(self, dn): - return [k for k, v in six.iteritems(self.db) + return [k for k, v in self.db.items() if re.match('%s.*,%s' % ( re.escape(self.__prefix), re.escape(self.dn(dn))), k)] @@ -451,6 +454,10 @@ class FakeLdap(core.LDAPHandler): if server_fail: raise ldap.SERVER_DOWN + if (not filterstr) and (scope != ldap.SCOPE_BASE): + raise AssertionError('Search without filter on onelevel or ' + 'subtree scope') + if scope == ldap.SCOPE_BASE: try: item_dict = self.db[self.key(base)] @@ -473,7 +480,7 @@ class FakeLdap(core.LDAPHandler): raise ldap.NO_SUCH_OBJECT results = [(base, item_dict)] extraresults = [(k[len(self.__prefix):], v) - for k, v in six.iteritems(self.db) + for k, v in self.db.items() if re.match('%s.*,%s' % (re.escape(self.__prefix), re.escape(self.dn(base))), k)] @@ -484,7 +491,7 @@ class FakeLdap(core.LDAPHandler): base_dn = ldap.dn.str2dn(core.utf8_encode(base)) base_len = len(base_dn) - for k, v in six.iteritems(self.db): + for k, v in self.db.items(): if not k.startswith(self.__prefix): continue k_dn_str = k[len(self.__prefix):] @@ -509,9 +516,15 @@ class FakeLdap(core.LDAPHandler): id_val = core.utf8_decode(id_val) match_attrs = attrs.copy() match_attrs[id_attr] = [id_val] - if not filterstr or _match_query(filterstr, match_attrs): + attrs_checked = set() + if not filterstr or _match_query(filterstr, match_attrs, + attrs_checked): + if (filterstr and + (scope != ldap.SCOPE_BASE) and + ('objectclass' not in attrs_checked)): + raise AssertionError('No objectClass in search filter') # filter the attributes by attrlist - attrs = {k: v for k, v in six.iteritems(attrs) + attrs = {k: v for k, v in attrs.items() if not attrlist or k in attrlist} objects.append((dn, attrs)) @@ -536,11 +549,11 @@ class FakeLdap(core.LDAPHandler): class FakeLdapPool(FakeLdap): - '''Emulate the python-ldap API with pooled connections using existing - FakeLdap logic. + """Emulate the python-ldap API with pooled connections. This class is used as connector class in PooledLDAPHandler. - ''' + + """ def __init__(self, uri, retry_max=None, retry_delay=None, conn=None): super(FakeLdapPool, self).__init__(conn=conn) @@ -571,7 +584,7 @@ class FakeLdapPool(FakeLdap): clientctrls=clientctrls) def unbind_ext_s(self): - '''Added to extend FakeLdap as connector class.''' + """Added to extend FakeLdap as connector class.""" pass diff --git a/keystone-moon/keystone/tests/unit/filtering.py b/keystone-moon/keystone/tests/unit/filtering.py index 1a31a23f..93e0bc28 100644 --- a/keystone-moon/keystone/tests/unit/filtering.py +++ b/keystone-moon/keystone/tests/unit/filtering.py @@ -15,6 +15,7 @@ import uuid from oslo_config import cfg +from six.moves import range CONF = cfg.CONF @@ -41,20 +42,50 @@ class FilterTests(object): self.assertTrue(found) def _create_entity(self, entity_type): + """Find the create_<entity_type> method. + + Searches through the [identity_api, resource_api, assignment_api] + managers for a method called create_<entity_type> and returns the first + one. + + """ + f = getattr(self.identity_api, 'create_%s' % entity_type, None) if f is None: + f = getattr(self.resource_api, 'create_%s' % entity_type, None) + if f is None: f = getattr(self.assignment_api, 'create_%s' % entity_type) return f def _delete_entity(self, entity_type): + """Find the delete_<entity_type> method. + + Searches through the [identity_api, resource_api, assignment_api] + managers for a method called delete_<entity_type> and returns the first + one. + + """ + f = getattr(self.identity_api, 'delete_%s' % entity_type, None) if f is None: + f = getattr(self.resource_api, 'delete_%s' % entity_type, None) + if f is None: f = getattr(self.assignment_api, 'delete_%s' % entity_type) return f def _list_entities(self, entity_type): + """Find the list_<entity_type> method. + + Searches through the [identity_api, resource_api, assignment_api] + managers for a method called list_<entity_type> and returns the first + one. + + """ + f = getattr(self.identity_api, 'list_%ss' % entity_type, None) if f is None: + f = getattr(self.resource_api, 'list_%ss' % entity_type, None) + if f is None: f = getattr(self.assignment_api, 'list_%ss' % entity_type) return f diff --git a/keystone-moon/keystone/tests/unit/identity/test_core.py b/keystone-moon/keystone/tests/unit/identity/test_core.py index 6c8faebb..fa95ec50 100644 --- a/keystone-moon/keystone/tests/unit/identity/test_core.py +++ b/keystone-moon/keystone/tests/unit/identity/test_core.py @@ -12,11 +12,13 @@ """Unit tests for core identity behavior.""" +import itertools import os import uuid import mock from oslo_config import cfg +from oslo_config import fixture as config_fixture from keystone import exception from keystone import identity @@ -34,7 +36,10 @@ class TestDomainConfigs(tests.BaseTestCase): self.addCleanup(CONF.reset) self.tmp_dir = tests.dirs.tmp() - CONF.set_override('domain_config_dir', self.tmp_dir, 'identity') + + self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + self.config_fixture.config(domain_config_dir=self.tmp_dir, + group='identity') def test_config_for_nonexistent_domain(self): """Having a config for a non-existent domain will be ignored. @@ -80,6 +85,45 @@ class TestDomainConfigs(tests.BaseTestCase): [domain_config_filename], 'abc.def.com') + def test_config_for_multiple_sql_backend(self): + domains_config = identity.DomainConfigs() + + # Create the right sequence of is_sql in the drivers being + # requested to expose the bug, which is that a False setting + # means it forgets previous True settings. + drivers = [] + files = [] + for idx, is_sql in enumerate((True, False, True)): + drv = mock.Mock(is_sql=is_sql) + drivers.append(drv) + name = 'dummy.{0}'.format(idx) + files.append(''.join(( + identity.DOMAIN_CONF_FHEAD, + name, + identity.DOMAIN_CONF_FTAIL))) + + walk_fake = lambda *a, **kwa: ( + ('/fake/keystone/domains/config', [], files), ) + + generic_driver = mock.Mock(is_sql=False) + + assignment_api = mock.Mock() + id_factory = itertools.count() + assignment_api.get_domain_by_name.side_effect = ( + lambda name: {'id': next(id_factory), '_': 'fake_domain'}) + load_driver_mock = mock.Mock(side_effect=drivers) + + with mock.patch.object(os, 'walk', walk_fake): + with mock.patch.object(identity.cfg, 'ConfigOpts'): + with mock.patch.object(domains_config, '_load_driver', + load_driver_mock): + self.assertRaises( + exception.MultipleSQLDriversInConfig, + domains_config.setup_domain_drivers, + generic_driver, assignment_api) + + self.assertEqual(3, load_driver_mock.call_count) + class TestDatabaseDomainConfigs(tests.TestCase): @@ -92,15 +136,16 @@ class TestDatabaseDomainConfigs(tests.TestCase): self.assertFalse(CONF.identity.domain_configurations_from_database) def test_loading_config_from_database(self): - CONF.set_override('domain_configurations_from_database', True, - 'identity') + self.config_fixture.config(domain_configurations_from_database=True, + group='identity') domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} self.resource_api.create_domain(domain['id'], domain) # Override two config options for our domain conf = {'ldap': {'url': uuid.uuid4().hex, - 'suffix': uuid.uuid4().hex}, + 'suffix': uuid.uuid4().hex, + 'use_tls': 'True'}, 'identity': { - 'driver': 'keystone.identity.backends.ldap.Identity'}} + 'driver': 'ldap'}} self.domain_config_api.create_config(domain['id'], conf) fake_standard_driver = None domain_config = identity.DomainConfigs() @@ -112,6 +157,11 @@ class TestDatabaseDomainConfigs(tests.TestCase): self.assertEqual(conf['ldap']['suffix'], res.ldap.suffix) self.assertEqual(CONF.ldap.query_scope, res.ldap.query_scope) + # Make sure the override is not changing the type of the config value + use_tls_type = type(CONF.ldap.use_tls) + self.assertEqual(use_tls_type(conf['ldap']['use_tls']), + res.ldap.use_tls) + # Now turn off using database domain configuration and check that the # default config file values are now seen instead of the overrides. CONF.set_override('domain_configurations_from_database', False, @@ -122,4 +172,5 @@ class TestDatabaseDomainConfigs(tests.TestCase): res = domain_config.get_domain_conf(domain['id']) self.assertEqual(CONF.ldap.url, res.ldap.url) self.assertEqual(CONF.ldap.suffix, res.ldap.suffix) + self.assertEqual(CONF.ldap.use_tls, res.ldap.use_tls) self.assertEqual(CONF.ldap.query_scope, res.ldap.query_scope) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/database.py b/keystone-moon/keystone/tests/unit/ksfixtures/database.py index 15597539..0012df74 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/database.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/database.py @@ -13,15 +13,12 @@ import functools import os -import shutil import fixtures from oslo_config import cfg from oslo_db import options as db_options -from oslo_db.sqlalchemy import migration from keystone.common import sql -from keystone.common.sql import migration_helpers from keystone.tests import unit as tests @@ -42,23 +39,6 @@ def run_once(f): return wrapper -def _setup_database(extensions=None): - if CONF.database.connection != tests.IN_MEM_DB_CONN_STRING: - db = tests.dirs.tmp('test.db') - pristine = tests.dirs.tmp('test.db.pristine') - - if os.path.exists(db): - os.unlink(db) - if not os.path.exists(pristine): - migration.db_sync(sql.get_engine(), - migration_helpers.find_migrate_repo()) - for extension in (extensions or []): - migration_helpers.sync_database_to_version(extension=extension) - shutil.copyfile(db, pristine) - else: - shutil.copyfile(pristine, db) - - # NOTE(I159): Every execution all the options will be cleared. The method must # be called at the every fixture initialization. def initialize_sql_session(): @@ -108,17 +88,18 @@ class Database(fixtures.Fixture): """ - def __init__(self, extensions=None): + def __init__(self): super(Database, self).__init__() - self._extensions = extensions initialize_sql_session() _load_sqlalchemy_models() def setUp(self): super(Database, self).setUp() - _setup_database(extensions=self._extensions) self.engine = sql.get_engine() - sql.ModelBase.metadata.create_all(bind=self.engine) self.addCleanup(sql.cleanup) + sql.ModelBase.metadata.create_all(bind=self.engine) self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + + def recreate(self): + sql.ModelBase.metadata.create_all(bind=self.engine) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py b/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py index 47ef6b4b..918087ad 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py @@ -118,8 +118,8 @@ class HackingCode(fixtures.Fixture): import logging as stlib_logging from keystone.i18n import _ from keystone.i18n import _ as oslo_i18n - from keystone.openstack.common import log - from keystone.openstack.common import log as oslo_logging + from oslo_log import log + from oslo_log import log as oslo_logging # stdlib logging L0 = logging.getLogger() @@ -138,7 +138,7 @@ class HackingCode(fixtures.Fixture): ) # oslo logging and specifying a logger - L2 = log.getLogger(__name__) + L2 = logging.getLogger(__name__) L2.debug(oslo_i18n('text')) # oslo logging w/ alias @@ -179,84 +179,6 @@ class HackingCode(fixtures.Fixture): ] } - oslo_namespace_imports = { - 'code': """ - import oslo.utils - import oslo_utils - import oslo.utils.encodeutils - import oslo_utils.encodeutils - from oslo import utils - from oslo.utils import encodeutils - from oslo_utils import encodeutils - - import oslo.serialization - import oslo_serialization - import oslo.serialization.jsonutils - import oslo_serialization.jsonutils - from oslo import serialization - from oslo.serialization import jsonutils - from oslo_serialization import jsonutils - - import oslo.messaging - import oslo_messaging - import oslo.messaging.conffixture - import oslo_messaging.conffixture - from oslo import messaging - from oslo.messaging import conffixture - from oslo_messaging import conffixture - - import oslo.db - import oslo_db - import oslo.db.api - import oslo_db.api - from oslo import db - from oslo.db import api - from oslo_db import api - - import oslo.config - import oslo_config - import oslo.config.cfg - import oslo_config.cfg - from oslo import config - from oslo.config import cfg - from oslo_config import cfg - - import oslo.i18n - import oslo_i18n - import oslo.i18n.log - import oslo_i18n.log - from oslo import i18n - from oslo.i18n import log - from oslo_i18n import log - """, - 'expected_errors': [ - (1, 0, 'K333'), - (3, 0, 'K333'), - (5, 0, 'K333'), - (6, 0, 'K333'), - (9, 0, 'K333'), - (11, 0, 'K333'), - (13, 0, 'K333'), - (14, 0, 'K333'), - (17, 0, 'K333'), - (19, 0, 'K333'), - (21, 0, 'K333'), - (22, 0, 'K333'), - (25, 0, 'K333'), - (27, 0, 'K333'), - (29, 0, 'K333'), - (30, 0, 'K333'), - (33, 0, 'K333'), - (35, 0, 'K333'), - (37, 0, 'K333'), - (38, 0, 'K333'), - (41, 0, 'K333'), - (43, 0, 'K333'), - (45, 0, 'K333'), - (46, 0, 'K333'), - ], - } - dict_constructor = { 'code': """ lower_res = {k.lower(): v for k, v in six.iteritems(res[1])} @@ -285,8 +207,8 @@ class HackingLogging(fixtures.Fixture): from keystone.i18n import _LE as error_hint from keystone.i18n import _LI from keystone.i18n import _LW - from keystone.openstack.common import log - from keystone.openstack.common import log as oslo_logging + from oslo_log import log + from oslo_log import log as oslo_logging """ examples = [ diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py b/keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py index d1ac2ab4..7784bddc 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import shutil -import tempfile - import fixtures from keystone.token.providers.fernet import utils @@ -25,8 +22,7 @@ class KeyRepository(fixtures.Fixture): def setUp(self): super(KeyRepository, self).setUp() - directory = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, directory) + directory = self.useFixture(fixtures.TempDir()).path self.config_fixture.config(group='fernet_tokens', key_repository=directory) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/ldapdb.py b/keystone-moon/keystone/tests/unit/ksfixtures/ldapdb.py new file mode 100644 index 00000000..b2cbe067 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/ldapdb.py @@ -0,0 +1,36 @@ +# 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 fixtures + +from keystone.common import ldap as common_ldap +from keystone.common.ldap import core as common_ldap_core +from keystone.tests.unit import fakeldap + + +class LDAPDatabase(fixtures.Fixture): + """A fixture for setting up and tearing down an LDAP database. + """ + + def setUp(self): + super(LDAPDatabase, self).setUp() + self.clear() + common_ldap_core._HANDLERS.clear() + common_ldap.register_handler('fake://', fakeldap.FakeLdap) + # TODO(dstanek): switch the flow here + self.addCleanup(self.clear) + self.addCleanup(common_ldap_core._HANDLERS.clear) + + def clear(self): + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() diff --git a/keystone-moon/keystone/tests/unit/mapping_fixtures.py b/keystone-moon/keystone/tests/unit/mapping_fixtures.py index 0892ada5..f86d9245 100644 --- a/keystone-moon/keystone/tests/unit/mapping_fixtures.py +++ b/keystone-moon/keystone/tests/unit/mapping_fixtures.py @@ -12,6 +12,9 @@ """Fixtures for Federation Mapping.""" +from six.moves import range, zip + + EMPLOYEE_GROUP_ID = "0cd5e9" CONTRACTOR_GROUP_ID = "85a868" TESTER_GROUP_ID = "123" @@ -786,6 +789,7 @@ MAPPING_USER_IDS = { { "user": { "name": "{0}", + "id": "abc123@example.com", "domain": { "id": "federated" } @@ -828,7 +832,7 @@ MAPPING_USER_IDS = { "local": [ { "user": { - "id": "abc123", + "id": "abc123@example.com", "name": "{0}", "domain": { "id": "federated" @@ -963,6 +967,7 @@ TESTER_ASSERTION = { } ANOTHER_TESTER_ASSERTION = { + 'Email': 'testacct@example.com', 'UserName': 'IamTester' } @@ -989,8 +994,8 @@ MALFORMED_TESTER_ASSERTION = { 'LastName': 'Account', 'orgPersonType': 'Tester', 'object': object(), - 'dictionary': dict(zip('teststring', xrange(10))), - 'tuple': tuple(xrange(5)) + 'dictionary': dict(zip('teststring', range(10))), + 'tuple': tuple(range(5)) } DEVELOPER_ASSERTION = { diff --git a/keystone-moon/keystone/tests/unit/rest.py b/keystone-moon/keystone/tests/unit/rest.py index 16513024..bfa52354 100644 --- a/keystone-moon/keystone/tests/unit/rest.py +++ b/keystone-moon/keystone/tests/unit/rest.py @@ -13,7 +13,6 @@ # under the License. from oslo_serialization import jsonutils -import six import webtest from keystone.auth import controllers as auth_controllers @@ -61,7 +60,7 @@ class RestfulTestCase(tests.TestCase): # Will need to reset the plug-ins self.addCleanup(setattr, auth_controllers, 'AUTH_METHODS', {}) - self.useFixture(database.Database(extensions=self.get_extensions())) + self.useFixture(database.Database()) self.load_backends() self.load_fixtures(default_fixtures) @@ -75,7 +74,7 @@ class RestfulTestCase(tests.TestCase): def request(self, app, path, body=None, headers=None, token=None, expected_status=None, **kwargs): if headers: - headers = {str(k): str(v) for k, v in six.iteritems(headers)} + headers = {str(k): str(v) for k, v in headers.items()} else: headers = {} @@ -119,7 +118,7 @@ class RestfulTestCase(tests.TestCase): self.assertEqual( response.status_code, expected_status, - 'Status code %s is not %s, as expected)\n\n%s' % + 'Status code %s is not %s, as expected\n\n%s' % (response.status_code, expected_status, response.body)) def assertValidResponseHeaders(self, response): diff --git a/keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml b/keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml index 410f9388..414ff9cf 100644 --- a/keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml +++ b/keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml @@ -49,15 +49,21 @@ UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk=</ns1:X509Certificate> </ns0:AuthnContext> </ns0:AuthnStatement> <ns0:AttributeStatement> - <ns0:Attribute FriendlyName="keystone_user" Name="user" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <ns0:Attribute Name="openstack_user" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:AttributeValue xsi:type="xs:string">test_user</ns0:AttributeValue> </ns0:Attribute> - <ns0:Attribute FriendlyName="keystone_roles" Name="roles" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <ns0:Attribute Name="openstack_user_domain" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <ns0:AttributeValue xsi:type="xs:string">user_domain</ns0:AttributeValue> + </ns0:Attribute> + <ns0:Attribute Name="openstack_roles" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:AttributeValue xsi:type="xs:string">admin</ns0:AttributeValue> <ns0:AttributeValue xsi:type="xs:string">member</ns0:AttributeValue> </ns0:Attribute> - <ns0:Attribute FriendlyName="keystone_project" Name="project" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <ns0:Attribute Name="openstack_project" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> <ns0:AttributeValue xsi:type="xs:string">development</ns0:AttributeValue> </ns0:Attribute> + <ns0:Attribute Name="openstack_project_domain" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <ns0:AttributeValue xsi:type="xs:string">project_domain</ns0:AttributeValue> + </ns0:Attribute> </ns0:AttributeStatement> </ns0:Assertion> diff --git a/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py b/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py index e0159b76..9cde704e 100644 --- a/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py +++ b/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py @@ -17,8 +17,6 @@ import uuid from testtools import matchers -# NOTE(morganfainberg): import endpoint filter to populate the SQL model -from keystone.contrib import endpoint_filter # noqa from keystone.tests.unit import test_v3 @@ -30,9 +28,7 @@ class TestExtensionCase(test_v3.RestfulTestCase): def config_overrides(self): super(TestExtensionCase, self).config_overrides() self.config_fixture.config( - group='catalog', - driver='keystone.contrib.endpoint_filter.backends.catalog_sql.' - 'EndpointFilterCatalog') + group='catalog', driver='endpoint_filter.sql') def setUp(self): super(TestExtensionCase, self).setUp() @@ -52,7 +48,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): """ self.put(self.default_request_url, - body='', expected_status=204) def test_create_endpoint_project_association_with_invalid_project(self): @@ -65,7 +60,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': uuid.uuid4().hex, 'endpoint_id': self.endpoint_id}, - body='', expected_status=404) def test_create_endpoint_project_association_with_invalid_endpoint(self): @@ -78,7 +72,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': self.default_domain_project_id, 'endpoint_id': uuid.uuid4().hex}, - body='', expected_status=404) def test_create_endpoint_project_association_with_unexpected_body(self): @@ -98,7 +91,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): """ self.put(self.default_request_url, - body='', expected_status=204) self.head('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { @@ -117,7 +109,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': uuid.uuid4().hex, 'endpoint_id': self.endpoint_id}, - body='', expected_status=404) def test_check_endpoint_project_association_with_invalid_endpoint(self): @@ -131,7 +122,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': self.default_domain_project_id, 'endpoint_id': uuid.uuid4().hex}, - body='', expected_status=404) def test_list_endpoints_associated_with_valid_project(self): @@ -156,7 +146,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): self.put(self.default_request_url) self.get('/OS-EP-FILTER/projects/%(project_id)s/endpoints' % { 'project_id': uuid.uuid4().hex}, - body='', expected_status=404) def test_list_projects_associated_with_endpoint(self): @@ -217,7 +206,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': uuid.uuid4().hex, 'endpoint_id': self.endpoint_id}, - body='', expected_status=404) def test_remove_endpoint_project_association_with_invalid_endpoint(self): @@ -231,7 +219,6 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': self.default_domain_project_id, 'endpoint_id': uuid.uuid4().hex}, - body='', expected_status=404) def test_endpoint_project_association_cleanup_when_project_deleted(self): @@ -289,7 +276,6 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': project['id'], 'endpoint_id': self.endpoint_id}, - body='', expected_status=204) # attempt to authenticate without requesting a project @@ -311,7 +297,6 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], 'endpoint_id': self.endpoint_id}, - body='', expected_status=204) auth_data = self.build_authentication_request( @@ -327,65 +312,12 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.assertEqual(r.result['token']['project']['id'], self.project['id']) - def test_project_scoped_token_with_no_catalog_using_endpoint_filter(self): - """Verify endpoint filter when project scoped token returns no catalog. - - Test that the project scoped token response is valid for a given - endpoint-project association when no service catalog is returned. - - """ - # create a project to work with - ref = self.new_project_ref(domain_id=self.domain_id) - r = self.post('/projects', body={'project': ref}) - project = self.assertValidProjectResponse(r, ref) - - # grant the user a role on the project - self.put( - '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { - 'user_id': self.user['id'], - 'project_id': project['id'], - 'role_id': self.role['id']}) - - # set the user's preferred project - body = {'user': {'default_project_id': project['id']}} - r = self.patch('/users/%(user_id)s' % { - 'user_id': self.user['id']}, - body=body) - self.assertValidUserResponse(r) - - # add one endpoint to the project - self.put('/OS-EP-FILTER/projects/%(project_id)s' - '/endpoints/%(endpoint_id)s' % { - 'project_id': project['id'], - 'endpoint_id': self.endpoint_id}, - body='', - expected_status=204) - - # attempt to authenticate without requesting a project - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password']) - r = self.post('/auth/tokens?nocatalog', body=auth_data) - self.assertValidProjectScopedTokenResponse( - r, - require_catalog=False, - endpoint_filter=True, - ep_filter_assoc=1) - self.assertEqual(r.result['token']['project']['id'], project['id']) - - def test_default_scoped_token_with_no_catalog_using_endpoint_filter(self): - """Verify endpoint filter when default scoped token returns no catalog. - - Test that the default project scoped token response is valid for a - given endpoint-project association when no service catalog is returned. - - """ - # add one endpoint to default project + def test_scoped_token_with_no_catalog_using_endpoint_filter(self): + """Verify endpoint filter does not affect no catalog.""" self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], 'endpoint_id': self.endpoint_id}, - body='', expected_status=204) auth_data = self.build_authentication_request( @@ -395,65 +327,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): r = self.post('/auth/tokens?nocatalog', body=auth_data) self.assertValidProjectScopedTokenResponse( r, - require_catalog=False, - endpoint_filter=True, - ep_filter_assoc=1) - self.assertEqual(r.result['token']['project']['id'], - self.project['id']) - - def test_project_scoped_token_with_no_endpoint_project_association(self): - """Verify endpoint filter when no endpoint-project association. - - Test that the project scoped token response is valid when there are - no endpoint-project associations defined. - - """ - # create a project to work with - ref = self.new_project_ref(domain_id=self.domain_id) - r = self.post('/projects', body={'project': ref}) - project = self.assertValidProjectResponse(r, ref) - - # grant the user a role on the project - self.put( - '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { - 'user_id': self.user['id'], - 'project_id': project['id'], - 'role_id': self.role['id']}) - - # set the user's preferred project - body = {'user': {'default_project_id': project['id']}} - r = self.patch('/users/%(user_id)s' % { - 'user_id': self.user['id']}, - body=body) - self.assertValidUserResponse(r) - - # attempt to authenticate without requesting a project - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password']) - r = self.post('/auth/tokens?nocatalog', body=auth_data) - self.assertValidProjectScopedTokenResponse( - r, - require_catalog=False, - endpoint_filter=True) - self.assertEqual(r.result['token']['project']['id'], project['id']) - - def test_default_scoped_token_with_no_endpoint_project_association(self): - """Verify endpoint filter when no endpoint-project association. - - Test that the default project scoped token response is valid when - there are no endpoint-project associations defined. - - """ - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - project_id=self.project['id']) - r = self.post('/auth/tokens?nocatalog', body=auth_data) - self.assertValidProjectScopedTokenResponse( - r, - require_catalog=False, - endpoint_filter=True,) + require_catalog=False) self.assertEqual(r.result['token']['project']['id'], self.project['id']) @@ -464,7 +338,6 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], 'endpoint_id': self.endpoint_id}, - body='', expected_status=204) # create a second temporary endpoint @@ -480,7 +353,6 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], 'endpoint_id': self.endpoint_id2}, - body='', expected_status=204) # remove the temporary reference @@ -576,6 +448,30 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): endpoint_filter=True, ep_filter_assoc=2) + def test_get_auth_catalog_using_endpoint_filter(self): + # add one endpoint to default project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': self.endpoint_id}, + expected_status=204) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + token_data = self.post('/auth/tokens', body=auth_data) + self.assertValidProjectScopedTokenResponse( + token_data, + require_catalog=True, + endpoint_filter=True, + ep_filter_assoc=1) + + auth_catalog = self.get('/auth/catalog', + token=token_data.headers['X-Subject-Token']) + self.assertEqual(token_data.result['token']['catalog'], + auth_catalog.result['catalog']) + class JsonHomeTests(TestExtensionCase, test_v3.JsonHomeTestMixin): JSON_HOME_DATA = { @@ -635,6 +531,16 @@ class JsonHomeTests(TestExtensionCase, test_v3.JsonHomeTestMixin): 'ext/OS-EP-FILTER/1.0/param/endpoint_group_id', }, }, + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' + '1.0/rel/project_endpoint_groups': { + 'href-template': '/OS-EP-FILTER/projects/{project_id}/' + 'endpoint_groups', + 'href-vars': { + 'project_id': + 'http://docs.openstack.org/api/openstack-identity/3/param/' + 'project_id', + }, + }, } @@ -883,6 +789,40 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): endpoint_group_id, project_id) self.get(url, expected_status=404) + def test_list_endpoint_groups_in_project(self): + """GET /OS-EP-FILTER/projects/{project_id}/endpoint_groups.""" + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # associate endpoint group with project + url = self._get_project_endpoint_group_url( + endpoint_group_id, self.project_id) + self.put(url) + + url = ('/OS-EP-FILTER/projects/%(project_id)s/endpoint_groups' % + {'project_id': self.project_id}) + response = self.get(url) + + self.assertEqual( + endpoint_group_id, + response.result['endpoint_groups'][0]['id']) + + def test_list_endpoint_groups_in_invalid_project(self): + """Test retrieving from invalid project.""" + project_id = uuid.uuid4().hex + url = ('/OS-EP-FILTER/projects/%(project_id)s/endpoint_groups' % + {'project_id': project_id}) + self.get(url, expected_status=404) + + def test_empty_endpoint_groups_in_project(self): + """Test when no endpoint groups associated with the project.""" + url = ('/OS-EP-FILTER/projects/%(project_id)s/endpoint_groups' % + {'project_id': self.project_id}) + response = self.get(url) + + self.assertEqual(0, len(response.result['endpoint_groups'])) + def test_check_endpoint_group_to_project(self): """Test HEAD with a valid endpoint group and project association.""" endpoint_group_id = self._create_valid_endpoint_group( @@ -1088,6 +1028,25 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): self.delete(url) self.get(url, expected_status=404) + def test_remove_endpoint_group_with_project_association(self): + # create an endpoint group + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # create an endpoint_group project + project_endpoint_group_url = self._get_project_endpoint_group_url( + endpoint_group_id, self.default_domain_project_id) + self.put(project_endpoint_group_url) + + # remove endpoint group, the associated endpoint_group project will + # be removed as well. + endpoint_group_url = ('/OS-EP-FILTER/endpoint_groups/' + '%(endpoint_group_id)s' + % {'endpoint_group_id': endpoint_group_id}) + self.delete(endpoint_group_url) + self.get(endpoint_group_url, expected_status=404) + self.get(project_endpoint_group_url, expected_status=404) + def _create_valid_endpoint_group(self, url, body): r = self.post(url, body=body) return r.result['endpoint_group']['id'] diff --git a/keystone-moon/keystone/tests/unit/test_auth.py b/keystone-moon/keystone/tests/unit/test_auth.py index 295e028d..f253b02d 100644 --- a/keystone-moon/keystone/tests/unit/test_auth.py +++ b/keystone-moon/keystone/tests/unit/test_auth.py @@ -18,7 +18,9 @@ import uuid import mock from oslo_config import cfg +import oslo_utils.fixture from oslo_utils import timeutils +import six from testtools import matchers from keystone import assignment @@ -74,6 +76,7 @@ class AuthTest(tests.TestCase): def setUp(self): self.useFixture(database.Database()) super(AuthTest, self).setUp() + self.time_fixture = self.useFixture(oslo_utils.fixture.TimeFixture()) self.load_backends() self.load_fixtures(default_fixtures) @@ -265,12 +268,12 @@ class AuthWithToken(AuthTest): self.user_foo['id'], self.tenant_bar['id'], self.role_member['id']) - # Get an unscoped tenant + # Get an unscoped token body_dict = _build_user_auth( username='FOO', password='foo2') unscoped_token = self.controller.authenticate({}, body_dict) - # Get a token on BAR tenant using the unscoped tenant + # Get a token on BAR tenant using the unscoped token body_dict = _build_user_auth( token=unscoped_token["access"]["token"], tenant_name="BAR") @@ -281,6 +284,50 @@ class AuthWithToken(AuthTest): self.assertEqual(self.tenant_bar['id'], tenant["id"]) self.assertThat(roles, matchers.Contains(self.role_member['id'])) + def test_auth_scoped_token_bad_project_with_debug(self): + """Authenticating with an invalid project fails.""" + # Bug 1379952 reports poor user feedback, even in debug mode, + # when the user accidentally passes a project name as an ID. + # This test intentionally does exactly that. + body_dict = _build_user_auth( + username=self.user_foo['name'], + password=self.user_foo['password'], + tenant_id=self.tenant_bar['name']) + + # with debug enabled, this produces a friendly exception. + self.config_fixture.config(debug=True) + e = self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + {}, body_dict) + # explicitly verify that the error message shows that a *name* is + # found where an *ID* is expected + self.assertIn( + 'Project ID not found: %s' % self.tenant_bar['name'], + six.text_type(e)) + + def test_auth_scoped_token_bad_project_without_debug(self): + """Authenticating with an invalid project fails.""" + # Bug 1379952 reports poor user feedback, even in debug mode, + # when the user accidentally passes a project name as an ID. + # This test intentionally does exactly that. + body_dict = _build_user_auth( + username=self.user_foo['name'], + password=self.user_foo['password'], + tenant_id=self.tenant_bar['name']) + + # with debug disabled, authentication failure details are suppressed. + self.config_fixture.config(debug=False) + e = self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + {}, body_dict) + # explicitly verify that the error message details above have been + # suppressed. + self.assertNotIn( + 'Project ID not found: %s' % self.tenant_bar['name'], + six.text_type(e)) + def test_auth_token_project_group_role(self): """Verify getting a token in a tenant with group roles.""" # Add a v2 style role in so we can check we get this back @@ -448,10 +495,13 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth(username='FOO', password='foo2') unscoped_token = self.controller.authenticate(context, body_dict) token_id = unscoped_token['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) + # get a second token body_dict = _build_user_auth(token=unscoped_token["access"]["token"]) unscoped_token_2 = self.controller.authenticate(context, body_dict) token_2_id = unscoped_token_2['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) self.token_provider_api.revoke_token(token_id, revoke_chain=True) @@ -470,10 +520,13 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth(username='FOO', password='foo2') unscoped_token = self.controller.authenticate(context, body_dict) token_id = unscoped_token['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) + # get a second token body_dict = _build_user_auth(token=unscoped_token["access"]["token"]) unscoped_token_2 = self.controller.authenticate(context, body_dict) token_2_id = unscoped_token_2['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) self.token_provider_api.revoke_token(token_2_id, revoke_chain=True) @@ -500,13 +553,17 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth(username='FOO', password='foo2') unscoped_token = self.controller.authenticate(context, body_dict) token_id = unscoped_token['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) + # get a second token body_dict = _build_user_auth( token=unscoped_token['access']['token']) unscoped_token_2 = self.controller.authenticate(context, body_dict) token_2_id = unscoped_token_2['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) self.token_provider_api.revoke_token(token_id, revoke_chain=True) + self.time_fixture.advance_time_seconds(1) revoke_events = self.revoke_api.list_events() self.assertThat(revoke_events, matchers.HasLength(1)) @@ -526,15 +583,18 @@ class AuthWithToken(AuthTest): body_dict = _build_user_auth(username='FOO', password='foo2') unscoped_token = self.controller.authenticate(context, body_dict) token_id = unscoped_token['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) # get a second token body_dict = _build_user_auth( token=unscoped_token['access']['token']) unscoped_token_2 = self.controller.authenticate(context, body_dict) token_2_id = unscoped_token_2['access']['token']['id'] + self.time_fixture.advance_time_seconds(1) # Revoke by audit_id, no audit_info means both parent and child # token are revoked. self.token_provider_api.revoke_token(token_id) + self.time_fixture.advance_time_seconds(1) revoke_events = self.revoke_api.list_events() self.assertThat(revoke_events, matchers.HasLength(2)) @@ -819,9 +879,8 @@ class AuthWithTrust(AuthTest): context, trust=self.sample_data) def test_create_trust(self): - expires_at = timeutils.strtime(timeutils.utcnow() + - datetime.timedelta(minutes=10), - fmt=TIME_FORMAT) + expires_at = (timeutils.utcnow() + + datetime.timedelta(minutes=10)).strftime(TIME_FORMAT) new_trust = self.create_trust(self.sample_data, self.trustor['name'], expires_at=expires_at) self.assertEqual(self.trustor['id'], new_trust['trustor_user_id']) @@ -848,6 +907,12 @@ class AuthWithTrust(AuthTest): self.create_trust, self.sample_data, self.trustor['name'], expires_at="Z") + def test_create_trust_expires_older_than_now(self): + self.assertRaises(exception.ValidationExpirationError, + self.create_trust, self.sample_data, + self.trustor['name'], + expires_at="2010-06-04T08:44:31.999999Z") + def test_create_trust_without_project_id(self): """Verify that trust can be created without project id and token can be generated with that trust. @@ -868,8 +933,8 @@ class AuthWithTrust(AuthTest): def test_get_trust(self): unscoped_token = self.get_unscoped_token(self.trustor['name']) - context = {'token_id': unscoped_token['access']['token']['id'], - 'host_url': HOST_URL} + context = self._create_auth_context( + unscoped_token['access']['token']['id']) new_trust = self.trust_controller.create_trust( context, trust=self.sample_data)['trust'] trust = self.trust_controller.get_trust(context, @@ -880,6 +945,21 @@ class AuthWithTrust(AuthTest): for role in new_trust['roles']: self.assertIn(role['id'], role_ids) + def test_get_trust_without_auth_context(self): + """Verify that a trust cannot be retrieved when the auth context is + missing. + """ + unscoped_token = self.get_unscoped_token(self.trustor['name']) + context = self._create_auth_context( + unscoped_token['access']['token']['id']) + new_trust = self.trust_controller.create_trust( + context, trust=self.sample_data)['trust'] + # Delete the auth context before calling get_trust(). + del context['environment'][authorization.AUTH_CONTEXT_ENV] + self.assertRaises(exception.Forbidden, + self.trust_controller.get_trust, context, + new_trust['id']) + def test_create_trust_no_impersonation(self): new_trust = self.create_trust(self.sample_data, self.trustor['name'], expires_at=None, impersonation=False) @@ -1051,13 +1131,18 @@ class AuthWithTrust(AuthTest): self.controller.authenticate, {}, request_body) def test_expired_trust_get_token_fails(self): - expiry = "1999-02-18T10:10:00Z" + expires_at = (timeutils.utcnow() + + datetime.timedelta(minutes=5)).strftime(TIME_FORMAT) + time_expired = timeutils.utcnow() + datetime.timedelta(minutes=10) new_trust = self.create_trust(self.sample_data, self.trustor['name'], - expiry) - request_body = self.build_v2_token_request('TWO', 'two2', new_trust) - self.assertRaises( - exception.Forbidden, - self.controller.authenticate, {}, request_body) + expires_at) + with mock.patch.object(timeutils, 'utcnow') as mock_now: + mock_now.return_value = time_expired + request_body = self.build_v2_token_request('TWO', 'two2', + new_trust) + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) def test_token_from_trust_with_wrong_role_fails(self): new_trust = self.create_trust(self.sample_data, self.trustor['name']) @@ -1196,9 +1281,7 @@ class TokenExpirationTest(AuthTest): self.assertEqual(original_expiration, r['access']['token']['expires']) def test_maintain_uuid_token_expiration(self): - self.config_fixture.config( - group='token', - provider='keystone.token.providers.uuid.Provider') + self.config_fixture.config(group='token', provider='uuid') self._maintain_token_expiration() diff --git a/keystone-moon/keystone/tests/unit/test_auth_plugin.py b/keystone-moon/keystone/tests/unit/test_auth_plugin.py index 11df95a5..a259cc2a 100644 --- a/keystone-moon/keystone/tests/unit/test_auth_plugin.py +++ b/keystone-moon/keystone/tests/unit/test_auth_plugin.py @@ -28,9 +28,6 @@ DEMO_USER_ID = uuid.uuid4().hex class SimpleChallengeResponse(auth.AuthMethodHandler): - - method = METHOD_NAME - def authenticate(self, context, auth_payload, user_context): if 'response' in auth_payload: if auth_payload['response'] != EXPECTED_RESPONSE: @@ -40,20 +37,6 @@ class SimpleChallengeResponse(auth.AuthMethodHandler): return {"challenge": "What's the name of your high school?"} -class DuplicateAuthPlugin(SimpleChallengeResponse): - """Duplicate simple challenge response auth plugin.""" - - -class MismatchedAuthPlugin(SimpleChallengeResponse): - method = uuid.uuid4().hex - - -class NoMethodAuthPlugin(auth.AuthMethodHandler): - """An auth plugin that does not supply a method attribute.""" - def authenticate(self, context, auth_payload, auth_context): - pass - - class TestAuthPlugin(tests.SQLDriverOverrides, tests.TestCase): def setUp(self): super(TestAuthPlugin, self).setUp() @@ -64,9 +47,6 @@ class TestAuthPlugin(tests.SQLDriverOverrides, tests.TestCase): def config_overrides(self): super(TestAuthPlugin, self).config_overrides() method_opts = { - 'external': 'keystone.auth.plugins.external.DefaultDomain', - 'password': 'keystone.auth.plugins.password.Password', - 'token': 'keystone.auth.plugins.token.Token', METHOD_NAME: 'keystone.tests.unit.test_auth_plugin.SimpleChallengeResponse', } @@ -123,6 +103,14 @@ class TestAuthPlugin(tests.SQLDriverOverrides, tests.TestCase): auth_info, auth_context) + def test_duplicate_method(self): + # Having the same method twice doesn't cause load_auth_methods to fail. + self.auth_plugin_config_override( + methods=['external', 'external']) + self.clear_auth_plugin_registry() + auth.controllers.load_auth_methods() + self.assertIn('external', auth.controllers.AUTH_METHODS) + class TestAuthPluginDynamicOptions(TestAuthPlugin): def config_overrides(self): @@ -137,25 +125,6 @@ class TestAuthPluginDynamicOptions(TestAuthPlugin): return config_files -class TestInvalidAuthMethodRegistration(tests.TestCase): - def test_duplicate_auth_method_registration(self): - self.config_fixture.config( - group='auth', - methods=[ - 'keystone.tests.unit.test_auth_plugin.SimpleChallengeResponse', - 'keystone.tests.unit.test_auth_plugin.DuplicateAuthPlugin']) - self.clear_auth_plugin_registry() - self.assertRaises(ValueError, auth.controllers.load_auth_methods) - - def test_no_method_attribute_auth_method_by_class_name_registration(self): - self.config_fixture.config( - group='auth', - methods=['keystone.tests.unit.test_auth_plugin.NoMethodAuthPlugin'] - ) - self.clear_auth_plugin_registry() - self.assertRaises(ValueError, auth.controllers.load_auth_methods) - - class TestMapped(tests.TestCase): def setUp(self): super(TestMapped, self).setUp() @@ -168,8 +137,9 @@ class TestMapped(tests.TestCase): config_files.append(tests.dirs.tests_conf('test_auth_plugin.conf')) return config_files - def config_overrides(self): - # don't override configs so we can use test_auth_plugin.conf only + def auth_plugin_config_override(self, methods=None, **method_classes): + # Do not apply the auth plugin overrides so that the config file is + # tested pass def _test_mapped_invocation_with_method_name(self, method_name): diff --git a/keystone-moon/keystone/tests/unit/test_backend.py b/keystone-moon/keystone/tests/unit/test_backend.py index 6cf06494..45b8e0b0 100644 --- a/keystone-moon/keystone/tests/unit/test_backend.py +++ b/keystone-moon/keystone/tests/unit/test_backend.py @@ -22,6 +22,7 @@ import mock from oslo_config import cfg from oslo_utils import timeutils import six +from six.moves import range from testtools import matchers from keystone.catalog import core @@ -505,7 +506,7 @@ class IdentityTests(object): 'fake2') def test_list_role_assignments_unfiltered(self): - """Test for unfiltered listing role assignments. + """Test unfiltered listing of role assignments. Test Plan: @@ -533,9 +534,6 @@ class IdentityTests(object): # First check how many role grants already exist existing_assignments = len(self.assignment_api.list_role_assignments()) - existing_assignments_for_role = len( - self.assignment_api.list_role_assignments_for_role( - role_id='admin')) # Now create the grants (roles are defined in default_fixtures) self.assignment_api.create_grant(user_id=new_user['id'], @@ -573,6 +571,48 @@ class IdentityTests(object): 'role_id': 'admin'}, assignment_list) + def test_list_role_assignments_filtered_by_role(self): + """Test listing of role assignments filtered by role ID. + + Test Plan: + + - Create a user, group & project + - Find how many role assignments already exist (from default + fixtures) + - Create a grant of each type (user/group on project/domain) + - Check that if we list assignments by role_id, then we get back + assignments that only contain that role. + + """ + new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': DEFAULT_DOMAIN_ID} + new_user = self.identity_api.create_user(new_user) + new_group = {'domain_id': DEFAULT_DOMAIN_ID, 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(new_project['id'], new_project) + + # First check how many role grants already exist + existing_assignments_for_role = len( + self.assignment_api.list_role_assignments_for_role( + role_id='admin')) + + # Now create the grants (roles are defined in default_fixtures) + self.assignment_api.create_grant(user_id=new_user['id'], + domain_id=DEFAULT_DOMAIN_ID, + role_id='member') + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=new_project['id'], + role_id='other') + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=DEFAULT_DOMAIN_ID, + role_id='admin') + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=new_project['id'], + role_id='admin') + # Read back the list of assignments for just the admin role, checking # this only goes up by two. assignment_list = self.assignment_api.list_role_assignments_for_role( @@ -582,7 +622,7 @@ class IdentityTests(object): # Now check that each of our two new entries are in the list self.assertIn( - {'group_id': new_group['id'], 'domain_id': new_domain['id'], + {'group_id': new_group['id'], 'domain_id': DEFAULT_DOMAIN_ID, 'role_id': 'admin'}, assignment_list) self.assertIn( @@ -598,8 +638,7 @@ class IdentityTests(object): def get_member_assignments(): assignments = self.assignment_api.list_role_assignments() - return filter(lambda x: x['role_id'] == MEMBER_ROLE_ID, - assignments) + return [x for x in assignments if x['role_id'] == MEMBER_ROLE_ID] orig_member_assignments = get_member_assignments() @@ -627,8 +666,8 @@ class IdentityTests(object): expected_member_assignments = orig_member_assignments + [{ 'group_id': new_group['id'], 'project_id': new_project['id'], 'role_id': MEMBER_ROLE_ID}] - self.assertThat(new_member_assignments, - matchers.Equals(expected_member_assignments)) + self.assertItemsEqual(expected_member_assignments, + new_member_assignments) def test_list_role_assignments_bad_role(self): assignment_list = self.assignment_api.list_role_assignments_for_role( @@ -1976,6 +2015,16 @@ class IdentityTests(object): project['id'], project) + def test_create_project_invalid_domain_id(self): + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': uuid.uuid4().hex, + 'enabled': True} + self.assertRaises(exception.DomainNotFound, + self.resource_api.create_project, + project['id'], + project) + def test_create_user_invalid_enabled_type_string(self): user = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, @@ -2079,7 +2128,7 @@ class IdentityTests(object): # Create a project project = {'id': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, 'name': uuid.uuid4().hex, 'description': uuid.uuid4().hex, - 'enabled': True, 'parent_id': None} + 'enabled': True, 'parent_id': None, 'is_domain': False} self.resource_api.create_project(project['id'], project) # Build driver hints with the project's name and inexistent description @@ -2131,12 +2180,15 @@ class IdentityTests(object): self.assertIn(project2['id'], project_ids) def _create_projects_hierarchy(self, hierarchy_size=2, - domain_id=DEFAULT_DOMAIN_ID): + domain_id=DEFAULT_DOMAIN_ID, + is_domain=False): """Creates a project hierarchy with specified size. :param hierarchy_size: the desired hierarchy size, default is 2 - a project with one child. :param domain_id: domain where the projects hierarchy will be created. + :param is_domain: if the hierarchy will have the is_domain flag active + or not. :returns projects: a list of the projects in the created hierarchy. @@ -2144,26 +2196,195 @@ class IdentityTests(object): project_id = uuid.uuid4().hex project = {'id': project_id, 'description': '', - 'domain_id': domain_id, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': None} + 'parent_id': None, + 'domain_id': domain_id, + 'is_domain': is_domain} self.resource_api.create_project(project_id, project) projects = [project] for i in range(1, hierarchy_size): new_project = {'id': uuid.uuid4().hex, 'description': '', - 'domain_id': domain_id, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': project_id} + 'parent_id': project_id, + 'is_domain': is_domain} + new_project['domain_id'] = domain_id + self.resource_api.create_project(new_project['id'], new_project) projects.append(new_project) project_id = new_project['id'] return projects + @tests.skip_if_no_multiple_domains_support + def test_create_domain_with_project_api(self): + project_id = uuid.uuid4().hex + project = {'id': project_id, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None, + 'is_domain': True} + ref = self.resource_api.create_project(project['id'], project) + self.assertTrue(ref['is_domain']) + self.assertEqual(DEFAULT_DOMAIN_ID, ref['domain_id']) + + @tests.skip_if_no_multiple_domains_support + @test_utils.wip('waiting for projects acting as domains implementation') + def test_is_domain_sub_project_has_parent_domain_id(self): + project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None, + 'is_domain': True} + self.resource_api.create_project(project['id'], project) + + sub_project_id = uuid.uuid4().hex + sub_project = {'id': sub_project_id, + 'description': '', + 'domain_id': project['id'], + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': project['id'], + 'is_domain': True} + ref = self.resource_api.create_project(sub_project['id'], sub_project) + self.assertTrue(ref['is_domain']) + self.assertEqual(project['id'], ref['parent_id']) + self.assertEqual(project['id'], ref['domain_id']) + + @tests.skip_if_no_multiple_domains_support + @test_utils.wip('waiting for projects acting as domains implementation') + def test_delete_domain_with_project_api(self): + project_id = uuid.uuid4().hex + project = {'id': project_id, + 'description': '', + 'domain_id': None, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None, + 'is_domain': True} + self.resource_api.create_project(project['id'], project) + + # Try to delete is_domain project that is enabled + self.assertRaises(exception.ValidationError, + self.resource_api.delete_project, + project['id']) + + # Disable the project + project['enabled'] = False + self.resource_api.update_project(project['id'], project) + + # Successfuly delete the project + self.resource_api.delete_project(project['id']) + + @tests.skip_if_no_multiple_domains_support + @test_utils.wip('waiting for projects acting as domains implementation') + def test_create_domain_under_regular_project_hierarchy_fails(self): + # Creating a regular project hierarchy. Projects acting as domains + # can't have a parent that is a regular project. + projects_hierarchy = self._create_projects_hierarchy() + parent = projects_hierarchy[1] + project_id = uuid.uuid4().hex + project = {'id': project_id, + 'description': '', + 'domain_id': parent['id'], + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': parent['id'], + 'is_domain': True} + + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], project) + + @tests.skip_if_no_multiple_domains_support + @test_utils.wip('waiting for projects acting as domains implementation') + def test_create_project_under_domain_hierarchy(self): + projects_hierarchy = self._create_projects_hierarchy(is_domain=True) + parent = projects_hierarchy[1] + project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': parent['id'], + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': parent['id'], + 'is_domain': False} + + ref = self.resource_api.create_project(project['id'], project) + self.assertFalse(ref['is_domain']) + self.assertEqual(parent['id'], ref['parent_id']) + self.assertEqual(parent['id'], ref['domain_id']) + + def test_create_project_without_is_domain_flag(self): + project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None} + + ref = self.resource_api.create_project(project['id'], project) + # The is_domain flag should be False by default + self.assertFalse(ref['is_domain']) + + def test_create_is_domain_project(self): + project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None, + 'is_domain': True} + + ref = self.resource_api.create_project(project['id'], project) + self.assertTrue(ref['is_domain']) + + @test_utils.wip('waiting for projects acting as domains implementation') + def test_create_project_with_parent_id_and_without_domain_id(self): + project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': None, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None} + self.resource_api.create_project(project['id'], project) + + sub_project = {'id': uuid.uuid4().hex, + 'description': '', + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': project['id']} + ref = self.resource_api.create_project(sub_project['id'], sub_project) + + # The domain_id should be set to the parent domain_id + self.assertEqual(project['domain_id'], ref['domain_id']) + + @test_utils.wip('waiting for projects acting as domains implementation') + def test_create_project_with_domain_id_and_without_parent_id(self): + project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': None, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None} + self.resource_api.create_project(project['id'], project) + + sub_project = {'id': uuid.uuid4().hex, + 'description': '', + 'enabled': True, + 'domain_id': project['id'], + 'name': uuid.uuid4().hex} + ref = self.resource_api.create_project(sub_project['id'], sub_project) + + # The parent_id should be set to the domain_id + self.assertEqual(ref['parent_id'], project['id']) + def test_check_leaf_projects(self): projects_hierarchy = self._create_projects_hierarchy() root_project = projects_hierarchy[0] @@ -2191,7 +2412,8 @@ class IdentityTests(object): 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': project2['id']} + 'parent_id': project2['id'], + 'is_domain': False} self.resource_api.create_project(project4['id'], project4) subtree = self.resource_api.list_projects_in_subtree(project1['id']) @@ -2208,6 +2430,48 @@ class IdentityTests(object): subtree = self.resource_api.list_projects_in_subtree(project3['id']) self.assertEqual(0, len(subtree)) + def test_list_projects_in_subtree_with_circular_reference(self): + project1_id = uuid.uuid4().hex + project2_id = uuid.uuid4().hex + + project1 = {'id': project1_id, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex} + self.resource_api.create_project(project1['id'], project1) + + project2 = {'id': project2_id, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': project1_id} + self.resource_api.create_project(project2['id'], project2) + + project1['parent_id'] = project2_id # Adds cyclic reference + + # NOTE(dstanek): The manager does not allow parent_id to be updated. + # Instead will directly use the driver to create the cyclic + # reference. + self.resource_api.driver.update_project(project1_id, project1) + + subtree = self.resource_api.list_projects_in_subtree(project1_id) + + # NOTE(dstanek): If a cyclic refence is detected the code bails + # and returns None instead of falling into the infinite + # recursion trap. + self.assertIsNone(subtree) + + def test_list_projects_in_subtree_invalid_project_id(self): + self.assertRaises(exception.ValidationError, + self.resource_api.list_projects_in_subtree, + None) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.list_projects_in_subtree, + uuid.uuid4().hex) + def test_list_project_parents(self): projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) project1 = projects_hierarchy[0] @@ -2218,7 +2482,8 @@ class IdentityTests(object): 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': project2['id']} + 'parent_id': project2['id'], + 'is_domain': False} self.resource_api.create_project(project4['id'], project4) parents1 = self.resource_api.list_project_parents(project3['id']) @@ -2232,6 +2497,15 @@ class IdentityTests(object): parents = self.resource_api.list_project_parents(project1['id']) self.assertEqual(0, len(parents)) + def test_list_project_parents_invalid_project_id(self): + self.assertRaises(exception.ValidationError, + self.resource_api.list_project_parents, + None) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.list_project_parents, + uuid.uuid4().hex) + def test_delete_project_with_role_assignments(self): tenant = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID} @@ -2812,29 +3086,36 @@ class IdentityTests(object): 'description': '', 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, - 'parent_id': 'fake'} + 'parent_id': 'fake', + 'is_domain': False} self.assertRaises(exception.ProjectNotFound, self.resource_api.create_project, project['id'], project) - def test_create_leaf_project_with_invalid_domain(self): + @tests.skip_if_no_multiple_domains_support + def test_create_leaf_project_with_different_domain(self): root_project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, 'description': '', 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(root_project['id'], root_project) + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + self.resource_api.create_domain(domain['id'], domain) leaf_project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, 'description': '', - 'domain_id': 'fake', + 'domain_id': domain['id'], 'enabled': True, - 'parent_id': root_project['id']} + 'parent_id': root_project['id'], + 'is_domain': False} - self.assertRaises(exception.ForbiddenAction, + self.assertRaises(exception.ValidationError, self.resource_api.create_project, leaf_project['id'], leaf_project) @@ -2883,17 +3164,19 @@ class IdentityTests(object): 'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': False, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(project1['id'], project1) project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, - 'parent_id': project1['id']} + 'parent_id': project1['id'], + 'is_domain': False} # It's not possible to create a project under a disabled one in the # hierarchy - self.assertRaises(exception.ForbiddenAction, + self.assertRaises(exception.ValidationError, self.resource_api.create_project, project2['id'], project2) @@ -2955,7 +3238,8 @@ class IdentityTests(object): 'id': project_id, 'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, - 'parent_id': leaf_project['id']} + 'parent_id': leaf_project['id'], + 'is_domain': False} self.assertRaises(exception.ForbiddenAction, self.resource_api.create_project, project_id, @@ -2967,7 +3251,8 @@ class IdentityTests(object): 'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(project['id'], project) # Add a description attribute. @@ -2983,7 +3268,8 @@ class IdentityTests(object): 'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(project['id'], project) # Add a description attribute. @@ -3427,8 +3713,7 @@ class IdentityTests(object): def get_member_assignments(): assignments = self.assignment_api.list_role_assignments() - return filter(lambda x: x['role_id'] == MEMBER_ROLE_ID, - assignments) + return [x for x in assignments if x['role_id'] == MEMBER_ROLE_ID] orig_member_assignments = get_member_assignments() @@ -3662,16 +3947,16 @@ class IdentityTests(object): domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} self.resource_api.create_domain(domain2['id'], domain2) project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain1['id']} + 'domain_id': domain1['id'], 'is_domain': False} project1 = self.resource_api.create_project(project1['id'], project1) project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain1['id']} + 'domain_id': domain1['id'], 'is_domain': False} project2 = self.resource_api.create_project(project2['id'], project2) project3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain1['id']} + 'domain_id': domain1['id'], 'is_domain': False} project3 = self.resource_api.create_project(project3['id'], project3) project4 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain2['id']} + 'domain_id': domain2['id'], 'is_domain': False} project4 = self.resource_api.create_project(project4['id'], project4) group_list = [] role_list = [] @@ -4291,7 +4576,9 @@ class TrustTests(object): trust_data = self.trust_api.get_trust(trust_id) self.assertEqual(new_id, trust_data['id']) self.trust_api.delete_trust(trust_id) - self.assertIsNone(self.trust_api.get_trust(trust_id)) + self.assertRaises(exception.TrustNotFound, + self.trust_api.get_trust, + trust_id) def test_delete_trust_not_found(self): trust_id = uuid.uuid4().hex @@ -4314,7 +4601,9 @@ class TrustTests(object): self.assertIsNotNone(trust_data) self.assertIsNone(trust_data['deleted_at']) self.trust_api.delete_trust(new_id) - self.assertIsNone(self.trust_api.get_trust(new_id)) + self.assertRaises(exception.TrustNotFound, + self.trust_api.get_trust, + new_id) deleted_trust = self.trust_api.get_trust(trust_data['id'], deleted=True) self.assertEqual(trust_data['id'], deleted_trust['id']) @@ -4389,7 +4678,9 @@ class TrustTests(object): self.assertEqual(1, t['remaining_uses']) self.trust_api.consume_use(trust_data['id']) # This was the last use, the trust isn't available anymore - self.assertIsNone(self.trust_api.get_trust(trust_data['id'])) + self.assertRaises(exception.TrustNotFound, + self.trust_api.get_trust, + trust_data['id']) class CatalogTests(object): @@ -4907,7 +5198,6 @@ class CatalogTests(object): endpoint = { 'id': uuid.uuid4().hex, - 'region_id': None, 'service_id': service['id'], 'interface': 'public', 'url': uuid.uuid4().hex, @@ -5007,6 +5297,29 @@ class CatalogTests(object): return service_ref, enabled_endpoint_ref, disabled_endpoint_ref + def test_list_endpoints(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service.copy()) + + expected_ids = set([uuid.uuid4().hex for _ in range(3)]) + for endpoint_id in expected_ids: + endpoint = { + 'id': endpoint_id, + 'region_id': None, + 'service_id': service['id'], + 'interface': 'public', + 'url': uuid.uuid4().hex, + } + self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy()) + + endpoints = self.catalog_api.list_endpoints() + self.assertEqual(expected_ids, set(e['id'] for e in endpoints)) + def test_get_catalog_endpoint_disabled(self): """Get back only enabled endpoints when get the v2 catalog.""" @@ -5157,6 +5470,77 @@ class PolicyTests(object): class InheritanceTests(object): + def _test_crud_inherited_and_direct_assignment(self, **kwargs): + """Tests inherited and direct assignments for the actor and target + + Ensure it is possible to create both inherited and direct role + assignments for the same actor on the same target. The actor and the + target are specified in the kwargs as ('user_id' or 'group_id') and + ('project_id' or 'domain_id'), respectively. + + """ + + # Create a new role to avoid assignments loaded from default fixtures + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = self.role_api.create_role(role['id'], role) + + # Define the common assigment entity + assignment_entity = {'role_id': role['id']} + assignment_entity.update(kwargs) + + # Define assignments under test + direct_assignment_entity = assignment_entity.copy() + inherited_assignment_entity = assignment_entity.copy() + inherited_assignment_entity['inherited_to_projects'] = 'projects' + + # Create direct assignment and check grants + self.assignment_api.create_grant(inherited_to_projects=False, + **assignment_entity) + + grants = self.assignment_api.list_role_assignments_for_role(role['id']) + self.assertThat(grants, matchers.HasLength(1)) + self.assertIn(direct_assignment_entity, grants) + + # Now add inherited assignment and check grants + self.assignment_api.create_grant(inherited_to_projects=True, + **assignment_entity) + + grants = self.assignment_api.list_role_assignments_for_role(role['id']) + self.assertThat(grants, matchers.HasLength(2)) + self.assertIn(direct_assignment_entity, grants) + self.assertIn(inherited_assignment_entity, grants) + + # Delete both and check grants + self.assignment_api.delete_grant(inherited_to_projects=False, + **assignment_entity) + self.assignment_api.delete_grant(inherited_to_projects=True, + **assignment_entity) + + grants = self.assignment_api.list_role_assignments_for_role(role['id']) + self.assertEqual([], grants) + + def test_crud_inherited_and_direct_assignment_for_user_on_domain(self): + self._test_crud_inherited_and_direct_assignment( + user_id=self.user_foo['id'], domain_id=DEFAULT_DOMAIN_ID) + + def test_crud_inherited_and_direct_assignment_for_group_on_domain(self): + group = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID} + group = self.identity_api.create_group(group) + + self._test_crud_inherited_and_direct_assignment( + group_id=group['id'], domain_id=DEFAULT_DOMAIN_ID) + + def test_crud_inherited_and_direct_assignment_for_user_on_project(self): + self._test_crud_inherited_and_direct_assignment( + user_id=self.user_foo['id'], project_id=self.tenant_baz['id']) + + def test_crud_inherited_and_direct_assignment_for_group_on_project(self): + group = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID} + group = self.identity_api.create_group(group) + + self._test_crud_inherited_and_direct_assignment( + group_id=group['id'], project_id=self.tenant_baz['id']) + def test_inherited_role_grants_for_user(self): """Test inherited user roles. @@ -5375,14 +5759,16 @@ class InheritanceTests(object): 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(root_project['id'], root_project) leaf_project = {'id': uuid.uuid4().hex, 'description': '', 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': root_project['id']} + 'parent_id': root_project['id'], + 'is_domain': False} self.resource_api.create_project(leaf_project['id'], leaf_project) user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, @@ -5496,14 +5882,16 @@ class InheritanceTests(object): 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(root_project['id'], root_project) leaf_project = {'id': uuid.uuid4().hex, 'description': '', 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True, 'name': uuid.uuid4().hex, - 'parent_id': root_project['id']} + 'parent_id': root_project['id'], + 'is_domain': False} self.resource_api.create_project(leaf_project['id'], leaf_project) user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, @@ -5663,6 +6051,65 @@ class FilterTests(filtering.FilterTests): self._delete_test_data('user', user_list) self._delete_test_data('group', group_list) + def _get_user_name_field_size(self): + """Return the size of the user name field for the backend. + + Subclasses can override this method to indicate that the user name + field is limited in length. The user name is the field used in the test + that validates that a filter value works even if it's longer than a + field. + + If the backend doesn't limit the value length then return None. + + """ + return None + + def test_filter_value_wider_than_field(self): + # If a filter value is given that's larger than the field in the + # backend then no values are returned. + + user_name_field_size = self._get_user_name_field_size() + + if user_name_field_size is None: + # The backend doesn't limit the size of the user name, so pass this + # test. + return + + # Create some users just to make sure would return something if the + # filter was ignored. + self._create_test_data('user', 2) + + hints = driver_hints.Hints() + value = 'A' * (user_name_field_size + 1) + hints.add_filter('name', value) + users = self.identity_api.list_users(hints=hints) + self.assertEqual([], users) + + def test_list_users_in_group_filtered(self): + number_of_users = 10 + user_name_data = { + 1: 'Arthur Conan Doyle', + 3: 'Arthur Rimbaud', + 9: 'Arthur Schopenhauer', + } + user_list = self._create_test_data( + 'user', number_of_users, + domain_id=DEFAULT_DOMAIN_ID, name_dict=user_name_data) + group = self._create_one_entity('group', + DEFAULT_DOMAIN_ID, 'Great Writers') + for i in range(7): + self.identity_api.add_user_to_group(user_list[i]['id'], + group['id']) + + hints = driver_hints.Hints() + hints.add_filter('name', 'Arthur', comparator='startswith') + users = self.identity_api.list_users_in_group(group['id'], hints=hints) + self.assertThat(len(users), matchers.Equals(2)) + self.assertIn(user_list[1]['id'], [users[0]['id'], users[1]['id']]) + self.assertIn(user_list[3]['id'], [users[0]['id'], users[1]['id']]) + self._delete_test_data('user', user_list) + self._delete_entity('group')(group['id']) + class LimitTests(filtering.FilterTests): ENTITIES = ['user', 'group', 'project'] diff --git a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py index cc41d977..6c2181aa 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py +++ b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py @@ -14,6 +14,7 @@ import uuid +from six.moves import range from testtools import matchers from keystone import exception diff --git a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py index dab02859..134a03f0 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py +++ b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py @@ -21,7 +21,8 @@ class SqlPolicyAssociationTable(test_backend_sql.SqlModels): """Set of tests for checking SQL Policy Association Mapping.""" def test_policy_association_mapping(self): - cols = (('policy_id', sql.String, 64), + cols = (('id', sql.String, 64), + ('policy_id', sql.String, 64), ('endpoint_id', sql.String, 64), ('service_id', sql.String, 64), ('region_id', sql.String, 64)) diff --git a/keystone-moon/keystone/tests/unit/test_backend_federation_sql.py b/keystone-moon/keystone/tests/unit/test_backend_federation_sql.py index 48ebad6c..995c564d 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_federation_sql.py +++ b/keystone-moon/keystone/tests/unit/test_backend_federation_sql.py @@ -21,11 +21,15 @@ class SqlFederation(test_backend_sql.SqlModels): def test_identity_provider(self): cols = (('id', sql.String, 64), - ('remote_id', sql.String, 256), ('enabled', sql.Boolean, None), ('description', sql.Text, None)) self.assertExpectedSchema('identity_provider', cols) + def test_idp_remote_ids(self): + cols = (('idp_id', sql.String, 64), + ('remote_id', sql.String, 255)) + self.assertExpectedSchema('idp_remote_ids', cols) + def test_federated_protocol(self): cols = (('id', sql.String, 64), ('idp_id', sql.String, 64), @@ -42,5 +46,6 @@ class SqlFederation(test_backend_sql.SqlModels): ('id', sql.String, 64), ('enabled', sql.Boolean, None), ('description', sql.Text, None), + ('relay_state_prefix', sql.String, 256), ('sp_url', sql.String, 256)) self.assertExpectedSchema('service_provider', cols) diff --git a/keystone-moon/keystone/tests/unit/test_backend_kvs.py b/keystone-moon/keystone/tests/unit/test_backend_kvs.py index c0997ad9..a22faa59 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_kvs.py +++ b/keystone-moon/keystone/tests/unit/test_backend_kvs.py @@ -18,6 +18,7 @@ from oslo_config import cfg from oslo_utils import timeutils import six +from keystone.common import utils from keystone import exception from keystone.tests import unit as tests from keystone.tests.unit import test_backend @@ -67,13 +68,13 @@ class KvsToken(tests.TestCase, test_backend.TokenTests): valid_token_ref = token_persistence.get_token(valid_token_id) expired_token_ref = token_persistence.get_token(expired_token_id) expected_user_token_list = [ - (valid_token_id, timeutils.isotime(valid_token_ref['expires'], - subsecond=True)), - (expired_token_id, timeutils.isotime(expired_token_ref['expires'], - subsecond=True))] + (valid_token_id, utils.isotime(valid_token_ref['expires'], + subsecond=True)), + (expired_token_id, utils.isotime(expired_token_ref['expires'], + subsecond=True))] self.assertEqual(expected_user_token_list, user_token_list) new_expired_data = (expired_token_id, - timeutils.isotime( + utils.isotime( (timeutils.utcnow() - expire_delta), subsecond=True)) self._update_user_token_index_direct(user_key, expired_token_id, @@ -82,10 +83,10 @@ class KvsToken(tests.TestCase, test_backend.TokenTests): user_id=user_id) valid_token_ref_2 = token_persistence.get_token(valid_token_id_2) expected_user_token_list = [ - (valid_token_id, timeutils.isotime(valid_token_ref['expires'], - subsecond=True)), - (valid_token_id_2, timeutils.isotime(valid_token_ref_2['expires'], - subsecond=True))] + (valid_token_id, utils.isotime(valid_token_ref['expires'], + subsecond=True)), + (valid_token_id_2, utils.isotime(valid_token_ref_2['expires'], + subsecond=True))] user_token_list = token_persistence.driver._store.get(user_key) self.assertEqual(expected_user_token_list, user_token_list) @@ -94,10 +95,10 @@ class KvsToken(tests.TestCase, test_backend.TokenTests): new_token_id, data = self.create_token_sample_data(user_id=user_id) new_token_ref = token_persistence.get_token(new_token_id) expected_user_token_list = [ - (valid_token_id, timeutils.isotime(valid_token_ref['expires'], - subsecond=True)), - (new_token_id, timeutils.isotime(new_token_ref['expires'], - subsecond=True))] + (valid_token_id, utils.isotime(valid_token_ref['expires'], + subsecond=True)), + (new_token_id, utils.isotime(new_token_ref['expires'], + subsecond=True))] user_token_list = token_persistence.driver._store.get(user_key) self.assertEqual(expected_user_token_list, user_token_list) @@ -110,9 +111,7 @@ class KvsCatalog(tests.TestCase, test_backend.CatalogTests): def config_overrides(self): super(KvsCatalog, self).config_overrides() - self.config_fixture.config( - group='catalog', - driver='keystone.catalog.backends.kvs.Catalog') + self.config_fixture.config(group='catalog', driver='kvs') def _load_fake_catalog(self): self.catalog_foobar = self.catalog_api.driver._create_catalog( @@ -167,6 +166,4 @@ class KvsTokenCacheInvalidation(tests.TestCase, def config_overrides(self): super(KvsTokenCacheInvalidation, self).config_overrides() - self.config_fixture.config( - group='token', - driver='keystone.token.persistence.backends.kvs.Token') + self.config_fixture.config(group='token', driver='kvs') diff --git a/keystone-moon/keystone/tests/unit/test_backend_ldap.py b/keystone-moon/keystone/tests/unit/test_backend_ldap.py index 10119808..94fb82e7 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_ldap.py +++ b/keystone-moon/keystone/tests/unit/test_backend_ldap.py @@ -20,27 +20,92 @@ import uuid import ldap import mock from oslo_config import cfg +import pkg_resources +from six.moves import range from testtools import matchers from keystone.common import cache from keystone.common import ldap as common_ldap from keystone.common.ldap import core as common_ldap_core -from keystone.common import sql from keystone import exception from keystone import identity from keystone.identity.mapping_backends import mapping as map from keystone import resource from keystone.tests import unit as tests from keystone.tests.unit import default_fixtures -from keystone.tests.unit import fakeldap from keystone.tests.unit import identity_mapping as mapping_sql from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit.ksfixtures import ldapdb from keystone.tests.unit import test_backend CONF = cfg.CONF +def _assert_backends(testcase, **kwargs): + + def _get_backend_cls(testcase, subsystem): + observed_backend = getattr(testcase, subsystem + '_api').driver + return observed_backend.__class__ + + def _get_domain_specific_backend_cls(manager, domain): + observed_backend = manager.domain_configs.get_domain_driver(domain) + return observed_backend.__class__ + + def _get_entrypoint_cls(subsystem, name): + entrypoint = entrypoint_map['keystone.' + subsystem][name] + return entrypoint.resolve() + + def _load_domain_specific_configs(manager): + if (not manager.domain_configs.configured and + CONF.identity.domain_specific_drivers_enabled): + manager.domain_configs.setup_domain_drivers( + manager.driver, manager.resource_api) + + def _assert_equal(expected_cls, observed_cls, subsystem, + domain=None): + msg = ('subsystem %(subsystem)s expected %(expected_cls)r, ' + 'but observed %(observed_cls)r') + if domain: + subsystem = '%s[domain=%s]' % (subsystem, domain) + assert expected_cls == observed_cls, msg % { + 'expected_cls': expected_cls, + 'observed_cls': observed_cls, + 'subsystem': subsystem, + } + + env = pkg_resources.Environment() + keystone_dist = env['keystone'][0] + entrypoint_map = pkg_resources.get_entry_map(keystone_dist) + + for subsystem, entrypoint_name in kwargs.items(): + if isinstance(entrypoint_name, str): + observed_cls = _get_backend_cls(testcase, subsystem) + expected_cls = _get_entrypoint_cls(subsystem, entrypoint_name) + _assert_equal(expected_cls, observed_cls, subsystem) + + elif isinstance(entrypoint_name, dict): + manager = getattr(testcase, subsystem + '_api') + _load_domain_specific_configs(manager) + + for domain, entrypoint_name in entrypoint_name.items(): + if domain is None: + observed_cls = _get_backend_cls(testcase, subsystem) + expected_cls = _get_entrypoint_cls( + subsystem, entrypoint_name) + _assert_equal(expected_cls, observed_cls, subsystem) + continue + + observed_cls = _get_domain_specific_backend_cls( + manager, domain) + expected_cls = _get_entrypoint_cls(subsystem, entrypoint_name) + _assert_equal(expected_cls, observed_cls, subsystem, domain) + + else: + raise ValueError('%r is not an expected value for entrypoint name' + % entrypoint_name) + + def create_group_container(identity_api): # Create the groups base entry (ou=Groups,cn=example,cn=com) group_api = identity_api.driver.group @@ -54,35 +119,22 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def setUp(self): super(BaseLDAPIdentity, self).setUp() - self.clear_database() + self.ldapdb = self.useFixture(ldapdb.LDAPDatabase()) - common_ldap.register_handler('fake://', fakeldap.FakeLdap) self.load_backends() self.load_fixtures(default_fixtures) - self.addCleanup(common_ldap_core._HANDLERS.clear) - def _get_domain_fixture(self): """Domains in LDAP are read-only, so just return the static one.""" return self.resource_api.get_domain(CONF.identity.default_domain_id) - def clear_database(self): - for shelf in fakeldap.FakeShelves: - fakeldap.FakeShelves[shelf].clear() - - def reload_backends(self, domain_id): - # Only one backend unless we are using separate domain backends - self.load_backends() - def get_config(self, domain_id): # Only one conf structure unless we are using separate domain backends return CONF def config_overrides(self): super(BaseLDAPIdentity, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config(group='identity', driver='ldap') def config_files(self): config_files = super(BaseLDAPIdentity, self).config_files() @@ -127,11 +179,11 @@ class BaseLDAPIdentity(test_backend.IdentityTests): user['id']) def test_configurable_forbidden_user_actions(self): - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_allow_create = False - conf.ldap.user_allow_update = False - conf.ldap.user_allow_delete = False - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.allow_create = False + driver.user.allow_update = False + driver.user.allow_delete = False user = {'name': u'fäké1', 'password': u'fäképass1', @@ -152,9 +204,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.user_foo['id']) def test_configurable_forbidden_create_existing_user(self): - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_allow_create = False - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.allow_create = False self.assertRaises(exception.ForbiddenAction, self.identity_api.create_user, @@ -165,9 +217,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.user_foo.pop('password') self.assertDictEqual(user_ref, self.user_foo) - conf = self.get_config(user_ref['domain_id']) - conf.ldap.user_filter = '(CN=DOES_NOT_MATCH)' - self.reload_backends(user_ref['domain_id']) + driver = self.identity_api._select_identity_driver( + user_ref['domain_id']) + driver.user.ldap_filter = '(CN=DOES_NOT_MATCH)' # invalidate the cache if the result is cached. self.identity_api.get_user.invalidate(self.identity_api, self.user_foo['id']) @@ -468,9 +520,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests): after_assignments = len(self.assignment_api.list_role_assignments()) self.assertEqual(existing_assignments + 2, after_assignments) + def test_list_role_assignments_filtered_by_role(self): + # Domain roles are not supported by the LDAP Assignment backend + self.assertRaises( + exception.NotImplemented, + super(BaseLDAPIdentity, self). + test_list_role_assignments_filtered_by_role) + def test_list_role_assignments_dumb_member(self): self.config_fixture.config(group='ldap', use_dumb_member=True) - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) @@ -495,7 +554,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_list_user_ids_for_project_dumb_member(self): self.config_fixture.config(group='ldap', use_dumb_member=True) - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) @@ -569,7 +628,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_list_group_members_dumb_member(self): self.config_fixture.config(group='ldap', use_dumb_member=True) - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) @@ -686,11 +745,10 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_create_user_none_mapping(self): # When create a user where an attribute maps to None, the entry is # created without that attribute and it doesn't fail with a TypeError. - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_attribute_ignore = ['enabled', 'email', - 'tenants', 'tenantId'] - self.reload_backends(CONF.identity.default_domain_id) - + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.attribute_ignore = ['enabled', 'email', + 'tenants', 'tenantId'] user = {'name': u'fäké1', 'password': u'fäképass1', 'domain_id': CONF.identity.default_domain_id, @@ -723,10 +781,10 @@ class BaseLDAPIdentity(test_backend.IdentityTests): # Ensure that an attribute that maps to None that is not explicitly # ignored in configuration is implicitly ignored without triggering # an error. - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_attribute_ignore = ['enabled', 'email', - 'tenants', 'tenantId'] - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.attribute_ignore = ['enabled', 'email', + 'tenants', 'tenantId'] user = {'name': u'fäké1', 'password': u'fäképass1', @@ -930,6 +988,10 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): # credentials) that require a database. self.useFixture(database.Database()) super(LDAPIdentity, self).setUp() + _assert_backends(self, + assignment='ldap', + identity='ldap', + resource='ldap') def load_fixtures(self, fixtures): # Override super impl since need to create group container. @@ -937,7 +999,9 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): super(LDAPIdentity, self).load_fixtures(fixtures) def test_configurable_allowed_project_actions(self): - tenant = {'id': u'fäké1', 'name': u'fäké1', 'enabled': True} + domain = self._get_domain_fixture() + tenant = {'id': u'fäké1', 'name': u'fäké1', 'enabled': True, + 'domain_id': domain['id']} self.resource_api.create_project(u'fäké1', tenant) tenant_ref = self.resource_api.get_project(u'fäké1') self.assertEqual(u'fäké1', tenant_ref['id']) @@ -990,7 +1054,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): project_allow_update=False, project_allow_delete=False) self.load_backends() - tenant = {'id': u'fäké1', 'name': u'fäké1'} + domain = self._get_domain_fixture() + tenant = {'id': u'fäké1', 'name': u'fäké1', 'domain_id': domain['id']} self.assertRaises(exception.ForbiddenAction, self.resource_api.create_project, u'fäké1', @@ -1029,7 +1094,7 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): def test_dumb_member(self): self.config_fixture.config(group='ldap', use_dumb_member=True) - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) dumb_id = common_ldap.BaseLdap._dn_to_id(CONF.ldap.dumb_member) @@ -1042,7 +1107,7 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): group='ldap', project_name_attribute='ou', project_desc_attribute='description', project_enabled_attribute='enabled') - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) # NOTE(morganfainberg): CONF.ldap.project_name_attribute, @@ -1087,7 +1152,7 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): self.config_fixture.config( group='ldap', project_attribute_ignore=['name', 'description', 'enabled']) - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) # NOTE(morganfainberg): CONF.ldap.project_attribute_ignore will not be @@ -1107,7 +1172,7 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): def test_user_enable_attribute_mask(self): self.config_fixture.config(group='ldap', user_enabled_mask=2, user_enabled_default='512') - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) @@ -1155,7 +1220,7 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): def test_user_enabled_invert(self): self.config_fixture.config(group='ldap', user_enabled_invert=True, user_enabled_default=False) - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) @@ -1426,6 +1491,26 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): new_user = [u for u in res if u['id'] == user['id']][0] self.assertThat(new_user['description'], matchers.Equals(description)) + def test_user_with_missing_id(self): + # create a user that doesn't have the id attribute + ldap_ = self.identity_api.driver.user.get_connection() + # `sn` is used for the attribute in the DN because it's allowed by + # the entry's objectclasses so that this test could conceivably run in + # the live tests. + ldap_id_field = 'sn' + ldap_id_value = uuid.uuid4().hex + dn = '%s=%s,ou=Users,cn=example,cn=com' % (ldap_id_field, + ldap_id_value) + modlist = [('objectClass', ['person', 'inetOrgPerson']), + (ldap_id_field, [ldap_id_value]), + ('mail', ['email@example.com']), + ('userPassword', [uuid.uuid4().hex])] + ldap_.add_s(dn, modlist) + + # make sure the user doesn't break other users + users = self.identity_api.driver.user.get_all() + self.assertThat(users, matchers.HasLength(len(default_fixtures.USERS))) + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') def test_user_mixed_case_attribute(self, mock_ldap_get): # Mock the search results to return attribute names @@ -1531,7 +1616,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): 'domain_id': CONF.identity.default_domain_id, 'description': uuid.uuid4().hex, 'enabled': True, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) @@ -1609,7 +1695,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): 'description': '', 'domain_id': domain['id'], 'enabled': True, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(project1['id'], project1) # Creating project2 under project1. LDAP will not allow @@ -1619,7 +1706,8 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): 'description': '', 'domain_id': domain['id'], 'enabled': True, - 'parent_id': project1['id']} + 'parent_id': project1['id'], + 'is_domain': False} self.assertRaises(exception.InvalidParentProject, self.resource_api.create_project, @@ -1633,6 +1721,58 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): # Returning projects to be used across the tests return [project1, project2] + def _assert_create_is_domain_project_not_allowed(self): + """Tests that we can't create more than one project acting as domain. + + This method will be used at any test that require the creation of a + project that act as a domain. LDAP does not support multiple domains + and the only domain it has (default) is immutable. + """ + domain = self._get_domain_fixture() + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': domain['id'], + 'enabled': True, + 'parent_id': None, + 'is_domain': True} + + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], project) + + def test_update_is_domain_field(self): + domain = self._get_domain_fixture() + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': domain['id'], + 'enabled': True, + 'parent_id': None, + 'is_domain': False} + self.resource_api.create_project(project['id'], project) + + # Try to update the is_domain field to True + project['is_domain'] = True + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], project) + + def test_delete_is_domain_project(self): + self._assert_create_is_domain_project_not_allowed() + + def test_create_domain_under_regular_project_hierarchy_fails(self): + self._assert_create_hierarchy_not_allowed() + + def test_create_not_is_domain_project_under_is_domain_hierarchy(self): + self._assert_create_hierarchy_not_allowed() + + def test_create_is_domain_project(self): + self._assert_create_is_domain_project_not_allowed() + + def test_create_project_with_parent_id_and_without_domain_id(self): + self._assert_create_hierarchy_not_allowed() + def test_check_leaf_projects(self): projects = self._assert_create_hierarchy_not_allowed() for project in projects: @@ -1642,13 +1782,17 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): projects = self._assert_create_hierarchy_not_allowed() for project in projects: subtree_list = self.resource_api.list_projects_in_subtree( - project) + project['id']) self.assertEqual(0, len(subtree_list)) + def test_list_projects_in_subtree_with_circular_reference(self): + self._assert_create_hierarchy_not_allowed() + def test_list_project_parents(self): projects = self._assert_create_hierarchy_not_allowed() for project in projects: - parents_list = self.resource_api.list_project_parents(project) + parents_list = self.resource_api.list_project_parents( + project['id']) self.assertEqual(0, len(parents_list)) def test_hierarchical_projects_crud(self): @@ -1826,9 +1970,9 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): self.assertEqual(set(expected_group_ids), group_ids) def test_user_id_attribute_in_create(self): - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_id_attribute = 'mail' - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.id_attr = 'mail' user = {'name': u'fäké1', 'password': u'fäképass1', @@ -1840,9 +1984,9 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): self.assertEqual(user_ref['id'], user_ref['email']) def test_user_id_attribute_map(self): - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_id_attribute = 'mail' - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.id_attr = 'mail' user_ref = self.identity_api.get_user(self.user_foo['email']) # the user_id_attribute map should be honored, which means @@ -1851,9 +1995,9 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') def test_get_id_from_dn_for_multivalued_attribute_id(self, mock_ldap_get): - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_id_attribute = 'mail' - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.id_attr = 'mail' # make 'email' multivalued so we can test the error condition email1 = uuid.uuid4().hex @@ -1888,10 +2032,10 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') def test_user_id_not_in_dn(self, mock_ldap_get): - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_id_attribute = 'uid' - conf.ldap.user_name_attribute = 'cn' - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.id_attr = 'uid' + driver.user.attribute_mapping['name'] = 'cn' mock_ldap_get.return_value = ( 'foo=bar,dc=example,dc=com', @@ -1908,10 +2052,10 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') def test_user_name_in_dn(self, mock_ldap_get): - conf = self.get_config(CONF.identity.default_domain_id) - conf.ldap.user_id_attribute = 'sAMAccountName' - conf.ldap.user_name_attribute = 'cn' - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.id_attr = 'SAMAccountName' + driver.user.attribute_mapping['name'] = 'cn' mock_ldap_get.return_value = ( 'cn=Foo Bar,dc=example,dc=com', @@ -1929,12 +2073,16 @@ class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): class LDAPIdentityEnabledEmulation(LDAPIdentity): def setUp(self): super(LDAPIdentityEnabledEmulation, self).setUp() - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) for obj in [self.tenant_bar, self.tenant_baz, self.user_foo, self.user_two, self.user_badguy]: obj.setdefault('enabled', True) + _assert_backends(self, + assignment='ldap', + identity='ldap', + resource='ldap') def load_fixtures(self, fixtures): # Override super impl since need to create group container. @@ -1961,7 +2109,8 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): 'name': uuid.uuid4().hex, 'domain_id': CONF.identity.default_domain_id, 'description': uuid.uuid4().hex, - 'parent_id': None} + 'parent_id': None, + 'is_domain': False} self.resource_api.create_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) @@ -2007,9 +2156,9 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): user['id']) def test_user_auth_emulated(self): - self.config_fixture.config(group='ldap', - user_enabled_emulation_dn='cn=test,dc=test') - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.user.enabled_emulation_dn = 'cn=test,dc=test' self.identity_api.authenticate( context={}, user_id=self.user_foo['id'], @@ -2022,7 +2171,7 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): def test_user_enabled_invert(self): self.config_fixture.config(group='ldap', user_enabled_invert=True, user_enabled_default=False) - self.clear_database() + self.ldapdb.clear() self.load_backends() self.load_fixtures(default_fixtures) @@ -2110,32 +2259,26 @@ class LdapIdentitySqlAssignment(BaseLDAPIdentity, tests.SQLDriverOverrides, return config_files def setUp(self): - self.useFixture(database.Database()) + sqldb = self.useFixture(database.Database()) super(LdapIdentitySqlAssignment, self).setUp() - self.clear_database() + self.ldapdb.clear() self.load_backends() cache.configure_cache_region(cache.REGION) - self.engine = sql.get_engine() - self.addCleanup(sql.cleanup) - - sql.ModelBase.metadata.create_all(bind=self.engine) - self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + sqldb.recreate() self.load_fixtures(default_fixtures) # defaulted by the data load self.user_foo['enabled'] = True + _assert_backends(self, + assignment='sql', + identity='ldap', + resource='sql') def config_overrides(self): super(LdapIdentitySqlAssignment, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - self.config_fixture.config( - group='resource', - driver='keystone.resource.backends.sql.Resource') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') + self.config_fixture.config(group='identity', driver='ldap') + self.config_fixture.config(group='resource', driver='sql') + self.config_fixture.config(group='assignment', driver='sql') def test_domain_crud(self): pass @@ -2214,6 +2357,11 @@ class LdapIdentitySqlAssignment(BaseLDAPIdentity, tests.SQLDriverOverrides, self.skipTest("Doesn't apply since LDAP configuration is ignored for " "SQL assignment backend.") + def test_list_role_assignments_filtered_by_role(self): + # Domain roles are supported by the SQL Assignment backend + base = super(BaseLDAPIdentity, self) + base.test_list_role_assignments_filtered_by_role() + class LdapIdentitySqlAssignmentWithMapping(LdapIdentitySqlAssignment): """Class to test mapping of default LDAP backend. @@ -2390,16 +2538,11 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides, """ def setUp(self): - self.useFixture(database.Database()) + sqldb = self.useFixture(database.Database()) super(MultiLDAPandSQLIdentity, self).setUp() self.load_backends() - - self.engine = sql.get_engine() - self.addCleanup(sql.cleanup) - - sql.ModelBase.metadata.create_all(bind=self.engine) - self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + sqldb.recreate() self.domain_count = 5 self.domain_specific_count = 3 @@ -2410,23 +2553,29 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides, # for separate backends per domain. self.enable_multi_domain() - self.clear_database() + self.ldapdb.clear() self.load_fixtures(default_fixtures) self.create_users_across_domains() + self.assert_backends() + + def assert_backends(self): + _assert_backends(self, + assignment='sql', + identity={ + None: 'sql', + self.domains['domain_default']['id']: 'ldap', + self.domains['domain1']['id']: 'ldap', + self.domains['domain2']['id']: 'ldap', + }, + resource='sql') def config_overrides(self): super(MultiLDAPandSQLIdentity, self).config_overrides() # Make sure identity and assignment are actually SQL drivers, # BaseLDAPIdentity sets these options to use LDAP. - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.sql.Identity') - self.config_fixture.config( - group='resource', - driver='keystone.resource.backends.sql.Resource') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') + self.config_fixture.config(group='identity', driver='sql') + self.config_fixture.config(group='resource', driver='sql') + self.config_fixture.config(group='assignment', driver='sql') def _setup_initial_users(self): # Create some identity entities BEFORE we switch to multi-backend, so @@ -2453,11 +2602,6 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides, self.config_fixture.config(group='identity_mapping', backward_compatible_ids=False) - def reload_backends(self, domain_id): - # Just reload the driver for this domain - which will pickup - # any updated cfg - self.identity_api.domain_configs.reload_domain_driver(domain_id) - def get_config(self, domain_id): # Get the config for this domain, will return CONF # if no specific config defined for this domain @@ -2619,7 +2763,8 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides, 'domain_id': domain['id'], 'description': uuid.uuid4().hex, 'parent_id': None, - 'enabled': True} + 'enabled': True, + 'is_domain': False} self.resource_api.create_domain(domain['id'], domain) self.resource_api.create_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) @@ -2653,6 +2798,11 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides, self.skipTest("Doesn't apply since LDAP configuration is ignored for " "SQL assignment backend.") + def test_list_role_assignments_filtered_by_role(self): + # Domain roles are supported by the SQL Assignment backend + base = super(BaseLDAPIdentity, self) + base.test_list_role_assignments_filtered_by_role() + class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): """Class to test the use of domain configs stored in the database. @@ -2662,6 +2812,18 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): database. """ + + def assert_backends(self): + _assert_backends(self, + assignment='sql', + identity={ + None: 'sql', + self.domains['domain_default']['id']: 'ldap', + self.domains['domain1']['id']: 'ldap', + self.domains['domain2']['id']: 'ldap', + }, + resource='sql') + def enable_multi_domain(self): # The values below are the same as in the domain_configs_multi_ldap # cdirectory of test config_files. @@ -2670,14 +2832,14 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): 'user': 'cn=Admin', 'password': 'password', 'suffix': 'cn=example,cn=com'}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } domain1_config = { 'ldap': {'url': 'fake://memory1', 'user': 'cn=Admin', 'password': 'password', 'suffix': 'cn=example,cn=com'}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } domain2_config = { 'ldap': {'url': 'fake://memory', @@ -2686,7 +2848,7 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): 'suffix': 'cn=myroot,cn=com', 'group_tree_dn': 'ou=UserGroups,dc=myroot,dc=org', 'user_tree_dn': 'ou=Users,dc=myroot,dc=org'}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } self.domain_config_api.create_config(CONF.identity.default_domain_id, @@ -2725,6 +2887,48 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): CONF.identity.default_domain_id)) self.assertEqual(CONF.ldap.url, default_config.ldap.url) + def test_reloading_domain_config(self): + """Ensure domain drivers are reloaded on a config modification.""" + + domain_cfgs = self.identity_api.domain_configs + + # Create a new config for the default domain, hence overwriting the + # current settings. + new_config = { + 'ldap': {'url': uuid.uuid4().hex}, + 'identity': {'driver': 'ldap'}} + self.domain_config_api.create_config( + CONF.identity.default_domain_id, new_config) + default_config = ( + domain_cfgs.get_domain_conf(CONF.identity.default_domain_id)) + self.assertEqual(new_config['ldap']['url'], default_config.ldap.url) + + # Ensure updating is also honored + updated_config = {'url': uuid.uuid4().hex} + self.domain_config_api.update_config( + CONF.identity.default_domain_id, updated_config, + group='ldap', option='url') + default_config = ( + domain_cfgs.get_domain_conf(CONF.identity.default_domain_id)) + self.assertEqual(updated_config['url'], default_config.ldap.url) + + # ...and finally ensure delete causes the driver to get the standard + # config again. + self.domain_config_api.delete_config(CONF.identity.default_domain_id) + default_config = ( + domain_cfgs.get_domain_conf(CONF.identity.default_domain_id)) + self.assertEqual(CONF.ldap.url, default_config.ldap.url) + + def test_setting_sql_driver_raises_exception(self): + """Ensure setting of domain specific sql driver is prevented.""" + + new_config = {'identity': {'driver': 'sql'}} + self.domain_config_api.create_config( + CONF.identity.default_domain_id, new_config) + self.assertRaises(exception.InvalidDomainConfig, + self.identity_api.domain_configs.get_domain_conf, + CONF.identity.default_domain_id) + class DomainSpecificLDAPandSQLIdentity( BaseLDAPIdentity, tests.SQLDriverOverrides, tests.TestCase, @@ -2740,11 +2944,11 @@ class DomainSpecificLDAPandSQLIdentity( """ def setUp(self): - self.useFixture(database.Database()) + sqldb = self.useFixture(database.Database()) super(DomainSpecificLDAPandSQLIdentity, self).setUp() - self.initial_setup() + self.initial_setup(sqldb) - def initial_setup(self): + def initial_setup(self, sqldb): # We aren't setting up any initial data ahead of switching to # domain-specific operation, so make the switch straight away. self.config_fixture.config( @@ -2755,37 +2959,33 @@ class DomainSpecificLDAPandSQLIdentity( backward_compatible_ids=False) self.load_backends() - - self.engine = sql.get_engine() - self.addCleanup(sql.cleanup) - - sql.ModelBase.metadata.create_all(bind=self.engine) - self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + sqldb.recreate() self.domain_count = 2 self.domain_specific_count = 2 self.setup_initial_domains() self.users = {} - self.clear_database() + self.ldapdb.clear() self.load_fixtures(default_fixtures) self.create_users_across_domains() + _assert_backends( + self, + assignment='sql', + identity={ + None: 'ldap', + 'default': 'ldap', + self.domains['domain1']['id']: 'sql', + }, + resource='sql') + def config_overrides(self): super(DomainSpecificLDAPandSQLIdentity, self).config_overrides() # Make sure resource & assignment are actually SQL drivers, # BaseLDAPIdentity causes this option to use LDAP. - self.config_fixture.config( - group='resource', - driver='keystone.resource.backends.sql.Resource') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') - - def reload_backends(self, domain_id): - # Just reload the driver for this domain - which will pickup - # any updated cfg - self.identity_api.domain_configs.reload_domain_driver(domain_id) + self.config_fixture.config(group='resource', driver='sql') + self.config_fixture.config(group='assignment', driver='sql') def get_config(self, domain_id): # Get the config for this domain, will return CONF @@ -2889,6 +3089,11 @@ class DomainSpecificLDAPandSQLIdentity( self.skipTest("Doesn't apply since LDAP configuration is ignored for " "SQL assignment backend.") + def test_list_role_assignments_filtered_by_role(self): + # Domain roles are supported by the SQL Assignment backend + base = super(BaseLDAPIdentity, self) + base.test_list_role_assignments_filtered_by_role() + class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): """Class to test simplest use of domain-specific SQL driver. @@ -2902,7 +3107,7 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): - A separate SQL backend for domain1 """ - def initial_setup(self): + def initial_setup(self, sqldb): # We aren't setting up any initial data ahead of switching to # domain-specific operation, so make the switch straight away. self.config_fixture.config( @@ -2916,12 +3121,7 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): backward_compatible_ids=True) self.load_backends() - - self.engine = sql.get_engine() - self.addCleanup(sql.cleanup) - - sql.ModelBase.metadata.create_all(bind=self.engine) - self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + sqldb.recreate() self.domain_count = 2 self.domain_specific_count = 1 @@ -2931,17 +3131,16 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): self.load_fixtures(default_fixtures) self.create_users_across_domains() + _assert_backends(self, + assignment='sql', + identity='ldap', + resource='sql') + def config_overrides(self): super(DomainSpecificSQLIdentity, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - self.config_fixture.config( - group='resource', - driver='keystone.resource.backends.sql.Resource') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') + self.config_fixture.config(group='identity', driver='ldap') + self.config_fixture.config(group='resource', driver='sql') + self.config_fixture.config(group='assignment', driver='sql') def get_config(self, domain_id): if domain_id == CONF.identity.default_domain_id: @@ -2949,36 +3148,20 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): else: return self.identity_api.domain_configs.get_domain_conf(domain_id) - def reload_backends(self, domain_id): - if domain_id == CONF.identity.default_domain_id: - self.load_backends() - else: - # Just reload the driver for this domain - which will pickup - # any updated cfg - self.identity_api.domain_configs.reload_domain_driver(domain_id) - def test_default_sql_plus_sql_specific_driver_fails(self): # First confirm that if ldap is default driver, domain1 can be # loaded as sql - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') + self.config_fixture.config(group='identity', driver='ldap') + self.config_fixture.config(group='assignment', driver='sql') self.load_backends() # Make any identity call to initiate the lazy loading of configs self.identity_api.list_users( domain_scope=CONF.identity.default_domain_id) self.assertIsNotNone(self.get_config(self.domains['domain1']['id'])) - # Now re-initialize, but with sql as the default identity driver - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.sql.Identity') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') + # Now re-initialize, but with sql as the identity driver + self.config_fixture.config(group='identity', driver='sql') + self.config_fixture.config(group='assignment', driver='sql') self.load_backends() # Make any identity call to initiate the lazy loading of configs, which # should fail since we would now have two sql drivers. @@ -2987,12 +3170,8 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): domain_scope=CONF.identity.default_domain_id) def test_multiple_sql_specific_drivers_fails(self): - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - self.config_fixture.config( - group='assignment', - driver='keystone.assignment.backends.sql.Assignment') + self.config_fixture.config(group='identity', driver='ldap') + self.config_fixture.config(group='assignment', driver='sql') self.load_backends() # Ensure default, domain1 and domain2 exist self.domain_count = 3 @@ -3019,31 +3198,30 @@ class LdapFilterTests(test_backend.FilterTests, tests.TestCase): def setUp(self): super(LdapFilterTests, self).setUp() - self.useFixture(database.Database()) - self.clear_database() + sqldb = self.useFixture(database.Database()) + self.useFixture(ldapdb.LDAPDatabase()) - common_ldap.register_handler('fake://', fakeldap.FakeLdap) self.load_backends() self.load_fixtures(default_fixtures) - - self.engine = sql.get_engine() - self.addCleanup(sql.cleanup) - sql.ModelBase.metadata.create_all(bind=self.engine) - - self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) - self.addCleanup(common_ldap_core._HANDLERS.clear) + sqldb.recreate() + _assert_backends(self, assignment='ldap', identity='ldap') def config_overrides(self): super(LdapFilterTests, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config(group='identity', driver='ldap') def config_files(self): config_files = super(LdapFilterTests, self).config_files() config_files.append(tests.dirs.tests_conf('backend_ldap.conf')) return config_files - def clear_database(self): - for shelf in fakeldap.FakeShelves: - fakeldap.FakeShelves[shelf].clear() + def test_list_users_in_group_filtered(self): + # The LDAP identity driver currently does not support filtering on the + # listing users for a given group, so will fail this test. + try: + super(LdapFilterTests, self).test_list_users_in_group_filtered() + except matchers.MismatchError: + return + # We shouldn't get here...if we do, it means someone has implemented + # filtering, so we can remove this test override. + self.assertTrue(False) diff --git a/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py b/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py index eee03b8b..66827d7e 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py +++ b/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py @@ -210,9 +210,7 @@ class LdapPoolCommonTestMixin(object): class LdapIdentitySqlAssignment(LdapPoolCommonTestMixin, test_backend_ldap.LdapIdentitySqlAssignment, tests.TestCase): - '''Executes existing base class 150+ tests with pooled LDAP handler to make - sure it works without any error. - ''' + """Executes tests in existing base class with pooled LDAP handler.""" def setUp(self): self.useFixture(mockpatch.PatchObject( ldap_core.PooledLDAPHandler, 'Connector', fakeldap.FakeLdapPool)) diff --git a/keystone-moon/keystone/tests/unit/test_backend_rules.py b/keystone-moon/keystone/tests/unit/test_backend_rules.py index c9c4f151..bc0dc13d 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_rules.py +++ b/keystone-moon/keystone/tests/unit/test_backend_rules.py @@ -25,9 +25,7 @@ class RulesPolicy(tests.TestCase, test_backend.PolicyTests): def config_overrides(self): super(RulesPolicy, self).config_overrides() - self.config_fixture.config( - group='policy', - driver='keystone.policy.backends.rules.Policy') + self.config_fixture.config(group='policy', driver='rules') def test_create(self): self.assertRaises(exception.NotImplemented, diff --git a/keystone-moon/keystone/tests/unit/test_backend_sql.py b/keystone-moon/keystone/tests/unit/test_backend_sql.py index a7c63bf6..bf50ac21 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_sql.py +++ b/keystone-moon/keystone/tests/unit/test_backend_sql.py @@ -20,6 +20,7 @@ import mock from oslo_config import cfg from oslo_db import exception as db_exception from oslo_db import options +from six.moves import range import sqlalchemy from sqlalchemy import exc from testtools import matchers @@ -28,7 +29,6 @@ from keystone.common import driver_hints from keystone.common import sql from keystone import exception from keystone.identity.backends import sql as identity_sql -from keystone.openstack.common import versionutils from keystone.tests import unit as tests from keystone.tests.unit import default_fixtures from keystone.tests.unit.ksfixtures import database @@ -67,18 +67,67 @@ class SqlModels(SqlTests): s = sqlalchemy.select([table]) return s - def assertExpectedSchema(self, table, cols): + def assertExpectedSchema(self, table, expected_schema): + """Assert that a table's schema is what we expect. + + :param string table: the name of the table to inspect + :param tuple expected_schema: a tuple of tuples containing the + expected schema + :raises AssertionError: when the database schema doesn't match the + expected schema + + The expected_schema format is simply:: + + ( + ('column name', sql type, qualifying detail), + ... + ) + + The qualifying detail varies based on the type of the column:: + + - sql.Boolean columns must indicate the column's default value or + None if there is no default + - Columns with a length, like sql.String, must indicate the + column's length + - All other column types should use None + + Example:: + + cols = (('id', sql.String, 64), + ('enabled', sql.Boolean, True), + ('extra', sql.JsonBlob, None)) + self.assertExpectedSchema('table_name', cols) + + """ table = self.select_table(table) - for col, type_, length in cols: - self.assertIsInstance(table.c[col].type, type_) - if length: - self.assertEqual(length, table.c[col].type.length) + + actual_schema = [] + for column in table.c: + if isinstance(column.type, sql.Boolean): + default = None + if column._proxies[0].default: + default = column._proxies[0].default.arg + actual_schema.append((column.name, type(column.type), default)) + elif (hasattr(column.type, 'length') and + not isinstance(column.type, sql.Enum)): + # NOTE(dstanek): Even though sql.Enum columns have a length + # set we don't want to catch them here. Maybe in the future + # we'll check to see that they contain a list of the correct + # possible values. + actual_schema.append((column.name, + type(column.type), + column.type.length)) + else: + actual_schema.append((column.name, type(column.type), None)) + + self.assertItemsEqual(expected_schema, actual_schema) def test_user_model(self): cols = (('id', sql.String, 64), ('name', sql.String, 255), ('password', sql.String, 128), ('domain_id', sql.String, 64), + ('default_project_id', sql.String, 64), ('enabled', sql.Boolean, None), ('extra', sql.JsonBlob, None)) self.assertExpectedSchema('user', cols) @@ -94,7 +143,8 @@ class SqlModels(SqlTests): def test_domain_model(self): cols = (('id', sql.String, 64), ('name', sql.String, 64), - ('enabled', sql.Boolean, None)) + ('enabled', sql.Boolean, True), + ('extra', sql.JsonBlob, None)) self.assertExpectedSchema('domain', cols) def test_project_model(self): @@ -104,7 +154,8 @@ class SqlModels(SqlTests): ('domain_id', sql.String, 64), ('enabled', sql.Boolean, None), ('extra', sql.JsonBlob, None), - ('parent_id', sql.String, 64)) + ('parent_id', sql.String, 64), + ('is_domain', sql.Boolean, False)) self.assertExpectedSchema('project', cols) def test_role_assignment_model(self): @@ -692,6 +743,9 @@ class SqlTokenCacheInvalidation(SqlTests, test_backend.TokenCacheInvalidation): class SqlFilterTests(SqlTests, test_backend.FilterTests): + def _get_user_name_field_size(self): + return identity_sql.User.name.type.length + def clean_up_entities(self): """Clean up entity test data from Filter Test Cases.""" @@ -761,21 +815,6 @@ class SqlFilterTests(SqlTests, test_backend.FilterTests): groups = self.identity_api.list_groups() self.assertTrue(len(groups) > 0) - def test_groups_for_user_filtered(self): - # The SQL identity driver currently does not support filtering on the - # listing groups for a given user, so will fail this test. This is - # raised as bug #1412447. - try: - super(SqlFilterTests, self).test_groups_for_user_filtered() - except matchers.MismatchError: - return - # We shouldn't get here...if we do, it means someone has fixed the - # above defect, so we can remove this test override. As an aside, it - # would be nice to have used self.assertRaises() around the call above - # to achieve the logic here...but that does not seem to work when - # wrapping another assert (it won't seem to catch the error). - self.assertTrue(False) - class SqlLimitTests(SqlTests, test_backend.LimitTests): def setUp(self): @@ -881,68 +920,3 @@ class SqlCredential(SqlTests): credentials = self.credential_api.list_credentials_for_user( self.user_foo['id']) self._validateCredentialList(credentials, self.user_credentials) - - -class DeprecatedDecorators(SqlTests): - - def test_assignment_to_role_api(self): - """Test that calling one of the methods does call LOG.deprecated. - - This method is really generic to the type of backend, but we need - one to execute the test, so the SQL backend is as good as any. - - """ - - # Rather than try and check that a log message is issued, we - # enable fatal_deprecations so that we can check for the - # raising of the exception. - - # First try to create a role without enabling fatal deprecations, - # which should work due to the cross manager deprecated calls. - role_ref = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} - self.assignment_api.create_role(role_ref['id'], role_ref) - self.role_api.get_role(role_ref['id']) - - # Now enable fatal exceptions - creating a role by calling the - # old manager should now fail. - self.config_fixture.config(fatal_deprecations=True) - role_ref = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} - self.assertRaises(versionutils.DeprecatedConfig, - self.assignment_api.create_role, - role_ref['id'], role_ref) - - def test_assignment_to_resource_api(self): - """Test that calling one of the methods does call LOG.deprecated. - - This method is really generic to the type of backend, but we need - one to execute the test, so the SQL backend is as good as any. - - """ - - # Rather than try and check that a log message is issued, we - # enable fatal_deprecations so that we can check for the - # raising of the exception. - - # First try to create a project without enabling fatal deprecations, - # which should work due to the cross manager deprecated calls. - project_ref = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID} - self.resource_api.create_project(project_ref['id'], project_ref) - self.resource_api.get_project(project_ref['id']) - - # Now enable fatal exceptions - creating a project by calling the - # old manager should now fail. - self.config_fixture.config(fatal_deprecations=True) - project_ref = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID} - self.assertRaises(versionutils.DeprecatedConfig, - self.assignment_api.create_project, - project_ref['id'], project_ref) diff --git a/keystone-moon/keystone/tests/unit/test_backend_templated.py b/keystone-moon/keystone/tests/unit/test_backend_templated.py index a1c15fb1..82a8bed8 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_templated.py +++ b/keystone-moon/keystone/tests/unit/test_backend_templated.py @@ -12,18 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. -import os import uuid +import mock +from six.moves import zip + +from keystone import catalog from keystone.tests import unit as tests from keystone.tests.unit import default_fixtures from keystone.tests.unit.ksfixtures import database from keystone.tests.unit import test_backend -DEFAULT_CATALOG_TEMPLATES = os.path.abspath(os.path.join( - os.path.dirname(__file__), - 'default_catalog.templates')) +BROKEN_WRITE_FUNCTIONALITY_MSG = ("Templated backend doesn't correctly " + "implement write operations") class TestTemplatedCatalog(tests.TestCase, test_backend.CatalogTests): @@ -55,8 +57,10 @@ class TestTemplatedCatalog(tests.TestCase, test_backend.CatalogTests): def config_overrides(self): super(TestTemplatedCatalog, self).config_overrides() - self.config_fixture.config(group='catalog', - template_file=DEFAULT_CATALOG_TEMPLATES) + self.config_fixture.config( + group='catalog', + driver='templated', + template_file=tests.dirs.tests('default_catalog.templates')) def test_get_catalog(self): catalog_ref = self.catalog_api.get_catalog('foo', 'bar') @@ -120,8 +124,116 @@ class TestTemplatedCatalog(tests.TestCase, test_backend.CatalogTests): 'id': '1'}] self.assert_catalogs_equal(exp_catalog, catalog_ref) + def test_get_catalog_ignores_endpoints_with_invalid_urls(self): + user_id = uuid.uuid4().hex + # If the URL has no 'tenant_id' to substitute, we will skip the + # endpoint which contains this kind of URL. + catalog_ref = self.catalog_api.get_v3_catalog(user_id, tenant_id=None) + exp_catalog = [ + {'endpoints': [], + 'type': 'compute', + 'name': "'Compute Service'", + 'id': '2'}, + {'endpoints': [ + {'interface': 'admin', + 'region': 'RegionOne', + 'url': 'http://localhost:35357/v2.0'}, + {'interface': 'public', + 'region': 'RegionOne', + 'url': 'http://localhost:5000/v2.0'}, + {'interface': 'internal', + 'region': 'RegionOne', + 'url': 'http://localhost:35357/v2.0'}], + 'type': 'identity', + 'name': "'Identity Service'", + 'id': '1'}] + self.assert_catalogs_equal(exp_catalog, catalog_ref) + def test_list_regions_filtered_by_parent_region_id(self): self.skipTest('Templated backend does not support hints') def test_service_filtering(self): self.skipTest("Templated backend doesn't support filtering") + + # NOTE(dstanek): the following methods have been overridden + # from test_backend.CatalogTests + + def test_region_crud(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + @tests.skip_if_cache_disabled('catalog') + def test_cache_layer_region_crud(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + @tests.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_region(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_create_region_with_duplicate_id(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_delete_region_404(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_create_region_invalid_parent_region_404(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_avoid_creating_circular_references_in_regions_update(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + @mock.patch.object(catalog.Driver, + "_ensure_no_circle_in_hierarchical_regions") + def test_circular_regions_can_be_deleted(self, mock_ensure_on_circle): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_service_crud(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + @tests.skip_if_cache_disabled('catalog') + def test_cache_layer_service_crud(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + @tests.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_service(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_delete_service_with_endpoint(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_cache_layer_delete_service_with_endpoint(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_delete_service_404(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_update_endpoint_nonexistent_service(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_create_endpoint_nonexistent_region(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_update_endpoint_nonexistent_region(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_get_endpoint_404(self): + self.skipTest("Templated backend doesn't use IDs for endpoints.") + + def test_delete_endpoint_404(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_create_endpoint(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_update_endpoint(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) + + def test_list_endpoints(self): + # NOTE(dstanek): a future commit will fix this functionality and + # this test + expected_ids = set() + endpoints = self.catalog_api.list_endpoints() + self.assertEqual(expected_ids, set(e['id'] for e in endpoints)) + + @tests.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_endpoint(self): + self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) diff --git a/keystone-moon/keystone/tests/unit/test_cache.py b/keystone-moon/keystone/tests/unit/test_cache.py index 5a778a07..c60df877 100644 --- a/keystone-moon/keystone/tests/unit/test_cache.py +++ b/keystone-moon/keystone/tests/unit/test_cache.py @@ -47,10 +47,12 @@ def _copy_value(value): # backend unless you are running tests or expecting odd/strange results. class CacheIsolatingProxy(proxy.ProxyBackend): """Proxy that forces a memory copy of stored values. - The default in-memory cache-region does not perform a copy on values it - is meant to cache. Therefore if the value is modified after set or after - get, the cached value also is modified. This proxy does a copy as the last + + The default in-memory cache-region does not perform a copy on values it is + meant to cache. Therefore if the value is modified after set or after get, + the cached value also is modified. This proxy does a copy as the last thing before storing data. + """ def get(self, key): return _copy_value(self.proxied.get(key)) diff --git a/keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py b/keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py index a56bf754..369570d6 100644 --- a/keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py +++ b/keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py @@ -20,6 +20,7 @@ import uuid from dogpile.cache import api from dogpile.cache import region as dp_region import six +from six.moves import range from keystone.common.cache.backends import mongo from keystone import exception @@ -139,13 +140,13 @@ class MockCollection(object): if self._apply_filter(document, spec)) def _apply_filter(self, document, query): - for key, search in six.iteritems(query): + for key, search in query.items(): doc_val = document.get(key) if isinstance(search, dict): op_dict = {'$in': lambda dv, sv: dv in sv} is_match = all( op_str in op_dict and op_dict[op_str](doc_val, search_val) - for op_str, search_val in six.iteritems(search) + for op_str, search_val in search.items() ) else: is_match = doc_val == search @@ -160,7 +161,7 @@ class MockCollection(object): return new if isinstance(obj, dict): new = container() - for key, value in obj.items(): + for key, value in list(obj.items()): new[key] = self._copy_doc(value, container) return new else: @@ -198,7 +199,7 @@ class MockCollection(object): existing_doc = self._documents[self._insert(document)] def _internalize_dict(self, d): - return {k: copy.deepcopy(v) for k, v in six.iteritems(d)} + return {k: copy.deepcopy(v) for k, v in d.items()} def remove(self, spec_or_id=None, search_filter=None): """Remove objects matching spec_or_id from the collection.""" diff --git a/keystone-moon/keystone/tests/unit/test_catalog.py b/keystone-moon/keystone/tests/unit/test_catalog.py index 9dda5d83..4e7f4037 100644 --- a/keystone-moon/keystone/tests/unit/test_catalog.py +++ b/keystone-moon/keystone/tests/unit/test_catalog.py @@ -14,8 +14,6 @@ import uuid -import six - from keystone import catalog from keystone.tests import unit as tests from keystone.tests.unit.ksfixtures import database @@ -47,9 +45,7 @@ class V2CatalogTestCase(rest.RestfulTestCase): def config_overrides(self): super(V2CatalogTestCase, self).config_overrides() - self.config_fixture.config( - group='catalog', - driver='keystone.catalog.backends.sql.Catalog') + self.config_fixture.config(group='catalog', driver='sql') def new_ref(self): """Populates a ref with attributes common to all API entities.""" @@ -95,7 +91,7 @@ class V2CatalogTestCase(rest.RestfulTestCase): req_body, response = self._endpoint_create() self.assertIn('endpoint', response.result) self.assertIn('id', response.result['endpoint']) - for field, value in six.iteritems(req_body['endpoint']): + for field, value in req_body['endpoint'].items(): self.assertEqual(response.result['endpoint'][field], value) def test_endpoint_create_with_null_adminurl(self): @@ -130,6 +126,92 @@ class V2CatalogTestCase(rest.RestfulTestCase): def test_endpoint_create_with_empty_service_id(self): self._endpoint_create(expected_status=400, service_id='') + def test_endpoint_create_with_valid_url(self): + """Create endpoint with valid URL should be tested, too.""" + # list one valid url is enough, no need to list too much + valid_url = 'http://127.0.0.1:8774/v1.1/$(tenant_id)s' + + # baseline tests that all valid URLs works + self._endpoint_create(expected_status=200, + publicurl=valid_url, + internalurl=valid_url, + adminurl=valid_url) + + def test_endpoint_create_with_invalid_url(self): + """Test the invalid cases: substitutions is not exactly right.""" + invalid_urls = [ + # using a substitution that is not whitelisted - KeyError + 'http://127.0.0.1:8774/v1.1/$(nonexistent)s', + + # invalid formatting - ValueError + 'http://127.0.0.1:8774/v1.1/$(tenant_id)', + 'http://127.0.0.1:8774/v1.1/$(tenant_id)t', + 'http://127.0.0.1:8774/v1.1/$(tenant_id', + + # invalid type specifier - TypeError + # admin_url is a string not an int + 'http://127.0.0.1:8774/v1.1/$(admin_url)d', + ] + + # list one valid url is enough, no need to list too much + valid_url = 'http://127.0.0.1:8774/v1.1/$(tenant_id)s' + + # Case one: publicurl, internalurl and adminurl are + # all invalid + for invalid_url in invalid_urls: + self._endpoint_create(expected_status=400, + publicurl=invalid_url, + internalurl=invalid_url, + adminurl=invalid_url) + + # Case two: publicurl, internalurl are invalid + # and adminurl is valid + for invalid_url in invalid_urls: + self._endpoint_create(expected_status=400, + publicurl=invalid_url, + internalurl=invalid_url, + adminurl=valid_url) + + # Case three: publicurl, adminurl are invalid + # and internalurl is valid + for invalid_url in invalid_urls: + self._endpoint_create(expected_status=400, + publicurl=invalid_url, + internalurl=valid_url, + adminurl=invalid_url) + + # Case four: internalurl, adminurl are invalid + # and publicurl is valid + for invalid_url in invalid_urls: + self._endpoint_create(expected_status=400, + publicurl=valid_url, + internalurl=invalid_url, + adminurl=invalid_url) + + # Case five: publicurl is invalid, internalurl + # and adminurl are valid + for invalid_url in invalid_urls: + self._endpoint_create(expected_status=400, + publicurl=invalid_url, + internalurl=valid_url, + adminurl=valid_url) + + # Case six: internalurl is invalid, publicurl + # and adminurl are valid + for invalid_url in invalid_urls: + self._endpoint_create(expected_status=400, + publicurl=valid_url, + internalurl=invalid_url, + adminurl=valid_url) + + # Case seven: adminurl is invalid, publicurl + # and internalurl are valid + for invalid_url in invalid_urls: + self._endpoint_create(expected_status=400, + publicurl=valid_url, + internalurl=valid_url, + adminurl=invalid_url) + class TestV2CatalogAPISQL(tests.TestCase): @@ -147,9 +229,7 @@ class TestV2CatalogAPISQL(tests.TestCase): def config_overrides(self): super(TestV2CatalogAPISQL, self).config_overrides() - self.config_fixture.config( - group='catalog', - driver='keystone.catalog.backends.sql.Catalog') + self.config_fixture.config(group='catalog', driver='sql') def new_endpoint_ref(self, service_id): return { diff --git a/keystone-moon/keystone/tests/unit/test_cert_setup.py b/keystone-moon/keystone/tests/unit/test_cert_setup.py index d1e9ccfd..3d300810 100644 --- a/keystone-moon/keystone/tests/unit/test_cert_setup.py +++ b/keystone-moon/keystone/tests/unit/test_cert_setup.py @@ -68,9 +68,7 @@ class CertSetupTestCase(rest.RestfulTestCase): ca_certs=ca_certs, certfile=os.path.join(CERTDIR, 'keystone.pem'), keyfile=os.path.join(KEYDIR, 'keystonekey.pem')) - self.config_fixture.config( - group='token', - provider='keystone.token.providers.pkiz.Provider') + self.config_fixture.config(group='token', provider='pkiz') def test_can_handle_missing_certs(self): controller = token.controllers.Auth() diff --git a/keystone-moon/keystone/tests/unit/test_cli.py b/keystone-moon/keystone/tests/unit/test_cli.py index 20aa03e6..3f37612e 100644 --- a/keystone-moon/keystone/tests/unit/test_cli.py +++ b/keystone-moon/keystone/tests/unit/test_cli.py @@ -17,14 +17,16 @@ import uuid import mock from oslo_config import cfg +from six.moves import range -from keystone import cli +from keystone.cmd import cli from keystone.common import dependency from keystone.i18n import _ from keystone import resource from keystone.tests import unit as tests from keystone.tests.unit.ksfixtures import database + CONF = cfg.CONF @@ -103,14 +105,14 @@ class CliDomainConfigAllTestCase(tests.SQLDriverOverrides, tests.TestCase): 'user': 'cn=Admin', 'password': 'password', 'suffix': 'cn=example,cn=com'}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } domain1_config = { 'ldap': {'url': 'fake://memory1', 'user': 'cn=Admin', 'password': 'password', 'suffix': 'cn=example,cn=com'}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } domain2_config = { 'ldap': {'url': 'fake://memory', @@ -119,7 +121,7 @@ class CliDomainConfigAllTestCase(tests.SQLDriverOverrides, tests.TestCase): 'suffix': 'cn=myroot,cn=com', 'group_tree_dn': 'ou=UserGroups,dc=myroot,dc=org', 'user_tree_dn': 'ou=Users,dc=myroot,dc=org'}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } # Clear backend dependencies, since cli loads these manually @@ -151,7 +153,7 @@ class CliDomainConfigSingleDomainTestCase(CliDomainConfigAllTestCase): 'user': 'cn=Admin', 'password': 'password', 'suffix': 'cn=example,cn=com'}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } # Clear backend dependencies, since cli loads these manually @@ -172,7 +174,7 @@ class CliDomainConfigSingleDomainTestCase(CliDomainConfigAllTestCase): # Create a config for the default domain default_config = { 'ldap': {'url': uuid.uuid4().hex}, - 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + 'identity': {'driver': 'ldap'} } self.domain_config_api.create_config( CONF.identity.default_domain_id, default_config) diff --git a/keystone-moon/keystone/tests/unit/test_config.py b/keystone-moon/keystone/tests/unit/test_config.py index 15cfac81..431f9965 100644 --- a/keystone-moon/keystone/tests/unit/test_config.py +++ b/keystone-moon/keystone/tests/unit/test_config.py @@ -46,10 +46,8 @@ class ConfigTestCase(tests.TestCase): config.find_paste_config()) def test_config_default(self): - self.assertEqual('keystone.auth.plugins.password.Password', - CONF.auth.password) - self.assertEqual('keystone.auth.plugins.token.Token', - CONF.auth.token) + self.assertIs(None, CONF.auth.password) + self.assertIs(None, CONF.auth.token) class DeprecatedTestCase(tests.TestCase): diff --git a/keystone-moon/keystone/tests/unit/test_contrib_ec2.py b/keystone-moon/keystone/tests/unit/test_contrib_ec2.py new file mode 100644 index 00000000..c6717dc5 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_contrib_ec2.py @@ -0,0 +1,208 @@ +# Copyright 2015 Intel Corporation +# +# 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 uuid + +from keystoneclient.contrib.ec2 import utils as ec2_utils + +from keystone.contrib.ec2 import controllers +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database + + +class TestCredentialEc2(tests.TestCase): + # TODO(davechen): more testcases for ec2 credential are expected here and + # the file name would be renamed to "test_credential" to correspond with + # "test_v3_credential.py". + def setUp(self): + super(TestCredentialEc2, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + self.load_fixtures(default_fixtures) + self.user_id = self.user_foo['id'] + self.project_id = self.tenant_bar['id'] + self.blob = {'access': uuid.uuid4().hex, + 'secret': uuid.uuid4().hex} + self.controller = controllers.Ec2Controller() + self.creds_ref = {'user_id': self.user_id, + 'tenant_id': self.project_id, + 'access': self.blob['access'], + 'secret': self.blob['secret'], + 'trust_id': None} + + def test_signature_validate_no_host_port(self): + """Test signature validation with the access/secret provided.""" + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + self.assertTrue(self.controller.check_signature(self.creds_ref, + sig_ref)) + + def test_signature_validate_with_host_port(self): + """Test signature validation when host is bound with port. + + Host is bound with a port, generally, the port here is not the + standard port for the protocol, like '80' for HTTP and port 443 + for HTTPS, the port is not omitted by the client library. + """ + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'foo:8181', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo:8181', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + self.assertTrue(self.controller.check_signature(self.creds_ref, + sig_ref)) + + def test_signature_validate_with_missed_host_port(self): + """Test signature validation when host is bound with well-known port. + + Host is bound with a port, but the port is well-know port like '80' + for HTTP and port 443 for HTTPS, sometimes, client library omit + the port but then make the request with the port. + see (How to create the string to sign): 'http://docs.aws.amazon.com/ + general/latest/gr/signature-version-2.html'. + + Since "credentials['host']" is not set by client library but is + taken from "req.host", so caused the differences. + """ + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + # Omit the port to generate the signature. + cnt_req = {'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(cnt_req) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo:8080', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + # Check the signature again after omitting the port. + self.assertTrue(self.controller.check_signature(self.creds_ref, + sig_ref)) + + def test_signature_validate_no_signature(self): + """Signature is not presented in signature reference data.""" + access = self.blob['access'] + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + + sig_ref = {'access': access, + 'signature': None, + 'host': 'foo:8080', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + creds_ref = {'user_id': self.user_id, + 'tenant_id': self.project_id, + 'access': self.blob['access'], + 'secret': self.blob['secret'], + 'trust_id': None + } + + # Now validate the signature based on the dummy request + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + creds_ref, sig_ref) + + def test_signature_validate_invalid_signature(self): + """Signature is not signed on the correct data.""" + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'bar', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo:8080', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + creds_ref = {'user_id': self.user_id, + 'tenant_id': self.project_id, + 'access': self.blob['access'], + 'secret': self.blob['secret'], + 'trust_id': None + } + + # Now validate the signature based on the dummy request + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + creds_ref, sig_ref) + + def test_check_non_admin_user(self): + """Checking if user is admin causes uncaught error. + + When checking if a user is an admin, keystone.exception.Unauthorized + is raised but not caught if the user is not an admin. + """ + # make a non-admin user + context = {'is_admin': False, 'token_id': uuid.uuid4().hex} + + # check if user is admin + # no exceptions should be raised + self.controller._is_admin(context) diff --git a/keystone-moon/keystone/tests/unit/test_exception.py b/keystone-moon/keystone/tests/unit/test_exception.py index f91fa2a7..bf541dfd 100644 --- a/keystone-moon/keystone/tests/unit/test_exception.py +++ b/keystone-moon/keystone/tests/unit/test_exception.py @@ -87,7 +87,10 @@ class ExceptionTestCase(tests.BaseTestCase): e = exception.ValidationError(attribute='xx', target='Long \xe2\x80\x93 Dash') - self.assertIn(u'\u2013', six.text_type(e)) + if six.PY2: + self.assertIn(u'\u2013', six.text_type(e)) + else: + self.assertIn('Long \xe2\x80\x93 Dash', six.text_type(e)) def test_invalid_unicode_string(self): # NOTE(jamielennox): This is a complete failure case so what is @@ -95,7 +98,12 @@ class ExceptionTestCase(tests.BaseTestCase): # as there is an error with a message e = exception.ValidationError(attribute='xx', target='\xe7a va') - self.assertIn('%(attribute)', six.text_type(e)) + + if six.PY2: + self.assertIn('%(attribute)', six.text_type(e)) + else: + # There's no UnicodeDecodeError on python 3. + self.assertIn('\xe7a va', six.text_type(e)) class UnexpectedExceptionTestCase(ExceptionTestCase): diff --git a/keystone-moon/keystone/tests/unit/test_hacking_checks.py b/keystone-moon/keystone/tests/unit/test_hacking_checks.py index b9b047b3..962f5f8a 100644 --- a/keystone-moon/keystone/tests/unit/test_hacking_checks.py +++ b/keystone-moon/keystone/tests/unit/test_hacking_checks.py @@ -14,13 +14,13 @@ import textwrap import mock import pep8 -import testtools -from keystone.hacking import checks +from keystone.tests.hacking import checks +from keystone.tests import unit from keystone.tests.unit.ksfixtures import hacking as hacking_fixtures -class BaseStyleCheck(testtools.TestCase): +class BaseStyleCheck(unit.BaseTestCase): def setUp(self): super(BaseStyleCheck, self).setUp() @@ -122,16 +122,6 @@ class TestCheckForNonDebugLoggingIssues(BaseStyleCheck): self.assertEqual(expected_errors or [], actual_errors) -class TestCheckOsloNamespaceImports(BaseStyleCheck): - def get_checker(self): - return checks.check_oslo_namespace_imports - - def test(self): - code = self.code_ex.oslo_namespace_imports['code'] - errors = self.code_ex.oslo_namespace_imports['expected_errors'] - self.assert_has_errors(code, expected_errors=errors) - - class TestDictConstructorWithSequenceCopy(BaseStyleCheck): def get_checker(self): diff --git a/keystone-moon/keystone/tests/unit/test_kvs.py b/keystone-moon/keystone/tests/unit/test_kvs.py index 4d80ea33..77e05e6d 100644 --- a/keystone-moon/keystone/tests/unit/test_kvs.py +++ b/keystone-moon/keystone/tests/unit/test_kvs.py @@ -28,6 +28,7 @@ from keystone.common.kvs import core from keystone import exception from keystone.tests import unit as tests + NO_VALUE = api.NO_VALUE @@ -487,6 +488,8 @@ class KVSTest(tests.TestCase): memcached_expire_time=memcache_expire_time, some_other_arg=uuid.uuid4().hex, no_expiry_keys=[self.key_bar]) + kvs_driver = kvs._region.backend.driver + # Ensure the set_arguments are correct self.assertDictEqual( kvs._region.backend._get_set_arguments_driver_attr(), @@ -498,8 +501,8 @@ class KVSTest(tests.TestCase): self.assertDictEqual( kvs._region.backend.driver.client.set_arguments_passed, expected_set_args) - self.assertEqual(expected_foo_keys, - kvs._region.backend.driver.client.keys_values.keys()) + observed_foo_keys = list(kvs_driver.client.keys_values.keys()) + self.assertEqual(expected_foo_keys, observed_foo_keys) self.assertEqual( self.value_foo, kvs._region.backend.driver.client.keys_values[self.key_foo][0]) @@ -510,8 +513,8 @@ class KVSTest(tests.TestCase): self.assertDictEqual( kvs._region.backend.driver.client.set_arguments_passed, expected_no_expiry_args) - self.assertEqual(expected_bar_keys, - kvs._region.backend.driver.client.keys_values.keys()) + observed_bar_keys = list(kvs_driver.client.keys_values.keys()) + self.assertEqual(expected_bar_keys, observed_bar_keys) self.assertEqual( self.value_bar, kvs._region.backend.driver.client.keys_values[self.key_bar][0]) @@ -522,8 +525,8 @@ class KVSTest(tests.TestCase): self.assertDictEqual( kvs._region.backend.driver.client.set_arguments_passed, expected_set_args) - self.assertEqual(expected_foo_keys, - kvs._region.backend.driver.client.keys_values.keys()) + observed_foo_keys = list(kvs_driver.client.keys_values.keys()) + self.assertEqual(expected_foo_keys, observed_foo_keys) self.assertEqual( self.value_foo, kvs._region.backend.driver.client.keys_values[self.key_foo][0]) @@ -534,8 +537,8 @@ class KVSTest(tests.TestCase): self.assertDictEqual( kvs._region.backend.driver.client.set_arguments_passed, expected_no_expiry_args) - self.assertEqual(expected_bar_keys, - kvs._region.backend.driver.client.keys_values.keys()) + observed_bar_keys = list(kvs_driver.client.keys_values.keys()) + self.assertEqual(expected_bar_keys, observed_bar_keys) self.assertEqual( self.value_bar, kvs._region.backend.driver.client.keys_values[self.key_bar][0]) diff --git a/keystone-moon/keystone/tests/unit/test_ldap_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_livetest.py index 5b449362..b9f56e8d 100644 --- a/keystone-moon/keystone/tests/unit/test_ldap_livetest.py +++ b/keystone-moon/keystone/tests/unit/test_ldap_livetest.py @@ -15,9 +15,9 @@ import subprocess import uuid -import ldap import ldap.modlist from oslo_config import cfg +from six.moves import range from keystone import exception from keystone.identity.backends import ldap as identity_ldap @@ -81,12 +81,6 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): config_files.append(tests.dirs.tests_conf('backend_liveldap.conf')) return config_files - def config_overrides(self): - super(LiveLDAPIdentity, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - def test_build_tree(self): """Regression test for building the tree names """ @@ -95,9 +89,6 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): self.assertTrue(user_api) self.assertEqual(user_api.tree_dn, CONF.ldap.user_tree_dn) - def tearDown(self): - tests.TestCase.tearDown(self) - def test_ldap_dereferencing(self): alt_users_ldif = {'objectclass': ['top', 'organizationalUnit'], 'ou': 'alt_users'} @@ -176,8 +167,10 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): negative_user['id']) self.assertEqual(0, len(group_refs)) - self.config_fixture.config(group='ldap', group_filter='(dn=xx)') - self.reload_backends(CONF.identity.default_domain_id) + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) + driver.group.ldap_filter = '(dn=xx)' + group_refs = self.identity_api.list_groups_for_user( positive_user['id']) self.assertEqual(0, len(group_refs)) @@ -185,9 +178,8 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): negative_user['id']) self.assertEqual(0, len(group_refs)) - self.config_fixture.config(group='ldap', - group_filter='(objectclass=*)') - self.reload_backends(CONF.identity.default_domain_id) + driver.group.ldap_filter = '(objectclass=*)' + group_refs = self.identity_api.list_groups_for_user( positive_user['id']) self.assertEqual(GROUP_COUNT, len(group_refs)) diff --git a/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py index 02fa8145..a8776e5b 100644 --- a/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py +++ b/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py @@ -30,10 +30,10 @@ CONF = cfg.CONF class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin, test_ldap_livetest.LiveLDAPIdentity): - """Executes existing LDAP live test with pooled LDAP handler to make - sure it works without any error. + """Executes existing LDAP live test with pooled LDAP handler. Also executes common pool specific tests via Mixin class. + """ def setUp(self): @@ -48,12 +48,6 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin, tests_conf('backend_pool_liveldap.conf')) return config_files - def config_overrides(self): - super(LiveLDAPPoolIdentity, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - def test_assert_connector_used_not_fake_ldap_pool(self): handler = ldap_core._get_connection(CONF.ldap.url, use_pool=True) self.assertNotEqual(type(handler.Connector), diff --git a/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py index d79c2bad..e77bbc98 100644 --- a/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py +++ b/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import ldap import ldap.modlist from oslo_config import cfg @@ -44,12 +43,6 @@ class LiveTLSLDAPIdentity(test_ldap_livetest.LiveLDAPIdentity): config_files.append(tests.dirs.tests_conf('backend_tls_liveldap.conf')) return config_files - def config_overrides(self): - super(LiveTLSLDAPIdentity, self).config_overrides() - self.config_fixture.config( - group='identity', - driver='keystone.identity.backends.ldap.Identity') - def test_tls_certfile_demand_option(self): self.config_fixture.config(group='ldap', use_tls=True, diff --git a/keystone-moon/keystone/tests/unit/test_policy.py b/keystone-moon/keystone/tests/unit/test_policy.py index 2c0c3995..30df0b2b 100644 --- a/keystone-moon/keystone/tests/unit/test_policy.py +++ b/keystone-moon/keystone/tests/unit/test_policy.py @@ -14,6 +14,7 @@ # under the License. import json +import os import mock from oslo_policy import policy as common_policy @@ -223,6 +224,48 @@ class PolicyJsonTestCase(tests.TestCase): cloud_policy_keys = self._load_entries( tests.dirs.etc('policy.v3cloudsample.json')) - diffs = set(policy_keys).difference(set(cloud_policy_keys)) + policy_extra_keys = ['admin_or_token_subject', + 'service_admin_or_token_subject', + 'token_subject', ] + expected_policy_keys = list(cloud_policy_keys) + policy_extra_keys + diffs = set(policy_keys).difference(set(expected_policy_keys)) self.assertThat(diffs, matchers.Equals(set())) + + def test_all_targets_documented(self): + # All the targets in the sample policy file must be documented in + # doc/source/policy_mapping.rst. + + policy_keys = self._load_entries(tests.dirs.etc('policy.json')) + + # These keys are in the policy.json but aren't targets. + policy_rule_keys = [ + 'admin_or_owner', 'admin_or_token_subject', 'admin_required', + 'default', 'owner', 'service_admin_or_token_subject', + 'service_or_admin', 'service_role', 'token_subject', ] + + def read_doc_targets(): + # Parse the doc/source/policy_mapping.rst file and return the + # targets. + + doc_path = os.path.join( + tests.ROOTDIR, 'doc', 'source', 'policy_mapping.rst') + with open(doc_path) as doc_file: + for line in doc_file: + if line.startswith('Target'): + break + for line in doc_file: + # Skip === line + if line.startswith('==='): + break + for line in doc_file: + line = line.rstrip() + if not line or line.startswith(' '): + continue + if line.startswith('=='): + break + target, dummy, dummy = line.partition(' ') + yield six.text_type(target) + + doc_targets = list(read_doc_targets()) + self.assertItemsEqual(policy_keys, doc_targets + policy_rule_keys) diff --git a/keystone-moon/keystone/tests/unit/test_revoke.py b/keystone-moon/keystone/tests/unit/test_revoke.py index 727eff78..5394688c 100644 --- a/keystone-moon/keystone/tests/unit/test_revoke.py +++ b/keystone-moon/keystone/tests/unit/test_revoke.py @@ -16,8 +16,10 @@ import uuid import mock from oslo_utils import timeutils +from six.moves import range from testtools import matchers +from keystone.common import utils from keystone.contrib.revoke import model from keystone import exception from keystone.tests import unit as tests @@ -112,6 +114,7 @@ def _matches(event, token_values): class RevokeTests(object): + def test_list(self): self.revoke_api.revoke_by_user(user_id=1) self.assertEqual(1, len(self.revoke_api.list_events())) @@ -140,8 +143,8 @@ class RevokeTests(object): def test_expired_events_removed_validate_token_success(self, mock_utcnow): def _sample_token_values(): token = _sample_blank_token() - token['expires_at'] = timeutils.isotime(_future_time(), - subsecond=True) + token['expires_at'] = utils.isotime(_future_time(), + subsecond=True) return token now = datetime.datetime.utcnow() @@ -168,7 +171,7 @@ class RevokeTests(object): def test_revoke_by_expiration_project_and_domain_fails(self): user_id = _new_id() - expires_at = timeutils.isotime(_future_time(), subsecond=True) + expires_at = utils.isotime(_future_time(), subsecond=True) domain_id = _new_id() project_id = _new_id() self.assertThat( @@ -181,24 +184,20 @@ class RevokeTests(object): class SqlRevokeTests(test_backend_sql.SqlTests, RevokeTests): def config_overrides(self): super(SqlRevokeTests, self).config_overrides() - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.sql.Revoke') + self.config_fixture.config(group='revoke', driver='sql') self.config_fixture.config( group='token', - provider='keystone.token.providers.pki.Provider', + provider='pki', revoke_by_id=False) class KvsRevokeTests(tests.TestCase, RevokeTests): def config_overrides(self): super(KvsRevokeTests, self).config_overrides() - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', - provider='keystone.token.providers.pki.Provider', + provider='pki', revoke_by_id=False) def setUp(self): diff --git a/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py b/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py index edfb91d7..87b3d48d 100644 --- a/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py +++ b/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py @@ -53,12 +53,6 @@ class SqlUpgradeExampleExtension(test_sql_upgrade.SqlMigrateBase): self.upgrade(1, repository=self.repo_path) self.assertTableColumns('example', ['id', 'type', 'extra']) - def test_downgrade(self): - self.upgrade(1, repository=self.repo_path) - self.assertTableColumns('example', ['id', 'type', 'extra']) - self.downgrade(0, repository=self.repo_path) - self.assertTableDoesNotExist('example') - class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase): def repo_package(self): @@ -68,10 +62,6 @@ class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase): super(SqlUpgradeOAuth1Extension, self).upgrade( version, repository=self.repo_path) - def downgrade(self, version): - super(SqlUpgradeOAuth1Extension, self).downgrade( - version, repository=self.repo_path) - def _assert_v1_3_tables(self): self.assertTableColumns('consumer', ['id', @@ -136,18 +126,6 @@ class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase): self.upgrade(5) self._assert_v4_later_tables() - def test_downgrade(self): - self.upgrade(5) - self._assert_v4_later_tables() - self.downgrade(3) - self._assert_v1_3_tables() - self.downgrade(1) - self._assert_v1_3_tables() - self.downgrade(0) - self.assertTableDoesNotExist('consumer') - self.assertTableDoesNotExist('request_token') - self.assertTableDoesNotExist('access_token') - class EndpointFilterExtension(test_sql_upgrade.SqlMigrateBase): def repo_package(self): @@ -157,10 +135,6 @@ class EndpointFilterExtension(test_sql_upgrade.SqlMigrateBase): super(EndpointFilterExtension, self).upgrade( version, repository=self.repo_path) - def downgrade(self, version): - super(EndpointFilterExtension, self).downgrade( - version, repository=self.repo_path) - def _assert_v1_tables(self): self.assertTableColumns('project_endpoint', ['endpoint_id', 'project_id']) @@ -184,14 +158,6 @@ class EndpointFilterExtension(test_sql_upgrade.SqlMigrateBase): self.upgrade(2) self._assert_v2_tables() - def test_downgrade(self): - self.upgrade(2) - self._assert_v2_tables() - self.downgrade(1) - self._assert_v1_tables() - self.downgrade(0) - self.assertTableDoesNotExist('project_endpoint') - class EndpointPolicyExtension(test_sql_upgrade.SqlMigrateBase): def repo_package(self): @@ -204,14 +170,6 @@ class EndpointPolicyExtension(test_sql_upgrade.SqlMigrateBase): ['id', 'policy_id', 'endpoint_id', 'service_id', 'region_id']) - def test_downgrade(self): - self.upgrade(1, repository=self.repo_path) - self.assertTableColumns('policy_association', - ['id', 'policy_id', 'endpoint_id', - 'service_id', 'region_id']) - self.downgrade(0, repository=self.repo_path) - self.assertTableDoesNotExist('policy_association') - class FederationExtension(test_sql_upgrade.SqlMigrateBase): """Test class for ensuring the Federation SQL.""" @@ -264,27 +222,7 @@ class FederationExtension(test_sql_upgrade.SqlMigrateBase): 'federation_protocol') self.assertFalse(federation_protocol.c.mapping_id.nullable) - def test_downgrade(self): - self.upgrade(3, repository=self.repo_path) - self.assertTableColumns(self.identity_provider, - ['id', 'enabled', 'description']) - self.assertTableColumns(self.federation_protocol, - ['id', 'idp_id', 'mapping_id']) - self.assertTableColumns(self.mapping, - ['id', 'rules']) - - self.downgrade(2, repository=self.repo_path) - federation_protocol = utils.get_table( - self.engine, - 'federation_protocol') - self.assertTrue(federation_protocol.c.mapping_id.nullable) - - self.downgrade(0, repository=self.repo_path) - self.assertTableDoesNotExist(self.identity_provider) - self.assertTableDoesNotExist(self.federation_protocol) - self.assertTableDoesNotExist(self.mapping) - - def test_fixup_service_provider_attributes(self): + def test_service_provider_attributes_cannot_be_null(self): self.upgrade(6, repository=self.repo_path) self.assertTableColumns(self.service_provider, ['id', 'description', 'enabled', 'auth_url', @@ -325,12 +263,28 @@ class FederationExtension(test_sql_upgrade.SqlMigrateBase): sp3) session.close() - self.downgrade(5, repository=self.repo_path) + + def test_fixup_service_provider_attributes(self): + session = self.Session() + sp1 = {'id': uuid.uuid4().hex, + 'auth_url': None, + 'sp_url': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True} + sp2 = {'id': uuid.uuid4().hex, + 'auth_url': uuid.uuid4().hex, + 'sp_url': None, + 'description': uuid.uuid4().hex, + 'enabled': True} + sp3 = {'id': uuid.uuid4().hex, + 'auth_url': None, + 'sp_url': None, + 'description': uuid.uuid4().hex, + 'enabled': True} + self.upgrade(5, repository=self.repo_path) self.assertTableColumns(self.service_provider, ['id', 'description', 'enabled', 'auth_url', 'sp_url']) - session = self.Session() - self.metadata.clear() # Before the migration, the table should accept null values self.insert_dict(session, self.service_provider, sp1) @@ -356,13 +310,20 @@ class FederationExtension(test_sql_upgrade.SqlMigrateBase): self.assertEqual('', sp.auth_url) self.assertEqual('', sp.sp_url) -_REVOKE_COLUMN_NAMES = ['id', 'domain_id', 'project_id', 'user_id', 'role_id', - 'trust_id', 'consumer_id', 'access_token_id', - 'issued_before', 'expires_at', 'revoked_at'] + def test_add_relay_state_column(self): + self.upgrade(8, repository=self.repo_path) + self.assertTableColumns(self.service_provider, + ['id', 'description', 'enabled', 'auth_url', + 'relay_state_prefix', 'sp_url']) class RevokeExtension(test_sql_upgrade.SqlMigrateBase): + _REVOKE_COLUMN_NAMES = ['id', 'domain_id', 'project_id', 'user_id', + 'role_id', 'trust_id', 'consumer_id', + 'access_token_id', 'issued_before', 'expires_at', + 'revoked_at'] + def repo_package(self): return revoke @@ -370,11 +331,4 @@ class RevokeExtension(test_sql_upgrade.SqlMigrateBase): self.assertTableDoesNotExist('revocation_event') self.upgrade(1, repository=self.repo_path) self.assertTableColumns('revocation_event', - _REVOKE_COLUMN_NAMES) - - def test_downgrade(self): - self.upgrade(1, repository=self.repo_path) - self.assertTableColumns('revocation_event', - _REVOKE_COLUMN_NAMES) - self.downgrade(0, repository=self.repo_path) - self.assertTableDoesNotExist('revocation_event') + self._REVOKE_COLUMN_NAMES) diff --git a/keystone-moon/keystone/tests/unit/test_sql_upgrade.py b/keystone-moon/keystone/tests/unit/test_sql_upgrade.py index e50bad56..96dfa9e8 100644 --- a/keystone-moon/keystone/tests/unit/test_sql_upgrade.py +++ b/keystone-moon/keystone/tests/unit/test_sql_upgrade.py @@ -38,7 +38,6 @@ from oslo_config import cfg from oslo_db import exception as db_exception from oslo_db.sqlalchemy import migration from oslo_db.sqlalchemy import session as db_session -import six from sqlalchemy.engine import reflection import sqlalchemy.exc from sqlalchemy import schema @@ -158,6 +157,7 @@ class SqlMigrateBase(tests.SQLDriverOverrides, tests.TestCase): # create and share a single sqlalchemy engine for testing self.engine = sql.get_engine() self.Session = db_session.get_maker(self.engine, autocommit=False) + self.addCleanup(sqlalchemy.orm.session.Session.close_all) self.initialize_sql() self.repo_path = migration_helpers.find_migrate_repo( @@ -169,8 +169,12 @@ class SqlMigrateBase(tests.SQLDriverOverrides, tests.TestCase): # auto-detect the highest available schema version in the migrate_repo self.max_version = self.schema.repository.version().version - def tearDown(self): - sqlalchemy.orm.session.Session.close_all() + self.addCleanup(sql.cleanup) + + # drop tables and FKs. + self.addCleanup(self._cleanupDB) + + def _cleanupDB(self): meta = sqlalchemy.MetaData() meta.bind = self.engine meta.reflect(self.engine) @@ -193,14 +197,12 @@ class SqlMigrateBase(tests.SQLDriverOverrides, tests.TestCase): all_fks.extend(fks) for fkc in all_fks: - conn.execute(schema.DropConstraint(fkc)) + if self.engine.name != 'sqlite': + conn.execute(schema.DropConstraint(fkc)) for table in tbs: conn.execute(schema.DropTable(table)) - sql.cleanup() - super(SqlMigrateBase, self).tearDown() - def select_table(self, name): table = sqlalchemy.Table(name, self.metadata, @@ -230,9 +232,6 @@ class SqlMigrateBase(tests.SQLDriverOverrides, tests.TestCase): def upgrade(self, *args, **kwargs): self._migrate(*args, **kwargs) - def downgrade(self, *args, **kwargs): - self._migrate(*args, downgrade=True, **kwargs) - def _migrate(self, version, repository=None, downgrade=False, current_schema=None): repository = repository or self.repo_path @@ -278,42 +277,6 @@ class SqlUpgradeTests(SqlMigrateBase): version, 'DB is not at version %s' % migrate_repo.DB_INIT_VERSION) - def test_two_steps_forward_one_step_back(self): - """You should be able to cleanly undo and re-apply all upgrades. - - Upgrades are run in the following order:: - - Starting with the initial version defined at - keystone.common.migrate_repo.DB_INIT_VERSION - - INIT +1 -> INIT +2 -> INIT +1 -> INIT +2 -> INIT +3 -> INIT +2 ... - ^---------------------^ ^---------------------^ - - Downgrade to the DB_INIT_VERSION does not occur based on the - requirement that the base version be DB_INIT_VERSION + 1 before - migration can occur. Downgrade below DB_INIT_VERSION + 1 is no longer - supported. - - DB_INIT_VERSION is the number preceding the release schema version from - two releases prior. Example, Juno releases with the DB_INIT_VERSION - being 35 where Havana (Havana was two releases before Juno) release - schema version is 36. - - The migrate utility requires the db must be initialized under version - control with the revision directly before the first version to be - applied. - - """ - for x in range(migrate_repo.DB_INIT_VERSION + 1, - self.max_version + 1): - self.upgrade(x) - downgrade_ver = x - 1 - # Don't actually downgrade to the init version. This will raise - # a not-implemented error. - if downgrade_ver != migrate_repo.DB_INIT_VERSION: - self.downgrade(x - 1) - self.upgrade(x) - def test_upgrade_add_initial_tables(self): self.upgrade(migrate_repo.DB_INIT_VERSION + 1) self.check_initial_table_structure() @@ -338,32 +301,6 @@ class SqlUpgradeTests(SqlMigrateBase): for k in default_domain.keys(): self.assertEqual(default_domain[k], getattr(refs[0], k)) - def test_downgrade_to_db_init_version(self): - self.upgrade(self.max_version) - - if self.engine.name == 'mysql': - self._mysql_check_all_tables_innodb() - - self.downgrade(migrate_repo.DB_INIT_VERSION + 1) - self.check_initial_table_structure() - - meta = sqlalchemy.MetaData() - meta.bind = self.engine - meta.reflect(self.engine) - - initial_table_set = set(INITIAL_TABLE_STRUCTURE.keys()) - table_set = set(meta.tables.keys()) - # explicitly remove the migrate_version table, this is not controlled - # by the migration scripts and should be exempt from this check. - table_set.remove('migrate_version') - - self.assertSetEqual(initial_table_set, table_set) - # Downgrade to before Icehouse's release schema version (044) is not - # supported. A NotImplementedError should be raised when attempting to - # downgrade. - self.assertRaises(NotImplementedError, self.downgrade, - migrate_repo.DB_INIT_VERSION) - def insert_dict(self, session, table_name, d, table=None): """Naively inserts key-value pairs into a table, given a dictionary.""" if table is None: @@ -380,8 +317,6 @@ class SqlUpgradeTests(SqlMigrateBase): self.assertTableDoesNotExist('id_mapping') self.upgrade(51) self.assertTableExists('id_mapping') - self.downgrade(50) - self.assertTableDoesNotExist('id_mapping') def test_region_url_upgrade(self): self.upgrade(52) @@ -389,42 +324,6 @@ class SqlUpgradeTests(SqlMigrateBase): ['id', 'description', 'parent_region_id', 'extra', 'url']) - def test_region_url_downgrade(self): - self.upgrade(52) - self.downgrade(51) - self.assertTableColumns('region', - ['id', 'description', 'parent_region_id', - 'extra']) - - def test_region_url_cleanup(self): - # make sure that the url field is dropped in the downgrade - self.upgrade(52) - session = self.Session() - beta = { - 'id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'parent_region_id': uuid.uuid4().hex, - 'url': uuid.uuid4().hex - } - acme = { - 'id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'parent_region_id': uuid.uuid4().hex, - 'url': None - } - self.insert_dict(session, 'region', beta) - self.insert_dict(session, 'region', acme) - region_table = sqlalchemy.Table('region', self.metadata, autoload=True) - self.assertEqual(2, session.query(region_table).count()) - session.close() - self.downgrade(51) - session = self.Session() - self.metadata.clear() - region_table = sqlalchemy.Table('region', self.metadata, autoload=True) - self.assertEqual(2, session.query(region_table).count()) - region = session.query(region_table)[0] - self.assertRaises(AttributeError, getattr, region, 'url') - def test_endpoint_region_upgrade_columns(self): self.upgrade(53) self.assertTableColumns('endpoint', @@ -439,21 +338,6 @@ class SqlUpgradeTests(SqlMigrateBase): autoload=True) self.assertEqual(255, endpoint_table.c.region_id.type.length) - def test_endpoint_region_downgrade_columns(self): - self.upgrade(53) - self.downgrade(52) - self.assertTableColumns('endpoint', - ['id', 'legacy_endpoint_id', 'interface', - 'service_id', 'url', 'extra', 'enabled', - 'region']) - region_table = sqlalchemy.Table('region', self.metadata, autoload=True) - self.assertEqual(64, region_table.c.id.type.length) - self.assertEqual(64, region_table.c.parent_region_id.type.length) - endpoint_table = sqlalchemy.Table('endpoint', - self.metadata, - autoload=True) - self.assertEqual(255, endpoint_table.c.region.type.length) - def test_endpoint_region_migration(self): self.upgrade(52) session = self.Session() @@ -519,106 +403,29 @@ class SqlUpgradeTests(SqlMigrateBase): self.assertEqual(1, session.query(endpoint_table). filter_by(region_id=_small_region_name).count()) - # downgrade to 52 - session.close() - self.downgrade(52) - session = self.Session() - self.metadata.clear() - - region_table = sqlalchemy.Table('region', self.metadata, autoload=True) - self.assertEqual(1, session.query(region_table).count()) - self.assertEqual(1, session.query(region_table). - filter_by(id=_small_region_name).count()) - - endpoint_table = sqlalchemy.Table('endpoint', - self.metadata, - autoload=True) - self.assertEqual(5, session.query(endpoint_table).count()) - self.assertEqual(2, session.query(endpoint_table). - filter_by(region=_long_region_name).count()) - self.assertEqual(1, session.query(endpoint_table). - filter_by(region=_clashing_region_name).count()) - self.assertEqual(1, session.query(endpoint_table). - filter_by(region=_small_region_name).count()) - def test_add_actor_id_index(self): self.upgrade(53) self.upgrade(54) table = sqlalchemy.Table('assignment', self.metadata, autoload=True) - index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] + index_data = [(idx.name, list(idx.columns.keys())) + for idx in table.indexes] self.assertIn(('ix_actor_id', ['actor_id']), index_data) def test_token_user_id_and_trust_id_index_upgrade(self): self.upgrade(54) self.upgrade(55) table = sqlalchemy.Table('token', self.metadata, autoload=True) - index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] + index_data = [(idx.name, list(idx.columns.keys())) + for idx in table.indexes] self.assertIn(('ix_token_user_id', ['user_id']), index_data) self.assertIn(('ix_token_trust_id', ['trust_id']), index_data) - def test_token_user_id_and_trust_id_index_downgrade(self): - self.upgrade(55) - self.downgrade(54) - table = sqlalchemy.Table('token', self.metadata, autoload=True) - index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] - self.assertNotIn(('ix_token_user_id', ['user_id']), index_data) - self.assertNotIn(('ix_token_trust_id', ['trust_id']), index_data) - - def test_remove_actor_id_index(self): - self.upgrade(54) - self.downgrade(53) - table = sqlalchemy.Table('assignment', self.metadata, autoload=True) - index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] - self.assertNotIn(('ix_actor_id', ['actor_id']), index_data) - def test_project_parent_id_upgrade(self): self.upgrade(61) self.assertTableColumns('project', ['id', 'name', 'extra', 'description', 'enabled', 'domain_id', 'parent_id']) - def test_project_parent_id_downgrade(self): - self.upgrade(61) - self.downgrade(60) - self.assertTableColumns('project', - ['id', 'name', 'extra', 'description', - 'enabled', 'domain_id']) - - def test_project_parent_id_cleanup(self): - # make sure that the parent_id field is dropped in the downgrade - self.upgrade(61) - session = self.Session() - domain = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'enabled': True} - acme = { - 'id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'domain_id': domain['id'], - 'name': uuid.uuid4().hex, - 'parent_id': None - } - beta = { - 'id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'domain_id': domain['id'], - 'name': uuid.uuid4().hex, - 'parent_id': acme['id'] - } - self.insert_dict(session, 'domain', domain) - self.insert_dict(session, 'project', acme) - self.insert_dict(session, 'project', beta) - proj_table = sqlalchemy.Table('project', self.metadata, autoload=True) - self.assertEqual(2, session.query(proj_table).count()) - session.close() - self.downgrade(60) - session = self.Session() - self.metadata.clear() - proj_table = sqlalchemy.Table('project', self.metadata, autoload=True) - self.assertEqual(2, session.query(proj_table).count()) - project = session.query(proj_table)[0] - self.assertRaises(AttributeError, getattr, project, 'parent_id') - def test_drop_assignment_role_fk(self): self.upgrade(61) self.assertTrue(self.does_fk_exist('assignment', 'role_id')) @@ -626,8 +433,80 @@ class SqlUpgradeTests(SqlMigrateBase): if self.engine.name != 'sqlite': # sqlite does not support FK deletions (or enforcement) self.assertFalse(self.does_fk_exist('assignment', 'role_id')) - self.downgrade(61) - self.assertTrue(self.does_fk_exist('assignment', 'role_id')) + + def test_insert_assignment_inherited_pk(self): + ASSIGNMENT_TABLE_NAME = 'assignment' + INHERITED_COLUMN_NAME = 'inherited' + ROLE_TABLE_NAME = 'role' + + self.upgrade(72) + + # Check that the 'inherited' column is not part of the PK + self.assertFalse(self.does_pk_exist(ASSIGNMENT_TABLE_NAME, + INHERITED_COLUMN_NAME)) + + session = self.Session() + + role = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.insert_dict(session, ROLE_TABLE_NAME, role) + + # Create both inherited and noninherited role assignments + inherited = {'type': 'UserProject', + 'actor_id': uuid.uuid4().hex, + 'target_id': uuid.uuid4().hex, + 'role_id': role['id'], + 'inherited': True} + + noninherited = inherited.copy() + noninherited['inherited'] = False + + # Create another inherited role assignment as a spoiler + spoiler = inherited.copy() + spoiler['actor_id'] = uuid.uuid4().hex + + self.insert_dict(session, ASSIGNMENT_TABLE_NAME, inherited) + self.insert_dict(session, ASSIGNMENT_TABLE_NAME, spoiler) + + # Since 'inherited' is not part of the PK, we can't insert noninherited + self.assertRaises(db_exception.DBDuplicateEntry, + self.insert_dict, + session, + ASSIGNMENT_TABLE_NAME, + noninherited) + + session.close() + + self.upgrade(73) + + session = self.Session() + self.metadata.clear() + + # Check that the 'inherited' column is now part of the PK + self.assertTrue(self.does_pk_exist(ASSIGNMENT_TABLE_NAME, + INHERITED_COLUMN_NAME)) + + # The noninherited role assignment can now be inserted + self.insert_dict(session, ASSIGNMENT_TABLE_NAME, noninherited) + + assignment_table = sqlalchemy.Table(ASSIGNMENT_TABLE_NAME, + self.metadata, + autoload=True) + + assignments = session.query(assignment_table).all() + for assignment in (inherited, spoiler, noninherited): + self.assertIn((assignment['type'], assignment['actor_id'], + assignment['target_id'], assignment['role_id'], + assignment['inherited']), + assignments) + + def does_pk_exist(self, table, pk_column): + """Checks whether a column is primary key on a table.""" + + inspector = reflection.Inspector.from_engine(self.engine) + pk_columns = inspector.get_pk_constraint(table)['constrained_columns'] + + return pk_column in pk_columns def does_fk_exist(self, table, fk_column): inspector = reflection.Inspector.from_engine(self.engine) @@ -642,14 +521,7 @@ class SqlUpgradeTests(SqlMigrateBase): ['id', 'description', 'parent_region_id', 'extra']) - def test_drop_region_url_downgrade(self): - self.upgrade(63) - self.downgrade(62) - self.assertTableColumns('region', - ['id', 'description', 'parent_region_id', - 'extra', 'url']) - - def test_drop_domain_fk(self): + def test_domain_fk(self): self.upgrade(63) self.assertTrue(self.does_fk_exist('group', 'domain_id')) self.assertTrue(self.does_fk_exist('user', 'domain_id')) @@ -658,9 +530,6 @@ class SqlUpgradeTests(SqlMigrateBase): # sqlite does not support FK deletions (or enforcement) self.assertFalse(self.does_fk_exist('group', 'domain_id')) self.assertFalse(self.does_fk_exist('user', 'domain_id')) - self.downgrade(63) - self.assertTrue(self.does_fk_exist('group', 'domain_id')) - self.assertTrue(self.does_fk_exist('user', 'domain_id')) def test_add_domain_config(self): whitelisted_table = 'whitelisted_config' @@ -673,9 +542,6 @@ class SqlUpgradeTests(SqlMigrateBase): ['domain_id', 'group', 'option', 'value']) self.assertTableColumns(sensitive_table, ['domain_id', 'group', 'option', 'value']) - self.downgrade(64) - self.assertTableDoesNotExist(whitelisted_table) - self.assertTableDoesNotExist(sensitive_table) def test_fixup_service_name_value_upgrade(self): """Update service name data from `extra` to empty string.""" @@ -724,6 +590,10 @@ class SqlUpgradeTests(SqlMigrateBase): random_attr_name_empty, random_attr_name_none_str), ] + # NOTE(viktors): Add a service with empty extra field + self.insert_dict(session, 'service', + {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex}) + session.close() self.upgrade(66) session = self.Session() @@ -744,6 +614,28 @@ class SqlUpgradeTests(SqlMigrateBase): extra = fetch_service_extra(service_id) self.assertDictEqual(exp_extra, extra, msg) + def _does_index_exist(self, table_name, index_name): + meta = sqlalchemy.MetaData(bind=self.engine) + table = sqlalchemy.Table('assignment', meta, autoload=True) + return index_name in [idx.name for idx in table.indexes] + + def test_drop_assignment_role_id_index_mysql(self): + self.upgrade(66) + if self.engine.name == "mysql": + self.assertTrue(self._does_index_exist('assignment', + 'assignment_role_id_fkey')) + self.upgrade(67) + if self.engine.name == "mysql": + self.assertFalse(self._does_index_exist('assignment', + 'assignment_role_id_fkey')) + + def test_project_is_domain_upgrade(self): + self.upgrade(74) + self.assertTableColumns('project', + ['id', 'name', 'extra', 'description', + 'enabled', 'domain_id', 'parent_id', + 'is_domain']) + def populate_user_table(self, with_pass_enab=False, with_pass_enab_domain=False): # Populate the appropriate fields in the user @@ -881,6 +773,13 @@ class VersionTests(SqlMigrateBase): version = migration_helpers.get_db_version() self.assertEqual(self.max_version, version) + def test_assert_not_schema_downgrade(self): + self.upgrade(self.max_version) + self.assertRaises( + db_exception.DbMigrationError, + migration_helpers._sync_common_repo, + self.max_version - 1) + def test_extension_not_controlled(self): """When get the version before controlling, raises DbMigrationError.""" self.assertRaises(db_exception.DbMigrationError, @@ -889,7 +788,7 @@ class VersionTests(SqlMigrateBase): def test_extension_initial(self): """When get the initial version of an extension, it's 0.""" - for name, extension in six.iteritems(EXTENSIONS): + for name, extension in EXTENSIONS.items(): abs_path = migration_helpers.find_migrate_repo(extension) migration.db_version_control(sql.get_engine(), abs_path) version = migration_helpers.get_db_version(extension=name) @@ -898,18 +797,7 @@ class VersionTests(SqlMigrateBase): def test_extension_migrated(self): """When get the version after migrating an extension, it's not 0.""" - for name, extension in six.iteritems(EXTENSIONS): - abs_path = migration_helpers.find_migrate_repo(extension) - migration.db_version_control(sql.get_engine(), abs_path) - migration.db_sync(sql.get_engine(), abs_path) - version = migration_helpers.get_db_version(extension=name) - self.assertTrue( - version > 0, - "Version for %s didn't change after migrated?" % name) - - def test_extension_downgraded(self): - """When get the version after downgrading an extension, it is 0.""" - for name, extension in six.iteritems(EXTENSIONS): + for name, extension in EXTENSIONS.items(): abs_path = migration_helpers.find_migrate_repo(extension) migration.db_version_control(sql.get_engine(), abs_path) migration.db_sync(sql.get_engine(), abs_path) @@ -917,10 +805,47 @@ class VersionTests(SqlMigrateBase): self.assertTrue( version > 0, "Version for %s didn't change after migrated?" % name) - migration.db_sync(sql.get_engine(), abs_path, version=0) - version = migration_helpers.get_db_version(extension=name) - self.assertEqual(0, version, - 'Migrate version for %s is not 0' % name) + # Verify downgrades cannot occur + self.assertRaises( + db_exception.DbMigrationError, + migration_helpers._sync_extension_repo, + extension=name, + version=0) + + def test_extension_federation_upgraded_values(self): + abs_path = migration_helpers.find_migrate_repo(federation) + migration.db_version_control(sql.get_engine(), abs_path) + migration.db_sync(sql.get_engine(), abs_path, version=6) + idp_table = sqlalchemy.Table("identity_provider", + self.metadata, + autoload=True) + idps = [{'id': uuid.uuid4().hex, + 'enabled': True, + 'description': uuid.uuid4().hex, + 'remote_id': uuid.uuid4().hex}, + {'id': uuid.uuid4().hex, + 'enabled': True, + 'description': uuid.uuid4().hex, + 'remote_id': uuid.uuid4().hex}] + for idp in idps: + ins = idp_table.insert().values({'id': idp['id'], + 'enabled': idp['enabled'], + 'description': idp['description'], + 'remote_id': idp['remote_id']}) + self.engine.execute(ins) + migration.db_sync(sql.get_engine(), abs_path) + idp_remote_ids_table = sqlalchemy.Table("idp_remote_ids", + self.metadata, + autoload=True) + for idp in idps: + s = idp_remote_ids_table.select().where( + idp_remote_ids_table.c.idp_id == idp['id']) + remote = self.engine.execute(s).fetchone() + self.assertEqual(idp['remote_id'], + remote['remote_id'], + 'remote_ids must be preserved during the ' + 'migration from identity_provider table to ' + 'idp_remote_ids table') def test_unexpected_extension(self): """The version for an extension that doesn't exist raises ImportError. diff --git a/keystone-moon/keystone/tests/unit/test_ssl.py b/keystone-moon/keystone/tests/unit/test_ssl.py index c5f443b0..3b86bb2d 100644 --- a/keystone-moon/keystone/tests/unit/test_ssl.py +++ b/keystone-moon/keystone/tests/unit/test_ssl.py @@ -36,6 +36,16 @@ CLIENT = os.path.join(CERTDIR, 'middleware.pem') class SSLTestCase(tests.TestCase): def setUp(self): super(SSLTestCase, self).setUp() + raise self.skipTest('SSL Version and Ciphers cannot be configured ' + 'with eventlet, some platforms have disabled ' + 'SSLv3. See bug 1381365.') + # NOTE(morganfainberg): It has been determined that this + # will not be fixed. These tests should be re-enabled for the full + # functional test suite when run against an SSL terminated + # endpoint. Some distributions/environments have patched OpenSSL to + # not have SSLv3 at all due to POODLE and this causes differing + # behavior depending on platform. See bug 1381365 for more information. + # NOTE(jamespage): # Deal with more secure certificate chain verification # introduced in python 2.7.9 under PEP-0476 diff --git a/keystone-moon/keystone/tests/unit/test_token_provider.py b/keystone-moon/keystone/tests/unit/test_token_provider.py index dc08664f..3ebb0187 100644 --- a/keystone-moon/keystone/tests/unit/test_token_provider.py +++ b/keystone-moon/keystone/tests/unit/test_token_provider.py @@ -18,11 +18,14 @@ from oslo_config import cfg from oslo_utils import timeutils from keystone.common import dependency +from keystone.common import utils from keystone import exception from keystone.tests import unit as tests from keystone.tests.unit.ksfixtures import database from keystone import token +from keystone.token.providers import fernet from keystone.token.providers import pki +from keystone.token.providers import pkiz from keystone.token.providers import uuid @@ -655,8 +658,8 @@ def create_v2_token(): return { "access": { "token": { - "expires": timeutils.isotime(timeutils.utcnow() + - FUTURE_DELTA), + "expires": utils.isotime(timeutils.utcnow() + + FUTURE_DELTA), "issued_at": "2013-05-21T00:02:43.941473Z", "tenant": { "enabled": True, @@ -671,7 +674,7 @@ def create_v2_token(): SAMPLE_V2_TOKEN_EXPIRED = { "access": { "token": { - "expires": timeutils.isotime(CURRENT_DATE), + "expires": utils.isotime(CURRENT_DATE), "issued_at": "2013-05-21T00:02:43.941473Z", "tenant": { "enabled": True, @@ -687,7 +690,7 @@ def create_v3_token(): return { "token": { 'methods': [], - "expires_at": timeutils.isotime(timeutils.utcnow() + FUTURE_DELTA), + "expires_at": utils.isotime(timeutils.utcnow() + FUTURE_DELTA), "issued_at": "2013-05-21T00:02:43.941473Z", } } @@ -695,7 +698,7 @@ def create_v3_token(): SAMPLE_V3_TOKEN_EXPIRED = { "token": { - "expires_at": timeutils.isotime(CURRENT_DATE), + "expires_at": utils.isotime(CURRENT_DATE), "issued_at": "2013-05-21T00:02:43.941473Z", } } @@ -742,22 +745,20 @@ class TestTokenProvider(tests.TestCase): uuid.Provider) dependency.reset() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.uuid.Provider') - token.provider.Manager() + self.config_fixture.config(group='token', provider='uuid') + self.assertIsInstance(token.provider.Manager().driver, uuid.Provider) dependency.reset() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.pki.Provider') - token.provider.Manager() + self.config_fixture.config(group='token', provider='pki') + self.assertIsInstance(token.provider.Manager().driver, pki.Provider) dependency.reset() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.pkiz.Provider') - token.provider.Manager() + self.config_fixture.config(group='token', provider='pkiz') + self.assertIsInstance(token.provider.Manager().driver, pkiz.Provider) + + dependency.reset() + self.config_fixture.config(group='token', provider='fernet') + self.assertIsInstance(token.provider.Manager().driver, fernet.Provider) def test_unsupported_token_provider(self): self.config_fixture.config(group='token', diff --git a/keystone-moon/keystone/tests/unit/test_v2.py b/keystone-moon/keystone/tests/unit/test_v2.py index 8c7c3792..415150cf 100644 --- a/keystone-moon/keystone/tests/unit/test_v2.py +++ b/keystone-moon/keystone/tests/unit/test_v2.py @@ -56,6 +56,8 @@ class CoreApiTests(object): def assertValidTenant(self, tenant): self.assertIsNotNone(tenant.get('id')) self.assertIsNotNone(tenant.get('name')) + self.assertNotIn('domain_id', tenant) + self.assertNotIn('parent_id', tenant) def assertValidUser(self, user): self.assertIsNotNone(user.get('id')) @@ -1373,12 +1375,10 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): class RevokeApiTestCase(V2TestCase): def config_overrides(self): super(RevokeApiTestCase, self).config_overrides() - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', - provider='keystone.token.providers.pki.Provider', + provider='pki', revoke_by_id=False) def test_fetch_revocation_list_admin_200(self): @@ -1410,9 +1410,7 @@ class TestFernetTokenProviderV2(RestfulTestCase): def config_overrides(self): super(TestFernetTokenProviderV2, self).config_overrides() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.fernet.Provider') + self.config_fixture.config(group='token', provider='fernet') def test_authenticate_unscoped_token(self): unscoped_token = self.get_unscoped_token() @@ -1498,3 +1496,44 @@ class TestFernetTokenProviderV2(RestfulTestCase): path=path, token=CONF.admin_token, expected_status=200) + + def test_rescoped_tokens_maintain_original_expiration(self): + project_ref = self.new_project_ref() + self.resource_api.create_project(project_ref['id'], project_ref) + self.assignment_api.add_role_to_user_and_project(self.user_foo['id'], + project_ref['id'], + self.role_admin['id']) + resp = self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'tenantName': project_ref['name'], + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'] + } + } + }, + # NOTE(lbragstad): This test may need to be refactored if Keystone + # decides to disallow rescoping using a scoped token. + expected_status=200) + original_token = resp.result['access']['token']['id'] + original_expiration = resp.result['access']['token']['expires'] + + resp = self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'tenantName': project_ref['name'], + 'token': { + 'id': original_token, + } + } + }, + expected_status=200) + rescoped_token = resp.result['access']['token']['id'] + rescoped_expiration = resp.result['access']['token']['expires'] + self.assertNotEqual(original_token, rescoped_token) + self.assertEqual(original_expiration, rescoped_expiration) diff --git a/keystone-moon/keystone/tests/unit/test_v2_controller.py b/keystone-moon/keystone/tests/unit/test_v2_controller.py index 6c1edd0a..0d4b3cdc 100644 --- a/keystone-moon/keystone/tests/unit/test_v2_controller.py +++ b/keystone-moon/keystone/tests/unit/test_v2_controller.py @@ -16,6 +16,7 @@ import uuid from keystone.assignment import controllers as assignment_controllers +from keystone import exception from keystone.resource import controllers as resource_controllers from keystone.tests import unit as tests from keystone.tests.unit import default_fixtures @@ -92,4 +93,51 @@ class TenantTestCase(tests.TestCase): for tenant in default_fixtures.TENANTS: tenant_copy = tenant.copy() tenant_copy.pop('domain_id') + tenant_copy.pop('parent_id') + tenant_copy.pop('is_domain') self.assertIn(tenant_copy, refs['tenants']) + + def _create_is_domain_project(self): + project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': 'default', 'is_domain': True} + project_ref = self.resource_api.create_project(project['id'], project) + return self.tenant_controller.v3_to_v2_project(project_ref) + + def test_update_is_domain_project_not_found(self): + """Test that update is_domain project is not allowed in v2.""" + project = self._create_is_domain_project() + + project['name'] = uuid.uuid4().hex + self.assertRaises( + exception.ProjectNotFound, + self.tenant_controller.update_project, + _ADMIN_CONTEXT, + project['id'], + project + ) + + def test_delete_is_domain_project_not_found(self): + """Test that delete is_domain project is not allowed in v2.""" + project = self._create_is_domain_project() + + self.assertRaises( + exception.ProjectNotFound, + self.tenant_controller.delete_project, + _ADMIN_CONTEXT, + project['id'] + ) + + def test_list_is_domain_project_not_found(self): + """Test v2 get_all_projects having projects that act as a domain. + + In v2 no project with the is_domain flag enabled should be + returned. + """ + project1 = self._create_is_domain_project() + project2 = self._create_is_domain_project() + + refs = self.tenant_controller.get_all_projects(_ADMIN_CONTEXT) + projects = refs.get('tenants') + + self.assertNotIn(project1, projects) + self.assertNotIn(project2, projects) diff --git a/keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py b/keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py index 7abc5bc4..e0843605 100644 --- a/keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py +++ b/keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py @@ -15,12 +15,14 @@ import datetime import uuid +from keystoneclient.contrib.ec2 import utils as ec2_utils from keystoneclient import exceptions as client_exceptions from keystoneclient.v2_0 import client as ks_client import mock from oslo_config import cfg from oslo_serialization import jsonutils from oslo_utils import timeutils +from six.moves import range import webob from keystone.tests import unit as tests @@ -35,6 +37,11 @@ DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id class ClientDrivenTestCase(tests.TestCase): + def config_files(self): + config_files = super(ClientDrivenTestCase, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + def setUp(self): super(ClientDrivenTestCase, self).setUp() @@ -61,7 +68,10 @@ class ClientDrivenTestCase(tests.TestCase): fixture = self.useFixture(appserver.AppServer(conf, appserver.ADMIN)) self.admin_server = fixture.server - self.addCleanup(self.cleanup_instance('public_server', 'admin_server')) + self.default_client = self.get_client() + + self.addCleanup(self.cleanup_instance('public_server', 'admin_server', + 'default_client')) def _public_url(self): public_port = self.public_server.socket_info['socket'][1] @@ -707,6 +717,20 @@ class ClientDrivenTestCase(tests.TestCase): client.roles.create, name="") + def test_role_create_member_role(self): + # delete the member role so that we can recreate it + client = self.get_client(admin=True) + client.roles.delete(role=CONF.member_role_id) + + # deleting the member role revokes our token, so re-authenticate + client = self.get_client(admin=True) + + # specify only the role name on creation + role = client.roles.create(name=CONF.member_role_name) + + # the ID should be set as defined in CONF + self.assertEqual(CONF.member_role_id, role.id) + def test_role_get_404(self): client = self.get_client(admin=True) self.assertRaises(client_exceptions.NotFound, @@ -1043,3 +1067,308 @@ class ClientDrivenTestCase(tests.TestCase): self.assertRaises(client_exceptions.Unauthorized, client.tenants.list) client.auth_token = new_token_id client.tenants.list() + + def test_endpoint_crud(self): + client = self.get_client(admin=True) + + service = client.services.create(name=uuid.uuid4().hex, + service_type=uuid.uuid4().hex, + description=uuid.uuid4().hex) + + endpoint_region = uuid.uuid4().hex + invalid_service_id = uuid.uuid4().hex + endpoint_publicurl = uuid.uuid4().hex + endpoint_internalurl = uuid.uuid4().hex + endpoint_adminurl = uuid.uuid4().hex + + # a non-existent service ID should trigger a 400 + self.assertRaises(client_exceptions.BadRequest, + client.endpoints.create, + region=endpoint_region, + service_id=invalid_service_id, + publicurl=endpoint_publicurl, + adminurl=endpoint_adminurl, + internalurl=endpoint_internalurl) + + endpoint = client.endpoints.create(region=endpoint_region, + service_id=service.id, + publicurl=endpoint_publicurl, + adminurl=endpoint_adminurl, + internalurl=endpoint_internalurl) + + self.assertEqual(endpoint_region, endpoint.region) + self.assertEqual(service.id, endpoint.service_id) + self.assertEqual(endpoint_publicurl, endpoint.publicurl) + self.assertEqual(endpoint_internalurl, endpoint.internalurl) + self.assertEqual(endpoint_adminurl, endpoint.adminurl) + + client.endpoints.delete(id=endpoint.id) + self.assertRaises(client_exceptions.NotFound, client.endpoints.delete, + id=endpoint.id) + + def _send_ec2_auth_request(self, credentials, client=None): + if not client: + client = self.default_client + url = '%s/ec2tokens' % self.default_client.auth_url + (resp, token) = client.request( + url=url, method='POST', + body={'credentials': credentials}) + return resp, token + + def _generate_default_user_ec2_credentials(self): + cred = self. default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + return self._generate_user_ec2_credentials(cred.access, cred.secret) + + def _generate_user_ec2_credentials(self, access, secret): + signer = ec2_utils.Ec2Signer(secret) + credentials = {'params': {'SignatureVersion': '2'}, + 'access': access, + 'verb': 'GET', + 'host': 'localhost', + 'path': '/service/cloud'} + signature = signer.generate(credentials) + return credentials, signature + + def test_ec2_auth_success(self): + credentials, signature = self._generate_default_user_ec2_credentials() + credentials['signature'] = signature + resp, token = self._send_ec2_auth_request(credentials) + self.assertEqual(200, resp.status_code) + self.assertIn('access', token) + + def test_ec2_auth_success_trust(self): + # Add "other" role user_foo and create trust delegating it to user_two + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_bar['id'], + self.role_other['id']) + trust_id = 'atrust123' + trust = {'trustor_user_id': self.user_foo['id'], + 'trustee_user_id': self.user_two['id'], + 'project_id': self.tenant_bar['id'], + 'impersonation': True} + roles = [self.role_other] + self.trust_api.create_trust(trust_id, trust, roles) + + # Create a client for user_two, scoped to the trust + client = self.get_client(self.user_two) + ret = client.authenticate(trust_id=trust_id, + tenant_id=self.tenant_bar['id']) + self.assertTrue(ret) + self.assertTrue(client.auth_ref.trust_scoped) + self.assertEqual(trust_id, client.auth_ref.trust_id) + + # Create an ec2 keypair using the trust client impersonating user_foo + cred = client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + credentials, signature = self._generate_user_ec2_credentials( + cred.access, cred.secret) + credentials['signature'] = signature + resp, token = self._send_ec2_auth_request(credentials) + self.assertEqual(200, resp.status_code) + self.assertEqual(trust_id, token['access']['trust']['id']) + # TODO(shardy) we really want to check the roles and trustee + # but because of where the stubbing happens we don't seem to + # hit the necessary code in controllers.py _authenticate_token + # so although all is OK via a real request, it incorrect in + # this test.. + + def test_ec2_auth_failure(self): + credentials, signature = self._generate_default_user_ec2_credentials() + credentials['signature'] = uuid.uuid4().hex + self.assertRaises(client_exceptions.Unauthorized, + self._send_ec2_auth_request, + credentials) + + def test_ec2_credential_crud(self): + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual([], creds) + + cred = self.default_client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual(creds, [cred]) + got = self.default_client.ec2.get(user_id=self.user_foo['id'], + access=cred.access) + self.assertEqual(cred, got) + + self.default_client.ec2.delete(user_id=self.user_foo['id'], + access=cred.access) + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual([], creds) + + def test_ec2_credential_crud_non_admin(self): + na_client = self.get_client(self.user_two) + creds = na_client.ec2.list(user_id=self.user_two['id']) + self.assertEqual([], creds) + + cred = na_client.ec2.create(user_id=self.user_two['id'], + tenant_id=self.tenant_baz['id']) + creds = na_client.ec2.list(user_id=self.user_two['id']) + self.assertEqual(creds, [cred]) + got = na_client.ec2.get(user_id=self.user_two['id'], + access=cred.access) + self.assertEqual(cred, got) + + na_client.ec2.delete(user_id=self.user_two['id'], + access=cred.access) + creds = na_client.ec2.list(user_id=self.user_two['id']) + self.assertEqual([], creds) + + def test_ec2_list_credentials(self): + cred_1 = self.default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + cred_2 = self.default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_service['id']) + cred_3 = self.default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_mtu['id']) + two = self.get_client(self.user_two) + cred_4 = two.ec2.create(user_id=self.user_two['id'], + tenant_id=self.tenant_bar['id']) + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual(3, len(creds)) + self.assertEqual(sorted([cred_1, cred_2, cred_3], + key=lambda x: x.access), + sorted(creds, key=lambda x: x.access)) + self.assertNotIn(cred_4, creds) + + def test_ec2_credentials_create_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.create, + user_id=uuid.uuid4().hex, + tenant_id=self.tenant_bar['id']) + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.create, + user_id=self.user_foo['id'], + tenant_id=uuid.uuid4().hex) + + def test_ec2_credentials_delete_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.delete, + user_id=uuid.uuid4().hex, + access=uuid.uuid4().hex) + + def test_ec2_credentials_get_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.get, + user_id=uuid.uuid4().hex, + access=uuid.uuid4().hex) + + def test_ec2_credentials_list_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.list, + user_id=uuid.uuid4().hex) + + def test_ec2_credentials_list_user_forbidden(self): + two = self.get_client(self.user_two) + self.assertRaises(client_exceptions.Forbidden, two.ec2.list, + user_id=self.user_foo['id']) + + def test_ec2_credentials_get_user_forbidden(self): + cred = self.default_client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + + two = self.get_client(self.user_two) + self.assertRaises(client_exceptions.Forbidden, two.ec2.get, + user_id=self.user_foo['id'], access=cred.access) + + self.default_client.ec2.delete(user_id=self.user_foo['id'], + access=cred.access) + + def test_ec2_credentials_delete_user_forbidden(self): + cred = self.default_client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + + two = self.get_client(self.user_two) + self.assertRaises(client_exceptions.Forbidden, two.ec2.delete, + user_id=self.user_foo['id'], access=cred.access) + + self.default_client.ec2.delete(user_id=self.user_foo['id'], + access=cred.access) + + def test_endpoint_create_nonexistent_service(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.BadRequest, + client.endpoints.create, + region=uuid.uuid4().hex, + service_id=uuid.uuid4().hex, + publicurl=uuid.uuid4().hex, + adminurl=uuid.uuid4().hex, + internalurl=uuid.uuid4().hex) + + def test_policy_crud(self): + # FIXME(dolph): this test was written prior to the v3 implementation of + # the client and essentially refers to a non-existent + # policy manager in the v2 client. this test needs to be + # moved to a test suite running against the v3 api + self.skipTest('Written prior to v3 client; needs refactor') + + client = self.get_client(admin=True) + + policy_blob = uuid.uuid4().hex + policy_type = uuid.uuid4().hex + service = client.services.create( + name=uuid.uuid4().hex, + service_type=uuid.uuid4().hex, + description=uuid.uuid4().hex) + endpoint = client.endpoints.create( + service_id=service.id, + region=uuid.uuid4().hex, + adminurl=uuid.uuid4().hex, + internalurl=uuid.uuid4().hex, + publicurl=uuid.uuid4().hex) + + # create + policy = client.policies.create( + blob=policy_blob, + type=policy_type, + endpoint=endpoint.id) + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + policy = client.policies.get(policy=policy.id) + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + endpoints = [x for x in client.endpoints.list() if x.id == endpoint.id] + endpoint = endpoints[0] + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + # update + policy_blob = uuid.uuid4().hex + policy_type = uuid.uuid4().hex + endpoint = client.endpoints.create( + service_id=service.id, + region=uuid.uuid4().hex, + adminurl=uuid.uuid4().hex, + internalurl=uuid.uuid4().hex, + publicurl=uuid.uuid4().hex) + + policy = client.policies.update( + policy=policy.id, + blob=policy_blob, + type=policy_type, + endpoint=endpoint.id) + + policy = client.policies.get(policy=policy.id) + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + # delete + client.policies.delete(policy=policy.id) + self.assertRaises( + client_exceptions.NotFound, + client.policies.get, + policy=policy.id) + policies = [x for x in client.policies.list() if x.id == policy.id] + self.assertEqual(0, len(policies)) diff --git a/keystone-moon/keystone/tests/unit/test_v3.py b/keystone-moon/keystone/tests/unit/test_v3.py index f6d6ed93..9bbfa103 100644 --- a/keystone-moon/keystone/tests/unit/test_v3.py +++ b/keystone-moon/keystone/tests/unit/test_v3.py @@ -299,10 +299,11 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase, ref = self.new_ref() return ref - def new_project_ref(self, domain_id, parent_id=None): + def new_project_ref(self, domain_id=None, parent_id=None, is_domain=False): ref = self.new_ref() ref['domain_id'] = domain_id ref['parent_id'] = parent_id + ref['is_domain'] = is_domain return ref def new_user_ref(self, domain_id, project_id=None): @@ -362,9 +363,9 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase, if isinstance(expires, six.string_types): ref['expires_at'] = expires elif isinstance(expires, dict): - ref['expires_at'] = timeutils.strtime( - timeutils.utcnow() + datetime.timedelta(**expires), - fmt=TIME_FORMAT) + ref['expires_at'] = ( + timeutils.utcnow() + datetime.timedelta(**expires) + ).strftime(TIME_FORMAT) elif expires is None: pass else: @@ -396,6 +397,29 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase, return project + def get_unscoped_token(self): + """Convenience method so that we can test authenticated requests.""" + r = self.admin_request( + method='POST', + path='/v3/auth/tokens', + body={ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': self.user['name'], + 'password': self.user['password'], + 'domain': { + 'id': self.user['domain_id'] + } + } + } + } + } + }) + return r.headers.get('X-Subject-Token') + def get_scoped_token(self): """Convenience method so that we can test authenticated requests.""" r = self.admin_request( @@ -424,6 +448,34 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase, }) return r.headers.get('X-Subject-Token') + def get_domain_scoped_token(self): + """Convenience method for requesting domain scoped token.""" + r = self.admin_request( + method='POST', + path='/v3/auth/tokens', + body={ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': self.user['name'], + 'password': self.user['password'], + 'domain': { + 'id': self.user['domain_id'] + } + } + } + }, + 'scope': { + 'domain': { + 'id': self.domain['id'], + } + } + } + }) + return r.headers.get('X-Subject-Token') + def get_requested_token(self, auth): """Request the specific token we want.""" @@ -593,20 +645,6 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase, return entity - def assertDictContainsSubset(self, expected, actual): - """"Asserts if dictionary actual is a superset of expected. - - Tests whether the key/value pairs in dictionary actual are a superset - of those in expected. - - """ - for k, v in expected.iteritems(): - self.assertIn(k, actual) - if isinstance(v, dict): - self.assertDictContainsSubset(v, actual[k]) - else: - self.assertEqual(v, actual[k]) - # auth validation def assertValidISO8601ExtendedFormatDatetime(self, dt): @@ -752,7 +790,7 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase, self.assertValidCatalog(resp.json['catalog']) self.assertIn('links', resp.json) self.assertIsInstance(resp.json['links'], dict) - self.assertEqual(['self'], resp.json['links'].keys()) + self.assertEqual(['self'], list(resp.json['links'].keys())) self.assertEqual( 'http://localhost/v3/auth/catalog', resp.json['links']['self']) @@ -1258,6 +1296,42 @@ class AuthContextMiddlewareTestCase(RestfulTestCase): self.assertDictEqual(req.environ.get(authorization.AUTH_CONTEXT_ENV), {}) + def test_unscoped_token_auth_context(self): + unscoped_token = self.get_unscoped_token() + req = self._mock_request_object(unscoped_token) + application = None + middleware.AuthContextMiddleware(application).process_request(req) + for key in ['project_id', 'domain_id', 'domain_name']: + self.assertNotIn( + key, + req.environ.get(authorization.AUTH_CONTEXT_ENV)) + + def test_project_scoped_token_auth_context(self): + project_scoped_token = self.get_scoped_token() + req = self._mock_request_object(project_scoped_token) + application = None + middleware.AuthContextMiddleware(application).process_request(req) + self.assertEqual( + self.project['id'], + req.environ.get(authorization.AUTH_CONTEXT_ENV)['project_id']) + + def test_domain_scoped_token_auth_context(self): + # grant the domain role to user + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + + domain_scoped_token = self.get_domain_scoped_token() + req = self._mock_request_object(domain_scoped_token) + application = None + middleware.AuthContextMiddleware(application).process_request(req) + self.assertEqual( + self.domain['id'], + req.environ.get(authorization.AUTH_CONTEXT_ENV)['domain_id']) + self.assertEqual( + self.domain['name'], + req.environ.get(authorization.AUTH_CONTEXT_ENV)['domain_name']) + class JsonHomeTestMixin(object): """JSON Home test @@ -1281,3 +1355,88 @@ class JsonHomeTestMixin(object): for rel in self.JSON_HOME_DATA: self.assertThat(resp_data['resources'][rel], matchers.Equals(self.JSON_HOME_DATA[rel])) + + +class AssignmentTestMixin(object): + """To hold assignment helper functions.""" + + def build_role_assignment_query_url(self, effective=False, **filters): + """Build and return a role assignment query url with provided params. + + Available filters are: domain_id, project_id, user_id, group_id, + role_id and inherited_to_projects. + """ + + query_params = '?effective' if effective else '' + + for k, v in filters.items(): + query_params += '?' if not query_params else '&' + + if k == 'inherited_to_projects': + query_params += 'scope.OS-INHERIT:inherited_to=projects' + else: + if k in ['domain_id', 'project_id']: + query_params += 'scope.' + elif k not in ['user_id', 'group_id', 'role_id']: + raise ValueError( + 'Invalid key \'%s\' in provided filters.' % k) + + query_params += '%s=%s' % (k.replace('_', '.'), v) + + return '/role_assignments%s' % query_params + + def build_role_assignment_link(self, **attribs): + """Build and return a role assignment link with provided attributes. + + Provided attributes are expected to contain: domain_id or project_id, + user_id or group_id, role_id and, optionally, inherited_to_projects. + """ + + if attribs.get('domain_id'): + link = '/domains/' + attribs['domain_id'] + else: + link = '/projects/' + attribs['project_id'] + + if attribs.get('user_id'): + link += '/users/' + attribs['user_id'] + else: + link += '/groups/' + attribs['group_id'] + + link += '/roles/' + attribs['role_id'] + + if attribs.get('inherited_to_projects'): + return '/OS-INHERIT%s/inherited_to_projects' % link + + return link + + def build_role_assignment_entity(self, link=None, **attribs): + """Build and return a role assignment entity with provided attributes. + + Provided attributes are expected to contain: domain_id or project_id, + user_id or group_id, role_id and, optionally, inherited_to_projects. + """ + + entity = {'links': {'assignment': ( + link or self.build_role_assignment_link(**attribs))}} + + if attribs.get('domain_id'): + entity['scope'] = {'domain': {'id': attribs['domain_id']}} + else: + entity['scope'] = {'project': {'id': attribs['project_id']}} + + if attribs.get('user_id'): + entity['user'] = {'id': attribs['user_id']} + + if attribs.get('group_id'): + entity['links']['membership'] = ('/groups/%s/users/%s' % + (attribs['group_id'], + attribs['user_id'])) + else: + entity['group'] = {'id': attribs['group_id']} + + entity['role'] = {'id': attribs['role_id']} + + if attribs.get('inherited_to_projects'): + entity['scope']['OS-INHERIT:inherited_to'] = 'projects' + + return entity diff --git a/keystone-moon/keystone/tests/unit/test_v3_assignment.py b/keystone-moon/keystone/tests/unit/test_v3_assignment.py index add14bfb..03e5d30b 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_assignment.py +++ b/keystone-moon/keystone/tests/unit/test_v3_assignment.py @@ -11,107 +11,23 @@ # under the License. import random -import six import uuid from oslo_config import cfg +from six.moves import range from keystone.common import controller from keystone import exception from keystone.tests import unit as tests from keystone.tests.unit import test_v3 +from keystone.tests.unit import utils CONF = cfg.CONF -def _build_role_assignment_query_url(effective=False, **filters): - '''Build and return a role assignment query url with provided params. - - Available filters are: domain_id, project_id, user_id, group_id, role_id - and inherited_to_projects. - - ''' - - query_params = '?effective' if effective else '' - - for k, v in six.iteritems(filters): - query_params += '?' if not query_params else '&' - - if k == 'inherited_to_projects': - query_params += 'scope.OS-INHERIT:inherited_to=projects' - else: - if k in ['domain_id', 'project_id']: - query_params += 'scope.' - elif k not in ['user_id', 'group_id', 'role_id']: - raise ValueError('Invalid key \'%s\' in provided filters.' % k) - - query_params += '%s=%s' % (k.replace('_', '.'), v) - - return '/role_assignments%s' % query_params - - -def _build_role_assignment_link(**attribs): - """Build and return a role assignment link with provided attributes. - - Provided attributes are expected to contain: domain_id or project_id, - user_id or group_id, role_id and, optionally, inherited_to_projects. - - """ - - if attribs.get('domain_id'): - link = '/domains/' + attribs['domain_id'] - else: - link = '/projects/' + attribs['project_id'] - - if attribs.get('user_id'): - link += '/users/' + attribs['user_id'] - else: - link += '/groups/' + attribs['group_id'] - - link += '/roles/' + attribs['role_id'] - - if attribs.get('inherited_to_projects'): - return '/OS-INHERIT%s/inherited_to_projects' % link - - return link - - -def _build_role_assignment_entity(link=None, **attribs): - """Build and return a role assignment entity with provided attributes. - - Provided attributes are expected to contain: domain_id or project_id, - user_id or group_id, role_id and, optionally, inherited_to_projects. - - """ - - entity = {'links': {'assignment': ( - link or _build_role_assignment_link(**attribs))}} - - if attribs.get('domain_id'): - entity['scope'] = {'domain': {'id': attribs['domain_id']}} - else: - entity['scope'] = {'project': {'id': attribs['project_id']}} - - if attribs.get('user_id'): - entity['user'] = {'id': attribs['user_id']} - - if attribs.get('group_id'): - entity['links']['membership'] = ('/groups/%s/users/%s' % - (attribs['group_id'], - attribs['user_id'])) - else: - entity['group'] = {'id': attribs['group_id']} - - entity['role'] = {'id': attribs['role_id']} - - if attribs.get('inherited_to_projects'): - entity['scope']['OS-INHERIT:inherited_to'] = 'projects' - - return entity - - -class AssignmentTestCase(test_v3.RestfulTestCase): +class AssignmentTestCase(test_v3.RestfulTestCase, + test_v3.AssignmentTestMixin): """Test domains, projects, roles and role assignments.""" def setUp(self): @@ -205,8 +121,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase): self.assignment_api.add_user_to_project(self.project2['id'], self.user2['id']) - # First check a user in that domain can authenticate, via - # Both v2 and v3 + # First check a user in that domain can authenticate. The v2 user + # cannot authenticate because they exist outside the default domain. body = { 'auth': { 'passwordCredentials': { @@ -216,7 +132,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase): 'tenantId': self.project2['id'] } } - self.admin_request(path='/v2.0/tokens', method='POST', body=body) + self.admin_request( + path='/v2.0/tokens', method='POST', body=body, expected_status=401) auth_data = self.build_authentication_request( user_id=self.user2['id'], @@ -507,26 +424,26 @@ class AssignmentTestCase(test_v3.RestfulTestCase): for domain in create_domains(): self.assertRaises( - AssertionError, self.assignment_api.create_domain, + AssertionError, self.resource_api.create_domain, domain['id'], domain) self.assertRaises( - AssertionError, self.assignment_api.update_domain, + AssertionError, self.resource_api.update_domain, domain['id'], domain) self.assertRaises( - exception.DomainNotFound, self.assignment_api.delete_domain, + exception.DomainNotFound, self.resource_api.delete_domain, domain['id']) # swap 'name' with 'id' and try again, expecting the request to # gracefully fail domain['id'], domain['name'] = domain['name'], domain['id'] self.assertRaises( - AssertionError, self.assignment_api.create_domain, + AssertionError, self.resource_api.create_domain, domain['id'], domain) self.assertRaises( - AssertionError, self.assignment_api.update_domain, + AssertionError, self.resource_api.update_domain, domain['id'], domain) self.assertRaises( - exception.DomainNotFound, self.assignment_api.delete_domain, + exception.DomainNotFound, self.resource_api.delete_domain, domain['id']) def test_forbid_operations_on_defined_federated_domain(self): @@ -542,47 +459,13 @@ class AssignmentTestCase(test_v3.RestfulTestCase): domain = self.new_domain_ref() domain['name'] = non_default_name self.assertRaises(AssertionError, - self.assignment_api.create_domain, + self.resource_api.create_domain, domain['id'], domain) self.assertRaises(exception.DomainNotFound, - self.assignment_api.delete_domain, + self.resource_api.delete_domain, domain['id']) self.assertRaises(AssertionError, - self.assignment_api.update_domain, - domain['id'], domain) - - def test_set_federated_domain_when_config_empty(self): - """Make sure we are operable even if config value is not properly - set. - - This includes operations like create, update, delete. - - """ - federated_name = 'Federated' - self.config_fixture.config(group='federation', - federated_domain_name='') - domain = self.new_domain_ref() - domain['id'] = federated_name - self.assertRaises(AssertionError, - self.assignment_api.create_domain, - domain['id'], domain) - self.assertRaises(exception.DomainNotFound, - self.assignment_api.delete_domain, - domain['id']) - self.assertRaises(AssertionError, - self.assignment_api.update_domain, - domain['id'], domain) - - # swap id with name - domain['id'], domain['name'] = domain['name'], domain['id'] - self.assertRaises(AssertionError, - self.assignment_api.create_domain, - domain['id'], domain) - self.assertRaises(exception.DomainNotFound, - self.assignment_api.delete_domain, - domain['id']) - self.assertRaises(AssertionError, - self.assignment_api.update_domain, + self.resource_api.update_domain, domain['id'], domain) # Project CRUD tests @@ -606,8 +489,71 @@ class AssignmentTestCase(test_v3.RestfulTestCase): """Call ``POST /projects``.""" self.post('/projects', body={'project': {}}, expected_status=400) + def test_create_project_invalid_domain_id(self): + """Call ``POST /projects``.""" + ref = self.new_project_ref(domain_id=uuid.uuid4().hex) + self.post('/projects', body={'project': ref}, expected_status=400) + + def test_create_project_is_domain_not_allowed(self): + """Call ``POST /projects``. + + Setting is_domain=True is not supported yet and should raise + NotImplemented. + + """ + ref = self.new_project_ref(domain_id=self.domain_id, is_domain=True) + self.post('/projects', + body={'project': ref}, + expected_status=501) + + @utils.wip('waiting for projects acting as domains implementation') + def test_create_project_without_parent_id_and_without_domain_id(self): + """Call ``POST /projects``.""" + + # Grant a domain role for the user + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domain_id, + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + self.put(member_url) + + # Create an authentication request for a domain scoped token + auth = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain_id) + + # Without domain_id and parent_id, the domain_id should be + # normalized to the domain on the token, when using a domain + # scoped token. + ref = self.new_project_ref() + r = self.post( + '/projects', + auth=auth, + body={'project': ref}) + ref['domain_id'] = self.domain['id'] + self.assertValidProjectResponse(r, ref) + + @utils.wip('waiting for projects acting as domains implementation') + def test_create_project_with_parent_id_and_no_domain_id(self): + """Call ``POST /projects``.""" + # With only the parent_id, the domain_id should be + # normalized to the parent's domain_id + ref_child = self.new_project_ref(parent_id=self.project['id']) + + r = self.post( + '/projects', + body={'project': ref_child}) + self.assertEqual(r.result['project']['domain_id'], + self.project['domain_id']) + ref_child['domain_id'] = self.domain['id'] + self.assertValidProjectResponse(r, ref_child) + def _create_projects_hierarchy(self, hierarchy_size=1): - """Creates a project hierarchy with specified size. + """Creates a single-branched project hierarchy with the specified size. :param hierarchy_size: the desired hierarchy size, default is 1 - a project with one child. @@ -615,9 +561,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase): :returns projects: a list of the projects in the created hierarchy. """ - resp = self.get( - '/projects/%(project_id)s' % { - 'project_id': self.project_id}) + new_ref = self.new_project_ref(domain_id=self.domain_id) + resp = self.post('/projects', body={'project': new_ref}) projects = [resp.result] @@ -633,6 +578,58 @@ class AssignmentTestCase(test_v3.RestfulTestCase): return projects + def test_list_projects_filtering_by_parent_id(self): + """Call ``GET /projects?parent_id={project_id}``.""" + projects = self._create_projects_hierarchy(hierarchy_size=2) + + # Add another child to projects[1] - it will be projects[3] + new_ref = self.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[1]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + + projects.append(resp.result) + + # Query for projects[0] immediate children - it will + # be only projects[1] + r = self.get( + '/projects?parent_id=%(project_id)s' % { + 'project_id': projects[0]['project']['id']}) + self.assertValidProjectListResponse(r) + + projects_result = r.result['projects'] + expected_list = [projects[1]['project']] + + # projects[0] has projects[1] as child + self.assertEqual(expected_list, projects_result) + + # Query for projects[1] immediate children - it will + # be projects[2] and projects[3] + r = self.get( + '/projects?parent_id=%(project_id)s' % { + 'project_id': projects[1]['project']['id']}) + self.assertValidProjectListResponse(r) + + projects_result = r.result['projects'] + expected_list = [projects[2]['project'], projects[3]['project']] + + # projects[1] has projects[2] and projects[3] as children + self.assertEqual(expected_list, projects_result) + + # Query for projects[2] immediate children - it will be an empty list + r = self.get( + '/projects?parent_id=%(project_id)s' % { + 'project_id': projects[2]['project']['id']}) + self.assertValidProjectListResponse(r) + + projects_result = r.result['projects'] + expected_list = [] + + # projects[2] has no child, projects_result must be an empty list + self.assertEqual(expected_list, projects_result) + def test_create_hierarchical_project(self): """Call ``POST /projects``.""" self._create_projects_hierarchy() @@ -644,6 +641,22 @@ class AssignmentTestCase(test_v3.RestfulTestCase): 'project_id': self.project_id}) self.assertValidProjectResponse(r, self.project) + def test_get_project_with_parents_as_list_with_invalid_id(self): + """Call ``GET /projects/{project_id}?parents_as_list``.""" + self.get('/projects/%(project_id)s?parents_as_list' % { + 'project_id': None}, expected_status=404) + + self.get('/projects/%(project_id)s?parents_as_list' % { + 'project_id': uuid.uuid4().hex}, expected_status=404) + + def test_get_project_with_subtree_as_list_with_invalid_id(self): + """Call ``GET /projects/{project_id}?subtree_as_list``.""" + self.get('/projects/%(project_id)s?subtree_as_list' % { + 'project_id': None}, expected_status=404) + + self.get('/projects/%(project_id)s?subtree_as_list' % { + 'project_id': uuid.uuid4().hex}, expected_status=404) + def test_get_project_with_parents_as_ids(self): """Call ``GET /projects/{project_id}?parents_as_ids``.""" projects = self._create_projects_hierarchy(hierarchy_size=2) @@ -683,18 +696,66 @@ class AssignmentTestCase(test_v3.RestfulTestCase): # projects[0] has no parents, parents_as_ids must be None self.assertIsNone(parents_as_ids) - def test_get_project_with_parents_as_list(self): - """Call ``GET /projects/{project_id}?parents_as_list``.""" - projects = self._create_projects_hierarchy(hierarchy_size=2) + def test_get_project_with_parents_as_list_with_full_access(self): + """``GET /projects/{project_id}?parents_as_list`` with full access. - r = self.get( - '/projects/%(project_id)s?parents_as_list' % { - 'project_id': projects[1]['project']['id']}) + Test plan: + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on each one of those projects; + - Check that calling parents_as_list on 'subproject' returns both + 'project' and 'parent'. + + """ + + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on all the created projects + for proj in (parent, project, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + + # Make the API call + r = self.get('/projects/%(project_id)s?parents_as_list' % + {'project_id': subproject['project']['id']}) + self.assertValidProjectResponse(r, subproject['project']) + + # Assert only 'project' and 'parent' are in the parents list + self.assertIn(project, r.result['project']['parents']) + self.assertIn(parent, r.result['project']['parents']) + self.assertEqual(2, len(r.result['project']['parents'])) + + def test_get_project_with_parents_as_list_with_partial_access(self): + """``GET /projects/{project_id}?parents_as_list`` with partial access. + + Test plan: + + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on 'parent' and 'subproject'; + - Check that calling parents_as_list on 'subproject' only returns + 'parent'. + + """ + + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on parent and subproject + for proj in (parent, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + + # Make the API call + r = self.get('/projects/%(project_id)s?parents_as_list' % + {'project_id': subproject['project']['id']}) + self.assertValidProjectResponse(r, subproject['project']) + + # Assert only 'parent' is in the parents list + self.assertIn(parent, r.result['project']['parents']) self.assertEqual(1, len(r.result['project']['parents'])) - self.assertValidProjectResponse(r, projects[1]['project']) - self.assertIn(projects[0], r.result['project']['parents']) - self.assertNotIn(projects[2], r.result['project']['parents']) def test_get_project_with_parents_as_list_and_parents_as_ids(self): """Call ``GET /projects/{project_id}?parents_as_list&parents_as_ids``. @@ -798,18 +859,65 @@ class AssignmentTestCase(test_v3.RestfulTestCase): # projects[3] has no subtree, subtree_as_ids must be None self.assertIsNone(subtree_as_ids) - def test_get_project_with_subtree_as_list(self): - """Call ``GET /projects/{project_id}?subtree_as_list``.""" - projects = self._create_projects_hierarchy(hierarchy_size=2) + def test_get_project_with_subtree_as_list_with_full_access(self): + """``GET /projects/{project_id}?subtree_as_list`` with full access. - r = self.get( - '/projects/%(project_id)s?subtree_as_list' % { - 'project_id': projects[1]['project']['id']}) + Test plan: + + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on each one of those projects; + - Check that calling subtree_as_list on 'parent' returns both 'parent' + and 'subproject'. + + """ + + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on all the created projects + for proj in (parent, project, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + # Make the API call + r = self.get('/projects/%(project_id)s?subtree_as_list' % + {'project_id': parent['project']['id']}) + self.assertValidProjectResponse(r, parent['project']) + + # Assert only 'project' and 'subproject' are in the subtree + self.assertIn(project, r.result['project']['subtree']) + self.assertIn(subproject, r.result['project']['subtree']) + self.assertEqual(2, len(r.result['project']['subtree'])) + + def test_get_project_with_subtree_as_list_with_partial_access(self): + """``GET /projects/{project_id}?subtree_as_list`` with partial access. + + Test plan: + + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on 'parent' and 'subproject'; + - Check that calling subtree_as_list on 'parent' returns 'subproject'. + + """ + + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on parent and subproject + for proj in (parent, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + + # Make the API call + r = self.get('/projects/%(project_id)s?subtree_as_list' % + {'project_id': parent['project']['id']}) + self.assertValidProjectResponse(r, parent['project']) + + # Assert only 'subproject' is in the subtree + self.assertIn(subproject, r.result['project']['subtree']) self.assertEqual(1, len(r.result['project']['subtree'])) - self.assertValidProjectResponse(r, projects[1]['project']) - self.assertNotIn(projects[0], r.result['project']['subtree']) - self.assertIn(projects[2], r.result['project']['subtree']) def test_get_project_with_subtree_as_list_and_subtree_as_ids(self): """Call ``GET /projects/{project_id}?subtree_as_list&subtree_as_ids``. @@ -859,6 +967,22 @@ class AssignmentTestCase(test_v3.RestfulTestCase): body={'project': leaf_project}, expected_status=403) + def test_update_project_is_domain_not_allowed(self): + """Call ``PATCH /projects/{project_id}`` with is_domain. + + The is_domain flag is immutable. + """ + project = self.new_project_ref(domain_id=self.domain['id']) + resp = self.post('/projects', + body={'project': project}) + self.assertFalse(resp.result['project']['is_domain']) + + project['is_domain'] = True + self.patch('/projects/%(project_id)s' % { + 'project_id': resp.result['project']['id']}, + body={'project': project}, + expected_status=400) + def test_disable_leaf_project(self): """Call ``PATCH /projects/{project_id}``.""" projects = self._create_projects_hierarchy() @@ -920,10 +1044,10 @@ class AssignmentTestCase(test_v3.RestfulTestCase): def test_delete_not_leaf_project(self): """Call ``DELETE /projects/{project_id}``.""" - self._create_projects_hierarchy() + projects = self._create_projects_hierarchy() self.delete( '/projects/%(project_id)s' % { - 'project_id': self.project_id}, + 'project_id': projects[0]['project']['id']}, expected_status=403) # Role CRUD tests @@ -967,6 +1091,19 @@ class AssignmentTestCase(test_v3.RestfulTestCase): self.delete('/roles/%(role_id)s' % { 'role_id': self.role_id}) + def test_create_member_role(self): + """Call ``POST /roles``.""" + # specify only the name on creation + ref = self.new_role_ref() + ref['name'] = CONF.member_role_name + r = self.post( + '/roles', + body={'role': ref}) + self.assertValidRoleResponse(r, ref) + + # but the ID should be set as defined in CONF + self.assertEqual(CONF.member_role_id, r.json['role']['id']) + # Role Grants tests def test_crud_user_project_role_grants(self): @@ -1252,9 +1389,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase): # Now add one of each of the four types of assignment, making sure # that we get them all back. - gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, - group_id=self.group_id, - role_id=self.role_id) + gd_entity = self.build_role_assignment_entity(domain_id=self.domain_id, + group_id=self.group_id, + role_id=self.role_id) self.put(gd_entity['links']['assignment']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse( @@ -1263,9 +1400,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase): resource_url=collection_url) self.assertRoleAssignmentInListResponse(r, gd_entity) - ud_entity = _build_role_assignment_entity(domain_id=self.domain_id, - user_id=self.user1['id'], - role_id=self.role_id) + ud_entity = self.build_role_assignment_entity(domain_id=self.domain_id, + user_id=self.user1['id'], + role_id=self.role_id) self.put(ud_entity['links']['assignment']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse( @@ -1274,9 +1411,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase): resource_url=collection_url) self.assertRoleAssignmentInListResponse(r, ud_entity) - gp_entity = _build_role_assignment_entity(project_id=self.project_id, - group_id=self.group_id, - role_id=self.role_id) + gp_entity = self.build_role_assignment_entity( + project_id=self.project_id, group_id=self.group_id, + role_id=self.role_id) self.put(gp_entity['links']['assignment']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse( @@ -1285,9 +1422,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase): resource_url=collection_url) self.assertRoleAssignmentInListResponse(r, gp_entity) - up_entity = _build_role_assignment_entity(project_id=self.project_id, - user_id=self.user1['id'], - role_id=self.role_id) + up_entity = self.build_role_assignment_entity( + project_id=self.project_id, user_id=self.user1['id'], + role_id=self.role_id) self.put(up_entity['links']['assignment']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse( @@ -1346,9 +1483,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase): resource_url=collection_url) existing_assignments = len(r.result.get('role_assignments')) - gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, - group_id=self.group_id, - role_id=self.role_id) + gd_entity = self.build_role_assignment_entity(domain_id=self.domain_id, + group_id=self.group_id, + role_id=self.role_id) self.put(gd_entity['links']['assignment']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse( @@ -1366,11 +1503,11 @@ class AssignmentTestCase(test_v3.RestfulTestCase): r, expected_length=existing_assignments + 2, resource_url=collection_url) - ud_entity = _build_role_assignment_entity( + ud_entity = self.build_role_assignment_entity( link=gd_entity['links']['assignment'], domain_id=self.domain_id, user_id=self.user1['id'], role_id=self.role_id) self.assertRoleAssignmentInListResponse(r, ud_entity) - ud_entity = _build_role_assignment_entity( + ud_entity = self.build_role_assignment_entity( link=gd_entity['links']['assignment'], domain_id=self.domain_id, user_id=self.user2['id'], role_id=self.role_id) self.assertRoleAssignmentInListResponse(r, ud_entity) @@ -1420,9 +1557,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase): resource_url=collection_url) existing_assignments = len(r.result.get('role_assignments')) - gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, - group_id=self.group_id, - role_id=self.role_id) + gd_entity = self.build_role_assignment_entity(domain_id=self.domain_id, + group_id=self.group_id, + role_id=self.role_id) self.put(gd_entity['links']['assignment']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse( @@ -1516,22 +1653,22 @@ class AssignmentTestCase(test_v3.RestfulTestCase): # Now add one of each of the four types of assignment - gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, - group_id=self.group1['id'], - role_id=self.role1['id']) + gd_entity = self.build_role_assignment_entity( + domain_id=self.domain_id, group_id=self.group1['id'], + role_id=self.role1['id']) self.put(gd_entity['links']['assignment']) - ud_entity = _build_role_assignment_entity(domain_id=self.domain_id, - user_id=self.user1['id'], - role_id=self.role2['id']) + ud_entity = self.build_role_assignment_entity(domain_id=self.domain_id, + user_id=self.user1['id'], + role_id=self.role2['id']) self.put(ud_entity['links']['assignment']) - gp_entity = _build_role_assignment_entity( + gp_entity = self.build_role_assignment_entity( project_id=self.project1['id'], group_id=self.group1['id'], role_id=self.role1['id']) self.put(gp_entity['links']['assignment']) - up_entity = _build_role_assignment_entity( + up_entity = self.build_role_assignment_entity( project_id=self.project1['id'], user_id=self.user1['id'], role_id=self.role2['id']) self.put(up_entity['links']['assignment']) @@ -1607,17 +1744,17 @@ class AssignmentTestCase(test_v3.RestfulTestCase): self.assertRoleAssignmentInListResponse(r, up_entity) self.assertRoleAssignmentInListResponse(r, ud_entity) # ...and the two via group membership... - gp1_link = _build_role_assignment_link(project_id=self.project1['id'], - group_id=self.group1['id'], - role_id=self.role1['id']) - gd1_link = _build_role_assignment_link(domain_id=self.domain_id, - group_id=self.group1['id'], - role_id=self.role1['id']) - - up1_entity = _build_role_assignment_entity( + gp1_link = self.build_role_assignment_link( + project_id=self.project1['id'], group_id=self.group1['id'], + role_id=self.role1['id']) + gd1_link = self.build_role_assignment_link(domain_id=self.domain_id, + group_id=self.group1['id'], + role_id=self.role1['id']) + + up1_entity = self.build_role_assignment_entity( link=gp1_link, project_id=self.project1['id'], user_id=self.user1['id'], role_id=self.role1['id']) - ud1_entity = _build_role_assignment_entity( + ud1_entity = self.build_role_assignment_entity( link=gd1_link, domain_id=self.domain_id, user_id=self.user1['id'], role_id=self.role1['id']) self.assertRoleAssignmentInListResponse(r, up1_entity) @@ -1641,7 +1778,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase): self.assertRoleAssignmentInListResponse(r, up1_entity) -class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase): +class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, + test_v3.AssignmentTestMixin): """Base class for testing /v3/role_assignments API behavior.""" MAX_HIERARCHY_BREADTH = 3 @@ -1665,8 +1803,8 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase): for i in range(breadth): subprojects.append(self.new_project_ref( domain_id=self.domain_id, parent_id=parent_id)) - self.assignment_api.create_project(subprojects[-1]['id'], - subprojects[-1]) + self.resource_api.create_project(subprojects[-1]['id'], + subprojects[-1]) new_parent = subprojects[random.randint(0, breadth - 1)] create_project_hierarchy(new_parent['id'], depth - 1) @@ -1676,12 +1814,12 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase): # Create a domain self.domain = self.new_domain_ref() self.domain_id = self.domain['id'] - self.assignment_api.create_domain(self.domain_id, self.domain) + self.resource_api.create_domain(self.domain_id, self.domain) # Create a project hierarchy self.project = self.new_project_ref(domain_id=self.domain_id) self.project_id = self.project['id'] - self.assignment_api.create_project(self.project_id, self.project) + self.resource_api.create_project(self.project_id, self.project) # Create a random project hierarchy create_project_hierarchy(self.project_id, @@ -1714,7 +1852,7 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase): # Create a role self.role = self.new_role_ref() self.role_id = self.role['id'] - self.assignment_api.create_role(self.role_id, self.role) + self.role_api.create_role(self.role_id, self.role) # Set default user and group to be used on tests self.default_user_id = self.user_ids[0] @@ -1748,7 +1886,7 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase): :returns: role assignments query URL. """ - return _build_role_assignment_query_url(**filters) + return self.build_role_assignment_query_url(**filters) class RoleAssignmentFailureTestCase(RoleAssignmentBaseTestCase): @@ -1869,7 +2007,7 @@ class RoleAssignmentDirectTestCase(RoleAssignmentBaseTestCase): :returns: the list of the expected role assignments. """ - return [_build_role_assignment_entity(**filters)] + return [self.build_role_assignment_entity(**filters)] # Test cases below call the generic test method, providing different filter # combinations. Filters are provided as specified in the method name, after @@ -1980,8 +2118,8 @@ class RoleAssignmentEffectiveTestCase(RoleAssignmentInheritedTestCase): query_filters.pop('domain_id', None) query_filters.pop('project_id', None) - return _build_role_assignment_query_url(effective=True, - **query_filters) + return self.build_role_assignment_query_url(effective=True, + **query_filters) def _list_expected_role_assignments(self, **filters): """Given the filters, it returns expected direct role assignments. @@ -1995,7 +2133,7 @@ class RoleAssignmentEffectiveTestCase(RoleAssignmentInheritedTestCase): """ # Get assignment link, to be put on 'links': {'assignment': link} - assignment_link = _build_role_assignment_link(**filters) + assignment_link = self.build_role_assignment_link(**filters) # Expand group membership user_ids = [None] @@ -2010,11 +2148,11 @@ class RoleAssignmentEffectiveTestCase(RoleAssignmentInheritedTestCase): project_ids = [None] if filters.get('domain_id'): project_ids = [project['id'] for project in - self.assignment_api.list_projects_in_domain( + self.resource_api.list_projects_in_domain( filters.pop('domain_id'))] else: project_ids = [project['id'] for project in - self.assignment_api.list_projects_in_subtree( + self.resource_api.list_projects_in_subtree( self.project_id)] # Compute expected role assignments @@ -2023,13 +2161,14 @@ class RoleAssignmentEffectiveTestCase(RoleAssignmentInheritedTestCase): filters['project_id'] = project_id for user_id in user_ids: filters['user_id'] = user_id - assignments.append(_build_role_assignment_entity( + assignments.append(self.build_role_assignment_entity( link=assignment_link, **filters)) return assignments -class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): +class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, + test_v3.AssignmentTestMixin): """Test inheritance crud and its effects.""" def config_overrides(self): @@ -2058,7 +2197,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.v3_authenticate_token(project_auth_data, expected_status=401) # Grant non-inherited role for user on domain - non_inher_ud_link = _build_role_assignment_link( + non_inher_ud_link = self.build_role_assignment_link( domain_id=self.domain_id, user_id=user['id'], role_id=self.role_id) self.put(non_inher_ud_link) @@ -2071,7 +2210,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.role_api.create_role(inherited_role['id'], inherited_role) # Grant inherited role for user on domain - inher_ud_link = _build_role_assignment_link( + inher_ud_link = self.build_role_assignment_link( domain_id=self.domain_id, user_id=user['id'], role_id=inherited_role['id'], inherited_to_projects=True) self.put(inher_ud_link) @@ -2120,7 +2259,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.v3_authenticate_token(project_auth_data, expected_status=401) # Grant non-inherited role for user on domain - non_inher_gd_link = _build_role_assignment_link( + non_inher_gd_link = self.build_role_assignment_link( domain_id=self.domain_id, user_id=user['id'], role_id=self.role_id) self.put(non_inher_gd_link) @@ -2133,7 +2272,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.role_api.create_role(inherited_role['id'], inherited_role) # Grant inherited role for user on domain - inher_gd_link = _build_role_assignment_link( + inher_gd_link = self.build_role_assignment_link( domain_id=self.domain_id, user_id=user['id'], role_id=inherited_role['id'], inherited_to_projects=True) self.put(inher_gd_link) @@ -2155,6 +2294,48 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): # Check the user cannot get a domain token anymore self.v3_authenticate_token(domain_auth_data, expected_status=401) + def _test_crud_inherited_and_direct_assignment_on_target(self, target_url): + # Create a new role to avoid assignments loaded from sample data + role = self.new_role_ref() + self.role_api.create_role(role['id'], role) + + # Define URLs + direct_url = '%s/users/%s/roles/%s' % ( + target_url, self.user_id, role['id']) + inherited_url = '/OS-INHERIT/%s/inherited_to_projects' % direct_url + + # Create the direct assignment + self.put(direct_url) + # Check the direct assignment exists, but the inherited one does not + self.head(direct_url) + self.head(inherited_url, expected_status=404) + + # Now add the inherited assignment + self.put(inherited_url) + # Check both the direct and inherited assignment exist + self.head(direct_url) + self.head(inherited_url) + + # Delete indirect assignment + self.delete(inherited_url) + # Check the direct assignment exists, but the inherited one does not + self.head(direct_url) + self.head(inherited_url, expected_status=404) + + # Now delete the inherited assignment + self.delete(direct_url) + # Check that none of them exist + self.head(direct_url, expected_status=404) + self.head(inherited_url, expected_status=404) + + def test_crud_inherited_and_direct_assignment_on_domains(self): + self._test_crud_inherited_and_direct_assignment_on_target( + '/domains/%s' % self.domain_id) + + def test_crud_inherited_and_direct_assignment_on_projects(self): + self._test_crud_inherited_and_direct_assignment_on_target( + '/projects/%s' % self.project_id) + def test_crud_user_inherited_domain_role_grants(self): role_list = [] for _ in range(2): @@ -2260,7 +2441,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.assertValidRoleAssignmentListResponse(r, expected_length=1, resource_url=collection_url) - ud_entity = _build_role_assignment_entity( + ud_entity = self.build_role_assignment_entity( domain_id=domain['id'], user_id=user1['id'], role_id=role_list[3]['id'], inherited_to_projects=True) self.assertRoleAssignmentInListResponse(r, ud_entity) @@ -2279,14 +2460,13 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): resource_url=collection_url) # An effective role for an inherited role will be a project # entity, with a domain link to the inherited assignment - ud_url = _build_role_assignment_link( + ud_url = self.build_role_assignment_link( domain_id=domain['id'], user_id=user1['id'], role_id=role_list[3]['id'], inherited_to_projects=True) - up_entity = _build_role_assignment_entity(link=ud_url, - project_id=project1['id'], - user_id=user1['id'], - role_id=role_list[3]['id'], - inherited_to_projects=True) + up_entity = self.build_role_assignment_entity( + link=ud_url, project_id=project1['id'], + user_id=user1['id'], role_id=role_list[3]['id'], + inherited_to_projects=True) self.assertRoleAssignmentInListResponse(r, up_entity) def test_list_role_assignments_for_disabled_inheritance_extension(self): @@ -2360,14 +2540,13 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): expected_length=3, resource_url=collection_url) - ud_url = _build_role_assignment_link( + ud_url = self.build_role_assignment_link( domain_id=domain['id'], user_id=user1['id'], role_id=role_list[3]['id'], inherited_to_projects=True) - up_entity = _build_role_assignment_entity(link=ud_url, - project_id=project1['id'], - user_id=user1['id'], - role_id=role_list[3]['id'], - inherited_to_projects=True) + up_entity = self.build_role_assignment_entity( + link=ud_url, project_id=project1['id'], + user_id=user1['id'], role_id=role_list[3]['id'], + inherited_to_projects=True) self.assertRoleAssignmentInListResponse(r, up_entity) @@ -2463,7 +2642,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.assertValidRoleAssignmentListResponse(r, expected_length=1, resource_url=collection_url) - gd_entity = _build_role_assignment_entity( + gd_entity = self.build_role_assignment_entity( domain_id=domain['id'], group_id=group1['id'], role_id=role_list[3]['id'], inherited_to_projects=True) self.assertRoleAssignmentInListResponse(r, gd_entity) @@ -2482,7 +2661,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): resource_url=collection_url) # An effective role for an inherited role will be a project # entity, with a domain link to the inherited assignment - up_entity = _build_role_assignment_entity( + up_entity = self.build_role_assignment_entity( link=gd_entity['links']['assignment'], project_id=project1['id'], user_id=user1['id'], role_id=role_list[3]['id'], inherited_to_projects=True) @@ -2573,10 +2752,10 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.assertValidRoleAssignmentListResponse(r, expected_length=2, resource_url=collection_url) - ud_entity = _build_role_assignment_entity( + ud_entity = self.build_role_assignment_entity( domain_id=domain['id'], user_id=user1['id'], role_id=role_list[3]['id'], inherited_to_projects=True) - gd_entity = _build_role_assignment_entity( + gd_entity = self.build_role_assignment_entity( domain_id=domain['id'], group_id=group1['id'], role_id=role_list[4]['id'], inherited_to_projects=True) self.assertRoleAssignmentInListResponse(r, ud_entity) @@ -2626,7 +2805,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.v3_authenticate_token(leaf_project_auth_data, expected_status=401) # Grant non-inherited role for user on leaf project - non_inher_up_link = _build_role_assignment_link( + non_inher_up_link = self.build_role_assignment_link( project_id=leaf_id, user_id=self.user['id'], role_id=non_inherited_role_id) self.put(non_inher_up_link) @@ -2636,7 +2815,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.v3_authenticate_token(leaf_project_auth_data) # Grant inherited role for user on root project - inher_up_link = _build_role_assignment_link( + inher_up_link = self.build_role_assignment_link( project_id=root_id, user_id=self.user['id'], role_id=inherited_role_id, inherited_to_projects=True) self.put(inher_up_link) @@ -2683,7 +2862,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.v3_authenticate_token(leaf_project_auth_data, expected_status=401) # Grant non-inherited role for group on leaf project - non_inher_gp_link = _build_role_assignment_link( + non_inher_gp_link = self.build_role_assignment_link( project_id=leaf_id, group_id=group['id'], role_id=non_inherited_role_id) self.put(non_inher_gp_link) @@ -2693,7 +2872,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.v3_authenticate_token(leaf_project_auth_data) # Grant inherited role for group on root project - inher_gp_link = _build_role_assignment_link( + inher_gp_link = self.build_role_assignment_link( project_id=root_id, group_id=group['id'], role_id=inherited_role_id, inherited_to_projects=True) self.put(inher_gp_link) @@ -2732,13 +2911,13 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self._setup_hierarchical_projects_scenario()) # Grant non-inherited role - non_inher_up_entity = _build_role_assignment_entity( + non_inher_up_entity = self.build_role_assignment_entity( project_id=root_id, user_id=self.user['id'], role_id=non_inherited_role_id) self.put(non_inher_up_entity['links']['assignment']) # Grant inherited role - inher_up_entity = _build_role_assignment_entity( + inher_up_entity = self.build_role_assignment_entity( project_id=root_id, user_id=self.user['id'], role_id=inherited_role_id, inherited_to_projects=True) self.put(inher_up_entity['links']['assignment']) @@ -2756,7 +2935,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.assertRoleAssignmentInListResponse(r, inher_up_entity) # Assert that the user does not have non-inherited role on leaf project - non_inher_up_entity = _build_role_assignment_entity( + non_inher_up_entity = self.build_role_assignment_entity( project_id=leaf_id, user_id=self.user['id'], role_id=non_inherited_role_id) self.assertRoleAssignmentNotInListResponse(r, non_inher_up_entity) @@ -2784,13 +2963,13 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self._setup_hierarchical_projects_scenario()) # Grant non-inherited role - non_inher_up_entity = _build_role_assignment_entity( + non_inher_up_entity = self.build_role_assignment_entity( project_id=root_id, user_id=self.user['id'], role_id=non_inherited_role_id) self.put(non_inher_up_entity['links']['assignment']) # Grant inherited role - inher_up_entity = _build_role_assignment_entity( + inher_up_entity = self.build_role_assignment_entity( project_id=root_id, user_id=self.user['id'], role_id=inherited_role_id, inherited_to_projects=True) self.put(inher_up_entity['links']['assignment']) @@ -2808,7 +2987,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.assertRoleAssignmentNotInListResponse(r, inher_up_entity) # Assert that the user does not have non-inherited role on leaf project - non_inher_up_entity = _build_role_assignment_entity( + non_inher_up_entity = self.build_role_assignment_entity( project_id=leaf_id, user_id=self.user['id'], role_id=non_inherited_role_id) self.assertRoleAssignmentNotInListResponse(r, non_inher_up_entity) @@ -2835,13 +3014,13 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self._setup_hierarchical_projects_scenario()) # Grant non-inherited role - non_inher_up_entity = _build_role_assignment_entity( + non_inher_up_entity = self.build_role_assignment_entity( project_id=root_id, user_id=self.user['id'], role_id=non_inherited_role_id) self.put(non_inher_up_entity['links']['assignment']) # Grant inherited role - inher_up_entity = _build_role_assignment_entity( + inher_up_entity = self.build_role_assignment_entity( project_id=root_id, user_id=self.user['id'], role_id=inherited_role_id, inherited_to_projects=True) self.put(inher_up_entity['links']['assignment']) @@ -2860,7 +3039,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): self.assertRoleAssignmentInListResponse(r, inher_up_entity) # Assert that the user does not have non-inherited role on leaf project - non_inher_up_entity = _build_role_assignment_entity( + non_inher_up_entity = self.build_role_assignment_entity( project_id=leaf_id, user_id=self.user['id'], role_id=non_inherited_role_id) self.assertRoleAssignmentNotInListResponse(r, non_inher_up_entity) @@ -2898,11 +3077,32 @@ class AssignmentInheritanceDisabledTestCase(test_v3.RestfulTestCase): class AssignmentV3toV2MethodsTestCase(tests.TestCase): """Test domain V3 to V2 conversion methods.""" + def _setup_initial_projects(self): + self.project_id = uuid.uuid4().hex + self.domain_id = CONF.identity.default_domain_id + self.parent_id = uuid.uuid4().hex + # Project with only domain_id in ref + self.project1 = {'id': self.project_id, + 'name': self.project_id, + 'domain_id': self.domain_id} + # Project with both domain_id and parent_id in ref + self.project2 = {'id': self.project_id, + 'name': self.project_id, + 'domain_id': self.domain_id, + 'parent_id': self.parent_id} + # Project with no domain_id and parent_id in ref + self.project3 = {'id': self.project_id, + 'name': self.project_id, + 'domain_id': self.domain_id, + 'parent_id': self.parent_id} + # Expected result with no domain_id and parent_id + self.expected_project = {'id': self.project_id, + 'name': self.project_id} def test_v2controller_filter_domain_id(self): # V2.0 is not domain aware, ensure domain_id is popped off the ref. other_data = uuid.uuid4().hex - domain_id = uuid.uuid4().hex + domain_id = CONF.identity.default_domain_id ref = {'domain_id': domain_id, 'other_data': other_data} @@ -2941,3 +3141,52 @@ class AssignmentV3toV2MethodsTestCase(tests.TestCase): self.assertRaises(exception.Unauthorized, controller.V2Controller.filter_domain, non_default_domain_ref) + + def test_v2controller_filter_project_parent_id(self): + # V2.0 is not project hierarchy aware, ensure parent_id is popped off. + other_data = uuid.uuid4().hex + parent_id = uuid.uuid4().hex + ref = {'parent_id': parent_id, + 'other_data': other_data} + + ref_no_parent = {'other_data': other_data} + expected_ref = ref_no_parent.copy() + + updated_ref = controller.V2Controller.filter_project_parent_id(ref) + self.assertIs(ref, updated_ref) + self.assertDictEqual(ref, expected_ref) + # Make sure we don't error/muck up data if parent_id isn't present + updated_ref = controller.V2Controller.filter_project_parent_id( + ref_no_parent) + self.assertIs(ref_no_parent, updated_ref) + self.assertDictEqual(ref_no_parent, expected_ref) + + def test_v3_to_v2_project_method(self): + self._setup_initial_projects() + updated_project1 = controller.V2Controller.v3_to_v2_project( + self.project1) + self.assertIs(self.project1, updated_project1) + self.assertDictEqual(self.project1, self.expected_project) + updated_project2 = controller.V2Controller.v3_to_v2_project( + self.project2) + self.assertIs(self.project2, updated_project2) + self.assertDictEqual(self.project2, self.expected_project) + updated_project3 = controller.V2Controller.v3_to_v2_project( + self.project3) + self.assertIs(self.project3, updated_project3) + self.assertDictEqual(self.project3, self.expected_project) + + def test_v3_to_v2_project_method_list(self): + self._setup_initial_projects() + project_list = [self.project1, self.project2, self.project3] + updated_list = controller.V2Controller.v3_to_v2_project(project_list) + + self.assertEqual(len(updated_list), len(project_list)) + + for i, ref in enumerate(updated_list): + # Order should not change. + self.assertIs(ref, project_list[i]) + + self.assertDictEqual(self.project1, self.expected_project) + self.assertDictEqual(self.project2, self.expected_project) + self.assertDictEqual(self.project3, self.expected_project) diff --git a/keystone-moon/keystone/tests/unit/test_v3_auth.py b/keystone-moon/keystone/tests/unit/test_v3_auth.py index ec079170..96f0ff1f 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_auth.py +++ b/keystone-moon/keystone/tests/unit/test_v3_auth.py @@ -22,18 +22,18 @@ from keystoneclient.common import cms import mock from oslo_config import cfg from oslo_utils import timeutils -import six +from six.moves import range from testtools import matchers from testtools import testcase from keystone import auth +from keystone.common import utils from keystone import exception from keystone.policy.backends import rules from keystone.tests import unit as tests from keystone.tests.unit import ksfixtures from keystone.tests.unit import test_v3 - CONF = cfg.CONF @@ -97,8 +97,8 @@ class TestAuthInfo(test_v3.AuthTestMixin, testcase.TestCase): 'password', 'password'] context = None auth_info = auth.controllers.AuthInfo.create(context, auth_data) - self.assertEqual(auth_info.get_method_names(), - ['password', 'token']) + self.assertEqual(['password', 'token'], + auth_info.get_method_names()) def test_get_method_data_invalid_method(self): auth_data = self.build_authentication_request( @@ -114,276 +114,294 @@ class TestAuthInfo(test_v3.AuthTestMixin, testcase.TestCase): class TokenAPITests(object): - # Why is this not just setUP? Because TokenAPITests is not a test class + # Why is this not just setUp? Because TokenAPITests is not a test class # itself. If TokenAPITests became a subclass of the testcase, it would get # called by the enumerate-tests-in-file code. The way the functions get # resolved in Python for multiple inheritance means that a setUp in this # would get skipped by the testrunner. def doSetUp(self): - auth_data = self.build_authentication_request( + r = self.v3_authenticate_token(self.build_authentication_request( username=self.user['name'], user_domain_id=self.domain_id, - password=self.user['password']) - resp = self.v3_authenticate_token(auth_data) - self.token_data = resp.result - self.token = resp.headers.get('X-Subject-Token') - self.headers = {'X-Subject-Token': resp.headers.get('X-Subject-Token')} + password=self.user['password'])) + self.v3_token_data = r.result + self.v3_token = r.headers.get('X-Subject-Token') + self.headers = {'X-Subject-Token': r.headers.get('X-Subject-Token')} def test_default_fixture_scope_token(self): self.assertIsNotNone(self.get_scoped_token()) - def verify_token(self, *args, **kwargs): - return cms.verify_token(*args, **kwargs) - - def test_v3_token_id(self): - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password']) - resp = self.v3_authenticate_token(auth_data) - token_data = resp.result - token_id = resp.headers.get('X-Subject-Token') - self.assertIn('expires_at', token_data['token']) - - decoded_token = self.verify_token(token_id, CONF.signing.certfile, - CONF.signing.ca_certs) - decoded_token_dict = json.loads(decoded_token) - - token_resp_dict = json.loads(resp.body) - - self.assertEqual(decoded_token_dict, token_resp_dict) - # should be able to validate hash PKI token as well - hash_token_id = cms.cms_hash_token(token_id) - headers = {'X-Subject-Token': hash_token_id} - resp = self.get('/auth/tokens', headers=headers) - expected_token_data = resp.result - self.assertDictEqual(expected_token_data, token_data) - def test_v3_v2_intermix_non_default_domain_failed(self): - auth_data = self.build_authentication_request( + v3_token = self.get_requested_token(self.build_authentication_request( user_id=self.user['id'], - password=self.user['password']) - token = self.get_requested_token(auth_data) + password=self.user['password'])) # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - self.admin_request(path=path, - token='ADMIN', - method='GET', - expected_status=401) + self.admin_request( + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token, + method='GET', + expected_status=401) def test_v3_v2_intermix_new_default_domain(self): # If the default_domain_id config option is changed, then should be # able to validate a v3 token with user in the new domain. # 1) Create a new domain for the user. - new_domain_id = uuid.uuid4().hex new_domain = { 'description': uuid.uuid4().hex, 'enabled': True, - 'id': new_domain_id, + 'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, } - - self.resource_api.create_domain(new_domain_id, new_domain) + self.resource_api.create_domain(new_domain['id'], new_domain) # 2) Create user in new domain. new_user_password = uuid.uuid4().hex new_user = { 'name': uuid.uuid4().hex, - 'domain_id': new_domain_id, + 'domain_id': new_domain['id'], 'password': new_user_password, 'email': uuid.uuid4().hex, } - new_user = self.identity_api.create_user(new_user) # 3) Update the default_domain_id config option to the new domain + self.config_fixture.config( + group='identity', + default_domain_id=new_domain['id']) - self.config_fixture.config(group='identity', - default_domain_id=new_domain_id) - - # 4) Get a token using v3 api. - - auth_data = self.build_authentication_request( + # 4) Get a token using v3 API. + v3_token = self.get_requested_token(self.build_authentication_request( user_id=new_user['id'], - password=new_user_password) - token = self.get_requested_token(auth_data) + password=new_user_password)) - # 5) Authenticate token using v2 api. - - path = '/v2.0/tokens/%s' % (token) - self.admin_request(path=path, - token='ADMIN', - method='GET') + # 5) Validate token using v2 API. + self.admin_request( + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token, + method='GET') def test_v3_v2_intermix_domain_scoped_token_failed(self): # grant the domain role to user - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - auth_data = self.build_authentication_request( + self.put( + path='/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id'])) + + # generate a domain-scoped v3 token + v3_token = self.get_requested_token(self.build_authentication_request( user_id=self.user['id'], password=self.user['password'], - domain_id=self.domain['id']) - token = self.get_requested_token(auth_data) + domain_id=self.domain['id'])) - # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - self.admin_request(path=path, - token='ADMIN', - method='GET', - expected_status=401) + # domain-scoped tokens are not supported by v2 + self.admin_request( + method='GET', + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token, + expected_status=401) def test_v3_v2_intermix_non_default_project_failed(self): - auth_data = self.build_authentication_request( + # self.project is in a non-default domain + v3_token = self.get_requested_token(self.build_authentication_request( user_id=self.default_domain_user['id'], password=self.default_domain_user['password'], - project_id=self.project['id']) - token = self.get_requested_token(auth_data) + project_id=self.project['id'])) - # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - self.admin_request(path=path, - token='ADMIN', - method='GET', - expected_status=401) + # v2 cannot reference projects outside the default domain + self.admin_request( + method='GET', + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token, + expected_status=401) + + def test_v3_v2_intermix_non_default_user_failed(self): + self.assignment_api.create_grant( + self.role['id'], + user_id=self.user['id'], + project_id=self.default_domain_project['id']) + + # self.user is in a non-default domain + v3_token = self.get_requested_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.default_domain_project['id'])) + + # v2 cannot reference projects outside the default domain + self.admin_request( + method='GET', + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token, + expected_status=401) + + def test_v3_v2_intermix_domain_scope_failed(self): + self.assignment_api.create_grant( + self.role['id'], + user_id=self.default_domain_user['id'], + domain_id=self.domain['id']) + + v3_token = self.get_requested_token(self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + domain_id=self.domain['id'])) + + # v2 cannot reference projects outside the default domain + self.admin_request( + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token, + method='GET', + expected_status=401) def test_v3_v2_unscoped_token_intermix(self): - auth_data = self.build_authentication_request( + r = self.v3_authenticate_token(self.build_authentication_request( user_id=self.default_domain_user['id'], - password=self.default_domain_user['password']) - resp = self.v3_authenticate_token(auth_data) - token_data = resp.result - token = resp.headers.get('X-Subject-Token') + password=self.default_domain_user['password'])) + self.assertValidUnscopedTokenResponse(r) + v3_token_data = r.result + v3_token = r.headers.get('X-Subject-Token') # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - resp = self.admin_request(path=path, - token='ADMIN', - method='GET') - v2_token = resp.result - self.assertEqual(v2_token['access']['user']['id'], - token_data['token']['user']['id']) + r = self.admin_request( + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token, + method='GET') + v2_token_data = r.result + + self.assertEqual(v2_token_data['access']['user']['id'], + v3_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.assertIn(v2_token_data['access']['token']['expires'][:-1], + v3_token_data['token']['expires_at']) def test_v3_v2_token_intermix(self): # FIXME(gyee): PKI tokens are not interchangeable because token # data is baked into the token itself. - auth_data = self.build_authentication_request( + r = self.v3_authenticate_token(self.build_authentication_request( user_id=self.default_domain_user['id'], password=self.default_domain_user['password'], - project_id=self.default_domain_project['id']) - resp = self.v3_authenticate_token(auth_data) - token_data = resp.result - token = resp.headers.get('X-Subject-Token') + project_id=self.default_domain_project['id'])) + self.assertValidProjectScopedTokenResponse(r) + v3_token_data = r.result + v3_token = r.headers.get('X-Subject-Token') # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - resp = self.admin_request(path=path, - token='ADMIN', - method='GET') - v2_token = resp.result - self.assertEqual(v2_token['access']['user']['id'], - 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['token']['roles'][0]['id']) + r = self.admin_request( + method='GET', + path='/v2.0/tokens/%s' % v3_token, + token=CONF.admin_token) + v2_token_data = r.result - def test_v3_v2_hashed_pki_token_intermix(self): - auth_data = self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - project_id=self.default_domain_project['id']) - resp = self.v3_authenticate_token(auth_data) - token_data = resp.result - token = resp.headers.get('X-Subject-Token') - - # should be able to validate a hash PKI token in v2 too - token = cms.cms_hash_token(token) - path = '/v2.0/tokens/%s' % (token) - resp = self.admin_request(path=path, - token='ADMIN', - method='GET') - v2_token = resp.result - self.assertEqual(v2_token['access']['user']['id'], - token_data['token']['user']['id']) + self.assertEqual(v2_token_data['access']['user']['id'], + v3_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['token']['roles'][0]['id']) + self.assertIn(v2_token_data['access']['token']['expires'][:-1], + v3_token_data['token']['expires_at']) + self.assertEqual(v2_token_data['access']['user']['roles'][0]['name'], + v3_token_data['token']['roles'][0]['name']) def test_v2_v3_unscoped_token_intermix(self): - body = { - 'auth': { - 'passwordCredentials': { - 'userId': self.user['id'], - 'password': self.user['password'] + r = self.admin_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'passwordCredentials': { + 'userId': self.default_domain_user['id'], + 'password': self.default_domain_user['password'] + } } - }} - resp = self.admin_request(path='/v2.0/tokens', - method='POST', - body=body) - v2_token_data = resp.result + }) + v2_token_data = r.result v2_token = v2_token_data['access']['token']['id'] - headers = {'X-Subject-Token': v2_token} - resp = self.get('/auth/tokens', headers=headers) - token_data = resp.result + + r = self.get('/auth/tokens', headers={'X-Subject-Token': v2_token}) + # FIXME(dolph): Due to bug 1476329, v2 tokens validated on v3 are + # missing timezones, so they will not pass this assertion. + # self.assertValidUnscopedTokenResponse(r) + v3_token_data = r.result + self.assertEqual(v2_token_data['access']['user']['id'], - token_data['token']['user']['id']) + v3_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']) + v3_token_data['token']['expires_at']) def test_v2_v3_token_intermix(self): - body = { - 'auth': { - 'passwordCredentials': { - 'userId': self.user['id'], - 'password': self.user['password'] - }, - 'tenantId': self.project['id'] - }} - resp = self.admin_request(path='/v2.0/tokens', - method='POST', - body=body) - v2_token_data = resp.result + r = self.admin_request( + path='/v2.0/tokens', + method='POST', + body={ + 'auth': { + 'passwordCredentials': { + 'userId': self.default_domain_user['id'], + 'password': self.default_domain_user['password'] + }, + 'tenantId': self.default_domain_project['id'] + } + }) + v2_token_data = r.result v2_token = v2_token_data['access']['token']['id'] - headers = {'X-Subject-Token': v2_token} - resp = self.get('/auth/tokens', headers=headers) - token_data = resp.result + + r = self.get('/auth/tokens', headers={'X-Subject-Token': v2_token}) + # FIXME(dolph): Due to bug 1476329, v2 tokens validated on v3 are + # missing timezones, so they will not pass this assertion. + # self.assertValidProjectScopedTokenResponse(r) + v3_token_data = r.result + self.assertEqual(v2_token_data['access']['user']['id'], - token_data['token']['user']['id']) + v3_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']) + v3_token_data['token']['expires_at']) self.assertEqual(v2_token_data['access']['user']['roles'][0]['name'], - token_data['token']['roles'][0]['name']) + v3_token_data['token']['roles'][0]['name']) v2_issued_at = timeutils.parse_isotime( v2_token_data['access']['token']['issued_at']) v3_issued_at = timeutils.parse_isotime( - token_data['token']['issued_at']) + v3_token_data['token']['issued_at']) self.assertEqual(v2_issued_at, v3_issued_at) + def test_v2_token_deleted_on_v3(self): + # Create a v2 token. + body = { + 'auth': { + 'passwordCredentials': { + 'userId': self.default_domain_user['id'], + 'password': self.default_domain_user['password'] + }, + 'tenantId': self.default_domain_project['id'] + } + } + r = self.admin_request( + path='/v2.0/tokens', method='POST', body=body) + v2_token = r.result['access']['token']['id'] + + # Delete the v2 token using v3. + resp = self.delete( + '/auth/tokens', headers={'X-Subject-Token': v2_token}) + self.assertEqual(resp.status_code, 204) + + # Attempting to use the deleted token on v2 should fail. + self.admin_request( + path='/v2.0/tenants', method='GET', token=v2_token, + expected_status=401) + def test_rescoping_token(self): - expires = self.token_data['token']['expires_at'] - auth_data = self.build_authentication_request( - token=self.token, - project_id=self.project_id) - r = self.v3_authenticate_token(auth_data) + expires = self.v3_token_data['token']['expires_at'] + + # rescope the token + r = self.v3_authenticate_token(self.build_authentication_request( + token=self.v3_token, + project_id=self.project_id)) self.assertValidProjectScopedTokenResponse(r) - # make sure expires stayed the same + + # ensure token expiration stayed the same self.assertEqual(expires, r.result['token']['expires_at']) def test_check_token(self): @@ -394,12 +412,13 @@ class TokenAPITests(object): self.assertValidUnscopedTokenResponse(r) def test_validate_token_nocatalog(self): - auth_data = self.build_authentication_request( + v3_token = self.get_requested_token(self.build_authentication_request( user_id=self.user['id'], password=self.user['password'], - project_id=self.project['id']) - headers = {'X-Subject-Token': self.get_requested_token(auth_data)} - r = self.get('/auth/tokens?nocatalog', headers=headers) + project_id=self.project['id'])) + r = self.get( + '/auth/tokens?nocatalog', + headers={'X-Subject-Token': v3_token}) self.assertValidProjectScopedTokenResponse(r, require_catalog=False) @@ -420,10 +439,10 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): def _v2_token(self): body = { 'auth': { - "tenantId": self.project['id'], + "tenantId": self.default_domain_project['id'], 'passwordCredentials': { - 'userId': self.user['id'], - 'password': self.user['password'] + 'userId': self.default_domain_user['id'], + 'password': self.default_domain_user['password'] } }} resp = self.admin_request(path='/v2.0/tokens', @@ -462,7 +481,7 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): def test_rescoped_domain_token_disabled(self): self.domainA = self.new_domain_ref() - self.assignment_api.create_domain(self.domainA['id'], self.domainA) + self.resource_api.create_domain(self.domainA['id'], self.domainA) self.assignment_api.create_grant(self.role['id'], user_id=self.user['id'], domain_id=self.domainA['id']) @@ -485,37 +504,77 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests): def config_overrides(self): super(TestPKITokenAPIs, self).config_overrides() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.pki.Provider') + self.config_fixture.config(group='token', provider='pki') def setUp(self): super(TestPKITokenAPIs, self).setUp() self.doSetUp() + def verify_token(self, *args, **kwargs): + return cms.verify_token(*args, **kwargs) -class TestPKIZTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): + def test_v3_token_id(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + resp = self.v3_authenticate_token(auth_data) + token_data = resp.result + token_id = resp.headers.get('X-Subject-Token') + self.assertIn('expires_at', token_data['token']) - def verify_token(self, *args, **kwargs): - return cms.pkiz_verify(*args, **kwargs) + decoded_token = self.verify_token(token_id, CONF.signing.certfile, + CONF.signing.ca_certs) + decoded_token_dict = json.loads(decoded_token) + + token_resp_dict = json.loads(resp.body) + + self.assertEqual(decoded_token_dict, token_resp_dict) + # should be able to validate hash PKI token as well + hash_token_id = cms.cms_hash_token(token_id) + headers = {'X-Subject-Token': hash_token_id} + resp = self.get('/auth/tokens', headers=headers) + expected_token_data = resp.result + self.assertDictEqual(expected_token_data, token_data) + def test_v3_v2_hashed_pki_token_intermix(self): + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project['id']) + resp = self.v3_authenticate_token(auth_data) + token_data = resp.result + token = resp.headers.get('X-Subject-Token') + + # should be able to validate a hash PKI token in v2 too + token = cms.cms_hash_token(token) + path = '/v2.0/tokens/%s' % (token) + resp = self.admin_request(path=path, + token=CONF.admin_token, + method='GET') + v2_token = resp.result + self.assertEqual(v2_token['access']['user']['id'], + 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['token']['roles'][0]['id']) + + +class TestPKIZTokenAPIs(TestPKITokenAPIs): def config_overrides(self): super(TestPKIZTokenAPIs, self).config_overrides() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.pkiz.Provider') + self.config_fixture.config(group='token', provider='pkiz') - def setUp(self): - super(TestPKIZTokenAPIs, self).setUp() - self.doSetUp() + def verify_token(self, *args, **kwargs): + return cms.pkiz_verify(*args, **kwargs) class TestUUIDTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): def config_overrides(self): super(TestUUIDTokenAPIs, self).config_overrides() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.uuid.Provider') + self.config_fixture.config(group='token', provider='uuid') def setUp(self): super(TestUUIDTokenAPIs, self).setUp() @@ -531,10 +590,16 @@ class TestUUIDTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): self.assertIn('expires_at', token_data['token']) self.assertFalse(cms.is_asn1_token(token_id)) - def test_v3_v2_hashed_pki_token_intermix(self): - # this test is only applicable for PKI tokens - # skipping it for UUID tokens - pass + +class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): + def config_overrides(self): + super(TestFernetTokenAPIs, self).config_overrides() + self.config_fixture.config(group='token', provider='fernet') + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + def setUp(self): + super(TestFernetTokenAPIs, self).setUp() + self.doSetUp() class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): @@ -675,12 +740,10 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): def config_overrides(self): super(TestTokenRevokeById, self).config_overrides() - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', - provider='keystone.token.providers.pki.Provider', + provider='pki', revoke_by_id=False) def setUp(self): @@ -1069,7 +1132,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): - Delete the grant group1 has on ProjectA - Check tokens for user1 & user2 are no longer valid, since user1 and user2 are members of group1 - - Check token for user3 is still valid + - Check token for user3 is invalid too """ auth_data = self.build_authentication_request( @@ -1112,10 +1175,11 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.head('/auth/tokens', headers={'X-Subject-Token': token2}, expected_status=404) - # But user3's token should still be valid + # But user3's token should be invalid too as revocation is done for + # scope role & project self.head('/auth/tokens', headers={'X-Subject-Token': token3}, - expected_status=200) + expected_status=404) def test_domain_group_role_assignment_maintains_token(self): """Test domain-group role assignment maintains existing token. @@ -1202,6 +1266,14 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): def test_removing_role_assignment_does_not_affect_other_users(self): """Revoking a role from one user should not affect other users.""" + + # This group grant is not needed for the test + self.delete( + '/projects/%(project_id)s/groups/%(group_id)s/roles/%(role_id)s' % + {'project_id': self.projectA['id'], + 'group_id': self.group1['id'], + 'role_id': self.role1['id']}) + user1_token = self.get_requested_token( self.build_authentication_request( user_id=self.user1['id'], @@ -1220,12 +1292,6 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): 'project_id': self.projectA['id'], 'user_id': self.user1['id'], 'role_id': self.role1['id']}) - self.delete( - '/projects/%(project_id)s/groups/%(group_id)s/roles/%(role_id)s' % - {'project_id': self.projectA['id'], - 'group_id': self.group1['id'], - 'role_id': self.role1['id']}) - # authorization for the first user should now fail self.head('/auth/tokens', headers={'X-Subject-Token': user1_token}, @@ -1384,6 +1450,58 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): expected_status=200) +class TestTokenRevokeByAssignment(TestTokenRevokeById): + + def config_overrides(self): + super(TestTokenRevokeById, self).config_overrides() + self.config_fixture.config( + group='revoke', + driver='kvs') + self.config_fixture.config( + group='token', + provider='uuid', + revoke_by_id=True) + + def test_removing_role_assignment_keeps_other_project_token_groups(self): + """Test assignment isolation. + + Revoking a group role from one project should not invalidate all group + users' tokens + """ + self.assignment_api.create_grant(self.role1['id'], + group_id=self.group1['id'], + project_id=self.projectB['id']) + + project_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectB['id'])) + + other_project_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id'])) + + self.assignment_api.delete_grant(self.role1['id'], + group_id=self.group1['id'], + project_id=self.projectB['id']) + + # authorization for the projectA should still succeed + self.head('/auth/tokens', + headers={'X-Subject-Token': other_project_token}, + expected_status=200) + # while token for the projectB should not + self.head('/auth/tokens', + headers={'X-Subject-Token': project_token}, + expected_status=404) + revoked_tokens = [ + t['id'] for t in self.token_provider_api.list_revoked_tokens()] + # token is in token revocation list + self.assertIn(project_token, revoked_tokens) + + class TestTokenRevokeApi(TestTokenRevokeById): EXTENSION_NAME = 'revoke' EXTENSION_TO_ADD = 'revoke_extension' @@ -1391,12 +1509,10 @@ class TestTokenRevokeApi(TestTokenRevokeById): """Test token revocation on the v3 Identity API.""" def config_overrides(self): super(TestTokenRevokeApi, self).config_overrides() - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', - provider='keystone.token.providers.pki.Provider', + provider='pki', revoke_by_id=False) def assertValidDeletedProjectResponse(self, events_response, project_id): @@ -1424,7 +1540,7 @@ class TestTokenRevokeApi(TestTokenRevokeById): def assertValidRevokedTokenResponse(self, events_response, **kwargs): events = events_response['events'] self.assertEqual(1, len(events)) - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): self.assertEqual(v, events[0].get(k)) self.assertIsNotNone(events[0]['issued_before']) self.assertIsNotNone(events_response['links']) @@ -1494,7 +1610,7 @@ class TestTokenRevokeApi(TestTokenRevokeById): def assertEventDataInList(self, events, **kwargs): found = False for e in events: - for key, value in six.iteritems(kwargs): + for key, value in kwargs.items(): try: if e[key] != value: break @@ -1512,8 +1628,7 @@ class TestTokenRevokeApi(TestTokenRevokeById): 'find event with key-value pairs. Expected: ' '"%(expected)s" Events: "%(events)s"' % {'expected': ','.join( - ["'%s=%s'" % (k, v) for k, v in six.iteritems( - kwargs)]), + ["'%s=%s'" % (k, v) for k, v in kwargs.items()]), 'events': events}) def test_list_delete_token_shows_in_event_list(self): @@ -1569,8 +1684,8 @@ class TestTokenRevokeApi(TestTokenRevokeById): expected_status=200).json_body['events'] self.assertEqual(2, len(events)) - future = timeutils.isotime(timeutils.utcnow() + - datetime.timedelta(seconds=1000)) + future = utils.isotime(timeutils.utcnow() + + datetime.timedelta(seconds=1000)) events = self.get('/OS-REVOKE/events?since=%s' % (future), expected_status=200).json_body['events'] @@ -1596,148 +1711,116 @@ class TestAuthExternalDisabled(test_v3.RestfulTestCase): auth_context) -class TestAuthExternalLegacyDefaultDomain(test_v3.RestfulTestCase): - content_type = 'json' - - def config_overrides(self): - super(TestAuthExternalLegacyDefaultDomain, self).config_overrides() - self.auth_plugin_config_override( - methods=['external', 'password', 'token'], - external='keystone.auth.plugins.external.LegacyDefaultDomain', - password='keystone.auth.plugins.password.Password', - token='keystone.auth.plugins.token.Token') - - def test_remote_user_no_realm(self): - self.config_fixture.config(group='auth', methods='external') - api = auth.controllers.Auth() - context, auth_info, auth_context = self.build_external_auth_request( - self.default_domain_user['name']) - api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], - self.default_domain_user['id']) - - def test_remote_user_no_domain(self): - api = auth.controllers.Auth() - context, auth_info, auth_context = self.build_external_auth_request( - self.user['name']) - self.assertRaises(exception.Unauthorized, - api.authenticate, - context, - auth_info, - auth_context) - - -class TestAuthExternalLegacyDomain(test_v3.RestfulTestCase): +class TestAuthExternalDomain(test_v3.RestfulTestCase): content_type = 'json' def config_overrides(self): - super(TestAuthExternalLegacyDomain, self).config_overrides() - self.auth_plugin_config_override( - methods=['external', 'password', 'token'], - external='keystone.auth.plugins.external.LegacyDomain', - password='keystone.auth.plugins.password.Password', - token='keystone.auth.plugins.token.Token') + super(TestAuthExternalDomain, self).config_overrides() + self.kerberos = False + self.auth_plugin_config_override(external='Domain') def test_remote_user_with_realm(self): api = auth.controllers.Auth() - remote_user = '%s@%s' % (self.user['name'], self.domain['name']) + remote_user = self.user['name'] + remote_domain = self.domain['name'] context, auth_info, auth_context = self.build_external_auth_request( - remote_user) + remote_user, remote_domain=remote_domain, kerberos=self.kerberos) api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], self.user['id']) + self.assertEqual(self.user['id'], auth_context['user_id']) # Now test to make sure the user name can, itself, contain the # '@' character. user = {'name': 'myname@mydivision'} self.identity_api.update_user(self.user['id'], user) - remote_user = '%s@%s' % (user['name'], self.domain['name']) + remote_user = user['name'] context, auth_info, auth_context = self.build_external_auth_request( - remote_user) + remote_user, remote_domain=remote_domain, kerberos=self.kerberos) api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], self.user['id']) + self.assertEqual(self.user['id'], auth_context['user_id']) def test_project_id_scoped_with_remote_user(self): self.config_fixture.config(group='token', bind=['kerberos']) auth_data = self.build_authentication_request( - project_id=self.project['id']) - remote_user = '%s@%s' % (self.user['name'], self.domain['name']) + project_id=self.project['id'], + kerberos=self.kerberos) + remote_user = self.user['name'] + remote_domain = self.domain['name'] self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'REMOTE_DOMAIN': remote_domain, 'AUTH_TYPE': 'Negotiate'}) r = self.v3_authenticate_token(auth_data) token = self.assertValidProjectScopedTokenResponse(r) - self.assertEqual(token['bind']['kerberos'], self.user['name']) + self.assertEqual(self.user['name'], token['bind']['kerberos']) def test_unscoped_bind_with_remote_user(self): self.config_fixture.config(group='token', bind=['kerberos']) - auth_data = self.build_authentication_request() - remote_user = '%s@%s' % (self.user['name'], self.domain['name']) + auth_data = self.build_authentication_request(kerberos=self.kerberos) + remote_user = self.user['name'] + remote_domain = self.domain['name'] self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'REMOTE_DOMAIN': remote_domain, 'AUTH_TYPE': 'Negotiate'}) r = self.v3_authenticate_token(auth_data) token = self.assertValidUnscopedTokenResponse(r) - self.assertEqual(token['bind']['kerberos'], self.user['name']) + self.assertEqual(self.user['name'], token['bind']['kerberos']) -class TestAuthExternalDomain(test_v3.RestfulTestCase): +class TestAuthExternalDefaultDomain(test_v3.RestfulTestCase): content_type = 'json' def config_overrides(self): - super(TestAuthExternalDomain, self).config_overrides() + super(TestAuthExternalDefaultDomain, self).config_overrides() self.kerberos = False self.auth_plugin_config_override( - methods=['external', 'password', 'token'], - external='keystone.auth.plugins.external.Domain', - password='keystone.auth.plugins.password.Password', - token='keystone.auth.plugins.token.Token') + external='keystone.auth.plugins.external.DefaultDomain') - def test_remote_user_with_realm(self): + def test_remote_user_with_default_domain(self): api = auth.controllers.Auth() - remote_user = self.user['name'] - remote_domain = self.domain['name'] + remote_user = self.default_domain_user['name'] context, auth_info, auth_context = self.build_external_auth_request( - remote_user, remote_domain=remote_domain, kerberos=self.kerberos) + remote_user, kerberos=self.kerberos) api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], self.user['id']) + self.assertEqual(self.default_domain_user['id'], + auth_context['user_id']) # Now test to make sure the user name can, itself, contain the # '@' character. user = {'name': 'myname@mydivision'} - self.identity_api.update_user(self.user['id'], user) + self.identity_api.update_user(self.default_domain_user['id'], user) remote_user = user['name'] context, auth_info, auth_context = self.build_external_auth_request( - remote_user, remote_domain=remote_domain, kerberos=self.kerberos) + remote_user, kerberos=self.kerberos) api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], self.user['id']) + self.assertEqual(self.default_domain_user['id'], + auth_context['user_id']) def test_project_id_scoped_with_remote_user(self): self.config_fixture.config(group='token', bind=['kerberos']) auth_data = self.build_authentication_request( - project_id=self.project['id'], + project_id=self.default_domain_project['id'], kerberos=self.kerberos) - remote_user = self.user['name'] - remote_domain = self.domain['name'] + remote_user = self.default_domain_user['name'] self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, - 'REMOTE_DOMAIN': remote_domain, 'AUTH_TYPE': 'Negotiate'}) r = self.v3_authenticate_token(auth_data) token = self.assertValidProjectScopedTokenResponse(r) - self.assertEqual(token['bind']['kerberos'], self.user['name']) + self.assertEqual(self.default_domain_user['name'], + token['bind']['kerberos']) def test_unscoped_bind_with_remote_user(self): self.config_fixture.config(group='token', bind=['kerberos']) auth_data = self.build_authentication_request(kerberos=self.kerberos) - remote_user = self.user['name'] - remote_domain = self.domain['name'] + remote_user = self.default_domain_user['name'] self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, - 'REMOTE_DOMAIN': remote_domain, 'AUTH_TYPE': 'Negotiate'}) r = self.v3_authenticate_token(auth_data) token = self.assertValidUnscopedTokenResponse(r) - self.assertEqual(token['bind']['kerberos'], self.user['name']) + self.assertEqual(self.default_domain_user['name'], + token['bind']['kerberos']) class TestAuthKerberos(TestAuthExternalDomain): @@ -1746,10 +1829,7 @@ class TestAuthKerberos(TestAuthExternalDomain): super(TestAuthKerberos, self).config_overrides() self.kerberos = True self.auth_plugin_config_override( - methods=['kerberos', 'password', 'token'], - kerberos='keystone.auth.plugins.external.KerberosDomain', - password='keystone.auth.plugins.password.Password', - token='keystone.auth.plugins.token.Token') + methods=['kerberos', 'password', 'token']) class TestAuth(test_v3.RestfulTestCase): @@ -1815,7 +1895,7 @@ class TestAuth(test_v3.RestfulTestCase): password=self.user['password']) r = self.v3_authenticate_token(auth_data) self.assertValidProjectScopedTokenResponse(r) - self.assertEqual(r.result['token']['project']['id'], project['id']) + self.assertEqual(project['id'], r.result['token']['project']['id']) def test_default_project_id_scoped_token_with_user_id_no_catalog(self): project = self._second_project_as_default() @@ -1826,7 +1906,7 @@ class TestAuth(test_v3.RestfulTestCase): password=self.user['password']) r = self.post('/auth/tokens?nocatalog', body=auth_data, noauth=True) self.assertValidProjectScopedTokenResponse(r, require_catalog=False) - self.assertEqual(r.result['token']['project']['id'], project['id']) + self.assertEqual(project['id'], r.result['token']['project']['id']) def test_explicit_unscoped_token(self): self._second_project_as_default() @@ -1850,8 +1930,8 @@ class TestAuth(test_v3.RestfulTestCase): project_id=self.project['id']) r = self.post('/auth/tokens?nocatalog', body=auth_data, noauth=True) self.assertValidProjectScopedTokenResponse(r, require_catalog=False) - self.assertEqual(r.result['token']['project']['id'], - self.project['id']) + self.assertEqual(self.project['id'], + r.result['token']['project']['id']) def test_auth_catalog_attributes(self): auth_data = self.build_authentication_request( @@ -2345,13 +2425,12 @@ class TestAuth(test_v3.RestfulTestCase): self.v3_authenticate_token(auth_data, expected_status=401) def test_remote_user_no_realm(self): - CONF.auth.methods = 'external' api = auth.controllers.Auth() context, auth_info, auth_context = self.build_external_auth_request( self.default_domain_user['name']) api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], - self.default_domain_user['id']) + self.assertEqual(self.default_domain_user['id'], + auth_context['user_id']) # Now test to make sure the user name can, itself, contain the # '@' character. user = {'name': 'myname@mydivision'} @@ -2359,8 +2438,8 @@ class TestAuth(test_v3.RestfulTestCase): context, auth_info, auth_context = self.build_external_auth_request( user["name"]) api.authenticate(context, auth_info, auth_context) - self.assertEqual(auth_context['user_id'], - self.default_domain_user['id']) + self.assertEqual(self.default_domain_user['id'], + auth_context['user_id']) def test_remote_user_no_domain(self): api = auth.controllers.Auth() @@ -2441,8 +2520,8 @@ class TestAuth(test_v3.RestfulTestCase): headers = {'X-Subject-Token': token} r = self.get('/auth/tokens', headers=headers, token=token) token = self.assertValidProjectScopedTokenResponse(r) - self.assertEqual(token['bind']['kerberos'], - self.default_domain_user['name']) + self.assertEqual(self.default_domain_user['name'], + token['bind']['kerberos']) def test_auth_with_bind_token(self): self.config_fixture.config(group='token', bind=['kerberos']) @@ -2455,7 +2534,7 @@ class TestAuth(test_v3.RestfulTestCase): # the unscoped token should have bind information in it token = self.assertValidUnscopedTokenResponse(r) - self.assertEqual(token['bind']['kerberos'], remote_user) + self.assertEqual(remote_user, token['bind']['kerberos']) token = r.headers.get('X-Subject-Token') @@ -2466,7 +2545,7 @@ class TestAuth(test_v3.RestfulTestCase): token = self.assertValidProjectScopedTokenResponse(r) # the bind information should be carried over from the original token - self.assertEqual(token['bind']['kerberos'], remote_user) + self.assertEqual(remote_user, token['bind']['kerberos']) def test_v2_v3_bind_token_intermix(self): self.config_fixture.config(group='token', bind='kerberos') @@ -2484,7 +2563,7 @@ class TestAuth(test_v3.RestfulTestCase): v2_token_data = resp.result bind = v2_token_data['access']['token']['bind'] - self.assertEqual(bind['kerberos'], self.default_domain_user['name']) + self.assertEqual(self.default_domain_user['name'], bind['kerberos']) v2_token_id = v2_token_data['access']['token']['id'] # NOTE(gyee): self.get() will try to obtain an auth token if one @@ -2613,12 +2692,8 @@ class TestAuth(test_v3.RestfulTestCase): class TestAuthJSONExternal(test_v3.RestfulTestCase): content_type = 'json' - def config_overrides(self): - super(TestAuthJSONExternal, self).config_overrides() - self.config_fixture.config(group='auth', methods='') - def auth_plugin_config_override(self, methods=None, **method_classes): - self.config_fixture.config(group='auth', methods='') + self.config_fixture.config(group='auth', methods=[]) def test_remote_user_no_method(self): api = auth.controllers.Auth() @@ -2787,12 +2862,12 @@ class TestTrustRedelegation(test_v3.RestfulTestCase): self.post('/OS-TRUST/trusts', body={'trust': self.chained_trust_ref}, token=trust_token, - expected_status=403) + expected_status=400) def test_roles_subset(self): # Build second role role = self.new_role_ref() - self.assignment_api.create_role(role['id'], role) + self.role_api.create_role(role['id'], role) # assign a new role to the user self.assignment_api.create_grant(role_id=role['id'], user_id=self.user_id, @@ -2860,7 +2935,7 @@ class TestTrustRedelegation(test_v3.RestfulTestCase): # Build second trust with a role not in parent's roles role = self.new_role_ref() - self.assignment_api.create_role(role['id'], role) + self.role_api.create_role(role['id'], role) # assign a new role to the user self.assignment_api.create_grant(role_id=role['id'], user_id=self.user_id, @@ -2895,7 +2970,7 @@ class TestTrustRedelegation(test_v3.RestfulTestCase): # Check that allow_redelegation == False caused redelegation_count # to be set to 0, while allow_redelegation is removed self.assertNotIn('allow_redelegation', trust) - self.assertEqual(trust['redelegation_count'], 0) + self.assertEqual(0, trust['redelegation_count']) trust_token = self._get_trust_token(trust) # Build third trust, same as second @@ -2921,7 +2996,7 @@ class TestTrustChain(test_v3.RestfulTestCase): # Create trust chain self.user_chain = list() self.trust_chain = list() - for _ in xrange(3): + for _ in range(3): user_ref = self.new_user_ref(domain_id=self.domain_id) user = self.identity_api.create_user(user_ref) user['password'] = user_ref['password'] @@ -3067,12 +3142,10 @@ class TestTrustAuth(test_v3.RestfulTestCase): def config_overrides(self): super(TestTrustAuth, self).config_overrides() - self.config_fixture.config( - group='revoke', - driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', - provider='keystone.token.providers.pki.Provider', + provider='pki', revoke_by_id=False) self.config_fixture.config(group='trust', enabled=True) @@ -3139,7 +3212,7 @@ class TestTrustAuth(test_v3.RestfulTestCase): expected_status=200) trust = r.result.get('trust') self.assertIsNotNone(trust) - self.assertEqual(trust['remaining_uses'], 1) + self.assertEqual(1, trust['remaining_uses']) def test_create_one_time_use_trust(self): trust = self._initialize_test_consume_trust(1) @@ -3320,26 +3393,6 @@ class TestTrustAuth(test_v3.RestfulTestCase): role_names=[uuid.uuid4().hex]) self.post('/OS-TRUST/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]) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - trust = self.assertValidTrustResponse(r, ref) - - self.get('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': trust['id']}, - expected_status=404) - - auth_data = self.build_authentication_request( - user_id=self.trustee_user['id'], - password=self.trustee_user['password'], - trust_id=trust['id']) - self.v3_authenticate_token(auth_data, expected_status=401) - def test_v3_v2_intermix_trustor_not_in_default_domain_failed(self): ref = self.new_trust_ref( trustor_user_id=self.user_id, @@ -3365,7 +3418,8 @@ class TestTrustAuth(test_v3.RestfulTestCase): # now validate the v3 token with v2 API path = '/v2.0/tokens/%s' % (token) self.admin_request( - path=path, token='ADMIN', method='GET', expected_status=401) + path=path, token=CONF.admin_token, + method='GET', expected_status=401) def test_v3_v2_intermix_trustor_not_in_default_domaini_failed(self): ref = self.new_trust_ref( @@ -3397,7 +3451,8 @@ class TestTrustAuth(test_v3.RestfulTestCase): # now validate the v3 token with v2 API path = '/v2.0/tokens/%s' % (token) self.admin_request( - path=path, token='ADMIN', method='GET', expected_status=401) + path=path, token=CONF.admin_token, + method='GET', expected_status=401) def test_v3_v2_intermix_project_not_in_default_domaini_failed(self): # create a trustee in default domain to delegate stuff to @@ -3436,7 +3491,8 @@ class TestTrustAuth(test_v3.RestfulTestCase): # now validate the v3 token with v2 API path = '/v2.0/tokens/%s' % (token) self.admin_request( - path=path, token='ADMIN', method='GET', expected_status=401) + path=path, token=CONF.admin_token, + method='GET', expected_status=401) def test_v3_v2_intermix(self): # create a trustee in default domain to delegate stuff to @@ -3474,7 +3530,8 @@ class TestTrustAuth(test_v3.RestfulTestCase): # now validate the v3 token with v2 API path = '/v2.0/tokens/%s' % (token) self.admin_request( - path=path, token='ADMIN', method='GET', expected_status=200) + path=path, token=CONF.admin_token, + method='GET', expected_status=200) def test_exercise_trust_scoped_token_without_impersonation(self): ref = self.new_trust_ref( @@ -3494,18 +3551,18 @@ class TestTrustAuth(test_v3.RestfulTestCase): trust_id=trust['id']) r = self.v3_authenticate_token(auth_data) self.assertValidProjectTrustScopedTokenResponse(r, self.trustee_user) - self.assertEqual(r.result['token']['user']['id'], - self.trustee_user['id']) - self.assertEqual(r.result['token']['user']['name'], - self.trustee_user['name']) - self.assertEqual(r.result['token']['user']['domain']['id'], - self.domain['id']) - self.assertEqual(r.result['token']['user']['domain']['name'], - self.domain['name']) - self.assertEqual(r.result['token']['project']['id'], - self.project['id']) - self.assertEqual(r.result['token']['project']['name'], - self.project['name']) + self.assertEqual(self.trustee_user['id'], + r.result['token']['user']['id']) + self.assertEqual(self.trustee_user['name'], + r.result['token']['user']['name']) + self.assertEqual(self.domain['id'], + r.result['token']['user']['domain']['id']) + self.assertEqual(self.domain['name'], + r.result['token']['user']['domain']['name']) + self.assertEqual(self.project['id'], + r.result['token']['project']['id']) + self.assertEqual(self.project['name'], + r.result['token']['project']['name']) def test_exercise_trust_scoped_token_with_impersonation(self): ref = self.new_trust_ref( @@ -3525,16 +3582,16 @@ class TestTrustAuth(test_v3.RestfulTestCase): trust_id=trust['id']) r = self.v3_authenticate_token(auth_data) self.assertValidProjectTrustScopedTokenResponse(r, self.user) - self.assertEqual(r.result['token']['user']['id'], self.user['id']) - self.assertEqual(r.result['token']['user']['name'], self.user['name']) - self.assertEqual(r.result['token']['user']['domain']['id'], - self.domain['id']) - self.assertEqual(r.result['token']['user']['domain']['name'], - self.domain['name']) - self.assertEqual(r.result['token']['project']['id'], - self.project['id']) - self.assertEqual(r.result['token']['project']['name'], - self.project['name']) + self.assertEqual(self.user['id'], r.result['token']['user']['id']) + self.assertEqual(self.user['name'], r.result['token']['user']['name']) + self.assertEqual(self.domain['id'], + r.result['token']['user']['domain']['id']) + self.assertEqual(self.domain['name'], + r.result['token']['user']['domain']['name']) + self.assertEqual(self.project['id'], + r.result['token']['project']['id']) + self.assertEqual(self.project['name'], + r.result['token']['project']['name']) def test_impersonation_token_cannot_create_new_trust(self): ref = self.new_trust_ref( @@ -3950,9 +4007,9 @@ class TestAuthContext(tests.TestCase): self.auth_context = auth.controllers.AuthContext() def test_pick_lowest_expires_at(self): - expires_at_1 = timeutils.isotime(timeutils.utcnow()) - expires_at_2 = timeutils.isotime(timeutils.utcnow() + - datetime.timedelta(seconds=10)) + expires_at_1 = utils.isotime(timeutils.utcnow()) + expires_at_2 = utils.isotime(timeutils.utcnow() + + datetime.timedelta(seconds=10)) # make sure auth_context picks the lowest value self.auth_context['expires_at'] = expires_at_1 self.auth_context['expires_at'] = expires_at_2 @@ -4113,7 +4170,7 @@ class TestFernetTokenProvider(test_v3.RestfulTestCase): trustor_user_id=self.user_id, trustee_user_id=trustee_user['id'], project_id=self.project_id, - impersonation=True, + impersonation=False, role_ids=[self.role_id]) # Create a trust @@ -4123,9 +4180,7 @@ class TestFernetTokenProvider(test_v3.RestfulTestCase): def config_overrides(self): super(TestFernetTokenProvider, self).config_overrides() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.fernet.Provider') + self.config_fixture.config(group='token', provider='fernet') def test_validate_unscoped_token(self): unscoped_token = self._get_unscoped_token() @@ -4135,7 +4190,7 @@ class TestFernetTokenProvider(test_v3.RestfulTestCase): unscoped_token = self._get_unscoped_token() tampered_token = (unscoped_token[:50] + uuid.uuid4().hex + unscoped_token[50 + 32:]) - self._validate_token(tampered_token, expected_status=401) + self._validate_token(tampered_token, expected_status=404) def test_revoke_unscoped_token(self): unscoped_token = self._get_unscoped_token() @@ -4215,7 +4270,7 @@ class TestFernetTokenProvider(test_v3.RestfulTestCase): project_scoped_token = self._get_project_scoped_token() tampered_token = (project_scoped_token[:50] + uuid.uuid4().hex + project_scoped_token[50 + 32:]) - self._validate_token(tampered_token, expected_status=401) + self._validate_token(tampered_token, expected_status=404) def test_revoke_project_scoped_token(self): project_scoped_token = self._get_project_scoped_token() @@ -4323,7 +4378,7 @@ class TestFernetTokenProvider(test_v3.RestfulTestCase): # Get a trust scoped token tampered_token = (trust_scoped_token[:50] + uuid.uuid4().hex + trust_scoped_token[50 + 32:]) - self._validate_token(tampered_token, expected_status=401) + self._validate_token(tampered_token, expected_status=404) def test_revoke_trust_scoped_token(self): trustee_user, trust = self._create_trust() @@ -4454,9 +4509,7 @@ class TestAuthFernetTokenProvider(TestAuth): def config_overrides(self): super(TestAuthFernetTokenProvider, self).config_overrides() - self.config_fixture.config( - group='token', - provider='keystone.token.providers.fernet.Provider') + self.config_fixture.config(group='token', provider='fernet') def test_verify_with_bound_token(self): self.config_fixture.config(group='token', bind='kerberos') diff --git a/keystone-moon/keystone/tests/unit/test_v3_catalog.py b/keystone-moon/keystone/tests/unit/test_v3_catalog.py index d231b2e1..f96b2a12 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_catalog.py +++ b/keystone-moon/keystone/tests/unit/test_v3_catalog.py @@ -15,6 +15,8 @@ import copy import uuid +from testtools import matchers + from keystone import catalog from keystone.tests import unit as tests from keystone.tests.unit.ksfixtures import database @@ -154,7 +156,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): ref2 = self.new_region_ref() del ref1['description'] - del ref2['description'] + ref2['description'] = None resp1 = self.post( '/regions', @@ -224,6 +226,39 @@ class CatalogTestCase(test_v3.RestfulTestCase): body={'region': region}) self.assertValidRegionResponse(r, region) + def test_update_region_without_description_keeps_original(self): + """Call ``PATCH /regions/{region_id}``.""" + region_ref = self.new_region_ref() + + resp = self.post('/regions', body={'region': region_ref}, + expected_status=201) + + region_updates = { + # update with something that's not the description + 'parent_region_id': self.region_id, + } + resp = self.patch('/regions/%s' % region_ref['id'], + body={'region': region_updates}, + expected_status=200) + + # NOTE(dstanek): Keystone should keep the original description. + self.assertEqual(region_ref['description'], + resp.result['region']['description']) + + def test_update_region_with_null_description(self): + """Call ``PATCH /regions/{region_id}``.""" + region = self.new_region_ref() + del region['id'] + region['description'] = None + r = self.patch('/regions/%(region_id)s' % { + 'region_id': self.region_id}, + body={'region': region}) + + # NOTE(dstanek): Keystone should turn the provided None value into + # an empty string before storing in the backend. + region['description'] = '' + self.assertValidRegionResponse(r, region) + def test_delete_region(self): """Call ``DELETE /regions/{region_id}``.""" @@ -379,6 +414,133 @@ class CatalogTestCase(test_v3.RestfulTestCase): r = self.get('/endpoints') self.assertValidEndpointListResponse(r, ref=self.endpoint) + def _create_random_endpoint(self, interface='public', + parent_region_id=None): + region = self._create_region_with_parent_id( + parent_id=parent_region_id) + service = self._create_random_service() + ref = self.new_endpoint_ref( + service_id=service['id'], + interface=interface, + region_id=region.result['region']['id']) + + response = self.post( + '/endpoints', + body={'endpoint': ref}) + return response.json['endpoint'] + + def test_list_endpoints_filtered_by_interface(self): + """Call ``GET /endpoints?interface={interface}``.""" + ref = self._create_random_endpoint(interface='internal') + + response = self.get('/endpoints?interface=%s' % ref['interface']) + self.assertValidEndpointListResponse(response, ref=ref) + + for endpoint in response.json['endpoints']: + self.assertEqual(ref['interface'], endpoint['interface']) + + def test_list_endpoints_filtered_by_service_id(self): + """Call ``GET /endpoints?service_id={service_id}``.""" + ref = self._create_random_endpoint() + + response = self.get('/endpoints?service_id=%s' % ref['service_id']) + self.assertValidEndpointListResponse(response, ref=ref) + + for endpoint in response.json['endpoints']: + self.assertEqual(ref['service_id'], endpoint['service_id']) + + def test_list_endpoints_filtered_by_region_id(self): + """Call ``GET /endpoints?region_id={region_id}``.""" + ref = self._create_random_endpoint() + + response = self.get('/endpoints?region_id=%s' % ref['region_id']) + self.assertValidEndpointListResponse(response, ref=ref) + + for endpoint in response.json['endpoints']: + self.assertEqual(ref['region_id'], endpoint['region_id']) + + def test_list_endpoints_filtered_by_parent_region_id(self): + """Call ``GET /endpoints?region_id={region_id}``. + + Ensure passing the parent_region_id as filter returns an + empty list. + + """ + parent_region = self._create_region_with_parent_id() + parent_region_id = parent_region.result['region']['id'] + self._create_random_endpoint(parent_region_id=parent_region_id) + + response = self.get('/endpoints?region_id=%s' % parent_region_id) + self.assertEqual(0, len(response.json['endpoints'])) + + def test_list_endpoints_with_multiple_filters(self): + """Call ``GET /endpoints?interface={interface}...``. + + Ensure passing different combinations of interface, region_id and + service_id as filters will return the correct result. + + """ + # interface and region_id specified + ref = self._create_random_endpoint(interface='internal') + response = self.get('/endpoints?interface=%s®ion_id=%s' % + (ref['interface'], ref['region_id'])) + self.assertValidEndpointListResponse(response, ref=ref) + + for endpoint in response.json['endpoints']: + self.assertEqual(ref['interface'], endpoint['interface']) + self.assertEqual(ref['region_id'], endpoint['region_id']) + + # interface and service_id specified + ref = self._create_random_endpoint(interface='internal') + response = self.get('/endpoints?interface=%s&service_id=%s' % + (ref['interface'], ref['service_id'])) + self.assertValidEndpointListResponse(response, ref=ref) + + for endpoint in response.json['endpoints']: + self.assertEqual(ref['interface'], endpoint['interface']) + self.assertEqual(ref['service_id'], endpoint['service_id']) + + # region_id and service_id specified + ref = self._create_random_endpoint(interface='internal') + response = self.get('/endpoints?region_id=%s&service_id=%s' % + (ref['region_id'], ref['service_id'])) + self.assertValidEndpointListResponse(response, ref=ref) + + for endpoint in response.json['endpoints']: + self.assertEqual(ref['region_id'], endpoint['region_id']) + self.assertEqual(ref['service_id'], endpoint['service_id']) + + # interface, region_id and service_id specified + ref = self._create_random_endpoint(interface='internal') + response = self.get(('/endpoints?interface=%s®ion_id=%s' + '&service_id=%s') % + (ref['interface'], ref['region_id'], + ref['service_id'])) + self.assertValidEndpointListResponse(response, ref=ref) + + for endpoint in response.json['endpoints']: + self.assertEqual(ref['interface'], endpoint['interface']) + self.assertEqual(ref['region_id'], endpoint['region_id']) + self.assertEqual(ref['service_id'], endpoint['service_id']) + + def test_list_endpoints_with_random_filter_values(self): + """Call ``GET /endpoints?interface={interface}...``. + + Ensure passing random values for: interface, region_id and + service_id will return an empty list. + + """ + self._create_random_endpoint(interface='internal') + + response = self.get('/endpoints?interface=%s' % uuid.uuid4().hex) + self.assertEqual(0, len(response.json['endpoints'])) + + response = self.get('/endpoints?region_id=%s' % uuid.uuid4().hex) + self.assertEqual(0, len(response.json['endpoints'])) + + response = self.get('/endpoints?service_id=%s' % uuid.uuid4().hex) + self.assertEqual(0, len(response.json['endpoints'])) + def test_create_endpoint_no_enabled(self): """Call ``POST /endpoints``.""" ref = self.new_endpoint_ref(service_id=self.service_id) @@ -582,6 +744,62 @@ class CatalogTestCase(test_v3.RestfulTestCase): self.assertEqual(endpoint_v2['region'], endpoint_v3['region_id']) + def test_deleting_endpoint_with_space_in_url(self): + # create a v3 endpoint ref + ref = self.new_endpoint_ref(service_id=self.service['id']) + + # add a space to all urls (intentional "i d" to test bug) + url_with_space = "http://127.0.0.1:8774 /v1.1/\$(tenant_i d)s" + ref['publicurl'] = url_with_space + ref['internalurl'] = url_with_space + ref['adminurl'] = url_with_space + ref['url'] = url_with_space + + # add the endpoint to the database + self.catalog_api.create_endpoint(ref['id'], ref) + + # delete the endpoint + self.delete('/endpoints/%s' % ref['id']) + + # make sure it's deleted (GET should return 404) + self.get('/endpoints/%s' % ref['id'], expected_status=404) + + def test_endpoint_create_with_valid_url(self): + """Create endpoint with valid url should be tested,too.""" + # list one valid url is enough, no need to list too much + valid_url = 'http://127.0.0.1:8774/v1.1/$(tenant_id)s' + + ref = self.new_endpoint_ref(self.service_id) + ref['url'] = valid_url + self.post('/endpoints', + body={'endpoint': ref}, + expected_status=201) + + def test_endpoint_create_with_invalid_url(self): + """Test the invalid cases: substitutions is not exactly right. + """ + invalid_urls = [ + # using a substitution that is not whitelisted - KeyError + 'http://127.0.0.1:8774/v1.1/$(nonexistent)s', + + # invalid formatting - ValueError + 'http://127.0.0.1:8774/v1.1/$(tenant_id)', + 'http://127.0.0.1:8774/v1.1/$(tenant_id)t', + 'http://127.0.0.1:8774/v1.1/$(tenant_id', + + # invalid type specifier - TypeError + # admin_url is a string not an int + 'http://127.0.0.1:8774/v1.1/$(admin_url)d', + ] + + ref = self.new_endpoint_ref(self.service_id) + + for invalid_url in invalid_urls: + ref['url'] = invalid_url + self.post('/endpoints', + body={'endpoint': ref}, + expected_status=400) + class TestCatalogAPISQL(tests.TestCase): """Tests for the catalog Manager against the SQL backend. @@ -602,9 +820,7 @@ class TestCatalogAPISQL(tests.TestCase): def config_overrides(self): super(TestCatalogAPISQL, self).config_overrides() - self.config_fixture.config( - group='catalog', - driver='keystone.catalog.backends.sql.Catalog') + self.config_fixture.config(group='catalog', driver='sql') def new_endpoint_ref(self, service_id): return { @@ -643,6 +859,20 @@ class TestCatalogAPISQL(tests.TestCase): # all three appear in the backend self.assertEqual(3, len(self.catalog_api.list_endpoints())) + # create another valid endpoint - tenant_id will be replaced + ref = self.new_endpoint_ref(self.service_id) + ref['url'] = 'http://keystone/%(tenant_id)s' + self.catalog_api.create_endpoint(ref['id'], ref) + + # there are two valid endpoints, positive check + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) + self.assertThat(catalog[0]['endpoints'], matchers.HasLength(2)) + + # If the URL has no 'tenant_id' to substitute, we will skip the + # endpoint which contains this kind of URL, negative check. + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id=None) + self.assertThat(catalog[0]['endpoints'], matchers.HasLength(1)) + def test_get_catalog_always_returns_service_name(self): user_id = uuid.uuid4().hex tenant_id = uuid.uuid4().hex @@ -691,9 +921,7 @@ class TestCatalogAPISQLRegions(tests.TestCase): def config_overrides(self): super(TestCatalogAPISQLRegions, self).config_overrides() - self.config_fixture.config( - group='catalog', - driver='keystone.catalog.backends.sql.Catalog') + self.config_fixture.config(group='catalog', driver='sql') def new_endpoint_ref(self, service_id): return { diff --git a/keystone-moon/keystone/tests/unit/test_v3_controller.py b/keystone-moon/keystone/tests/unit/test_v3_controller.py index 3ac4ba5a..eef64a82 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_controller.py +++ b/keystone-moon/keystone/tests/unit/test_v3_controller.py @@ -15,6 +15,7 @@ import uuid import six +from six.moves import range from testtools import matchers from keystone.common import controller diff --git a/keystone-moon/keystone/tests/unit/test_v3_credential.py b/keystone-moon/keystone/tests/unit/test_v3_credential.py index d792b216..f8f6d35b 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_credential.py +++ b/keystone-moon/keystone/tests/unit/test_v3_credential.py @@ -18,6 +18,7 @@ import uuid from keystoneclient.contrib.ec2 import utils as ec2_utils from oslo_config import cfg +from testtools import matchers from keystone import exception from keystone.tests.unit import test_v3 @@ -375,14 +376,17 @@ class TestCredentialEc2(CredentialBaseTestCase): self.assertIsNone(ec2_cred['trust_id']) self._validate_signature(access=ec2_cred['access'], secret=ec2_cred['secret']) - - return ec2_cred + uri = '/'.join([self._get_ec2_cred_uri(), ec2_cred['access']]) + self.assertThat(ec2_cred['links']['self'], + matchers.EndsWith(uri)) def test_ec2_get_credential(self): ec2_cred = self._get_ec2_cred() uri = '/'.join([self._get_ec2_cred_uri(), ec2_cred['access']]) r = self.get(uri) self.assertDictEqual(ec2_cred, r.result['credential']) + self.assertThat(ec2_cred['links']['self'], + matchers.EndsWith(uri)) def test_ec2_list_credentials(self): """Test ec2 credential listing.""" @@ -391,6 +395,8 @@ class TestCredentialEc2(CredentialBaseTestCase): r = self.get(uri) cred_list = r.result['credentials'] self.assertEqual(1, len(cred_list)) + self.assertThat(r.result['links']['self'], + matchers.EndsWith(uri)) def test_ec2_delete_credential(self): """Test ec2 credential deletion.""" diff --git a/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py b/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py index 437fb155..4daeff4d 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py +++ b/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py @@ -17,13 +17,7 @@ from testtools import matchers from keystone.tests.unit import test_v3 -class TestExtensionCase(test_v3.RestfulTestCase): - - EXTENSION_NAME = 'endpoint_policy' - EXTENSION_TO_ADD = 'endpoint_policy_extension' - - -class EndpointPolicyTestCase(TestExtensionCase): +class EndpointPolicyTestCase(test_v3.RestfulTestCase): """Test endpoint policy CRUD. In general, the controller layer of the endpoint policy extension is really @@ -203,7 +197,7 @@ class EndpointPolicyTestCase(TestExtensionCase): self.head(url, expected_status=404) -class JsonHomeTests(TestExtensionCase, test_v3.JsonHomeTestMixin): +class JsonHomeTests(test_v3.JsonHomeTestMixin): EXTENSION_LOCATION = ('http://docs.openstack.org/api/openstack-identity/3/' 'ext/OS-ENDPOINT-POLICY/1.0/rel') PARAM_LOCATION = 'http://docs.openstack.org/api/openstack-identity/3/param' diff --git a/keystone-moon/keystone/tests/unit/test_v3_federation.py b/keystone-moon/keystone/tests/unit/test_v3_federation.py index 3b6f4d8b..e646bc0a 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_federation.py +++ b/keystone-moon/keystone/tests/unit/test_v3_federation.py @@ -13,26 +13,27 @@ import os import random import subprocess +from testtools import matchers import uuid +import fixtures from lxml import etree import mock from oslo_config import cfg from oslo_log import log -from oslo_serialization import jsonutils +from oslo_utils import importutils from oslotest import mockpatch import saml2 from saml2 import saml from saml2 import sigver -from six.moves import urllib -import xmldsig +from six.moves import range, urllib, zip +xmldsig = importutils.try_import("saml2.xmldsig") +if not xmldsig: + xmldsig = importutils.try_import("xmldsig") from keystone.auth import controllers as auth_controllers -from keystone.auth.plugins import mapped -from keystone.contrib import federation from keystone.contrib.federation import controllers as federation_controllers from keystone.contrib.federation import idp as keystone_idp -from keystone.contrib.federation import utils as mapping_utils from keystone import exception from keystone import notifications from keystone.tests.unit import core @@ -68,7 +69,7 @@ class FederatedSetupMixin(object): USER = 'user@ORGANIZATION' ASSERTION_PREFIX = 'PREFIX_' IDP_WITH_REMOTE = 'ORG_IDP_REMOTE' - REMOTE_ID = 'entityID_IDP' + REMOTE_IDS = ['entityID_IDP1', 'entityID_IDP2'] REMOTE_ID_ATTR = uuid.uuid4().hex UNSCOPED_V3_SAML2_REQ = { @@ -108,14 +109,14 @@ class FederatedSetupMixin(object): self.assertEqual(token_projects, projects_ref) def _check_scoped_token_attributes(self, token): - def xor_project_domain(iterable): - return sum(('project' in iterable, 'domain' in iterable)) % 2 + def xor_project_domain(token_keys): + return sum(('project' in token_keys, 'domain' in token_keys)) % 2 for obj in ('user', 'catalog', 'expires_at', 'issued_at', 'methods', 'roles'): self.assertIn(obj, token) # Check for either project or domain - if not xor_project_domain(token.keys()): + if not xor_project_domain(list(token.keys())): raise AssertionError("You must specify either" "project or domain.") @@ -123,6 +124,10 @@ class FederatedSetupMixin(object): os_federation = token['user']['OS-FEDERATION'] self.assertEqual(self.IDP, os_federation['identity_provider']['id']) self.assertEqual(self.PROTOCOL, os_federation['protocol']['id']) + self.assertListEqual(sorted(['groups', + 'identity_provider', + 'protocol']), + sorted(os_federation.keys())) def _issue_unscoped_token(self, idp=None, @@ -327,7 +332,8 @@ class FederatedSetupMixin(object): }, { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } } ], @@ -336,6 +342,9 @@ class FederatedSetupMixin(object): 'type': 'UserName' }, { + 'type': 'Email', + }, + { 'type': 'orgPersonType', 'any_one_of': [ 'Employee' @@ -352,7 +361,8 @@ class FederatedSetupMixin(object): }, { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } } ], @@ -361,6 +371,9 @@ class FederatedSetupMixin(object): 'type': self.ASSERTION_PREFIX + 'UserName' }, { + 'type': self.ASSERTION_PREFIX + 'Email', + }, + { 'type': self.ASSERTION_PREFIX + 'orgPersonType', 'any_one_of': [ 'SuperEmployee' @@ -377,7 +390,8 @@ class FederatedSetupMixin(object): }, { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } } ], @@ -386,6 +400,9 @@ class FederatedSetupMixin(object): 'type': 'UserName' }, { + 'type': 'Email' + }, + { 'type': 'orgPersonType', 'any_one_of': [ 'Customer' @@ -413,7 +430,8 @@ class FederatedSetupMixin(object): { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } } ], @@ -422,6 +440,9 @@ class FederatedSetupMixin(object): 'type': 'UserName' }, { + 'type': 'Email' + }, + { 'type': 'orgPersonType', 'any_one_of': [ 'Admin', @@ -444,7 +465,8 @@ class FederatedSetupMixin(object): }, { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } } ], @@ -453,6 +475,9 @@ class FederatedSetupMixin(object): 'type': 'UserName', }, { + 'type': 'Email', + }, + { 'type': 'FirstName', 'any_one_of': [ 'Jill' @@ -475,7 +500,8 @@ class FederatedSetupMixin(object): }, { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } } ], @@ -485,6 +511,9 @@ class FederatedSetupMixin(object): }, { 'type': 'Email', + }, + { + 'type': 'Email', 'any_one_of': [ 'testacct@example.com' ] @@ -502,7 +531,8 @@ class FederatedSetupMixin(object): "local": [ { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } }, { @@ -519,6 +549,9 @@ class FederatedSetupMixin(object): 'type': 'UserName', }, { + 'type': 'Email', + }, + { "type": "orgPersonType", "any_one_of": [ "CEO", @@ -531,7 +564,8 @@ class FederatedSetupMixin(object): "local": [ { 'user': { - 'name': '{0}' + 'name': '{0}', + 'id': '{1}' } }, { @@ -548,6 +582,9 @@ class FederatedSetupMixin(object): "type": "UserName", }, { + "type": "Email", + }, + { "type": "orgPersonType", "any_one_of": [ "Managers" @@ -559,7 +596,8 @@ class FederatedSetupMixin(object): "local": [ { "user": { - "name": "{0}" + "name": "{0}", + "id": "{1}" } }, { @@ -576,6 +614,9 @@ class FederatedSetupMixin(object): "type": "UserName", }, { + "type": "Email", + }, + { "type": "UserName", "any_one_of": [ "IamTester" @@ -639,7 +680,7 @@ class FederatedSetupMixin(object): self.idp) # Add IDP with remote self.idp_with_remote = self.idp_ref(id=self.IDP_WITH_REMOTE) - self.idp_with_remote['remote_id'] = self.REMOTE_ID + self.idp_with_remote['remote_ids'] = self.REMOTE_IDS self.federation_api.create_idp(self.idp_with_remote['id'], self.idp_with_remote) # Add a mapping @@ -793,28 +834,137 @@ class FederatedIdentityProviderTests(FederationTests): return r def test_create_idp(self): - """Creates the IdentityProvider entity.""" + """Creates the IdentityProvider entity associated to remote_ids.""" - keys_to_check = self.idp_keys - body = self._http_idp_input() + keys_to_check = list(self.idp_keys) + body = self.default_body.copy() + body['description'] = uuid.uuid4().hex resp = self._create_default_idp(body=body) self.assertValidResponse(resp, 'identity_provider', dummy_validator, keys_to_check=keys_to_check, ref=body) def test_create_idp_remote(self): - """Creates the IdentityProvider entity associated to a remote_id.""" + """Creates the IdentityProvider entity associated to remote_ids.""" keys_to_check = list(self.idp_keys) - keys_to_check.append('remote_id') + keys_to_check.append('remote_ids') body = self.default_body.copy() body['description'] = uuid.uuid4().hex - body['remote_id'] = uuid.uuid4().hex + body['remote_ids'] = [uuid.uuid4().hex, + uuid.uuid4().hex, + uuid.uuid4().hex] resp = self._create_default_idp(body=body) self.assertValidResponse(resp, 'identity_provider', dummy_validator, keys_to_check=keys_to_check, ref=body) + def test_create_idp_remote_repeated(self): + """Creates two IdentityProvider entities with some remote_ids + + A remote_id is the same for both so the second IdP is not + created because of the uniqueness of the remote_ids + + Expect HTTP 409 code for the latter call. + + """ + + body = self.default_body.copy() + repeated_remote_id = uuid.uuid4().hex + body['remote_ids'] = [uuid.uuid4().hex, + uuid.uuid4().hex, + uuid.uuid4().hex, + repeated_remote_id] + self._create_default_idp(body=body) + + url = self.base_url(suffix=uuid.uuid4().hex) + body['remote_ids'] = [uuid.uuid4().hex, + repeated_remote_id] + self.put(url, body={'identity_provider': body}, + expected_status=409) + + def test_create_idp_remote_empty(self): + """Creates an IdP with empty remote_ids.""" + + keys_to_check = list(self.idp_keys) + keys_to_check.append('remote_ids') + body = self.default_body.copy() + body['description'] = uuid.uuid4().hex + body['remote_ids'] = [] + resp = self._create_default_idp(body=body) + self.assertValidResponse(resp, 'identity_provider', dummy_validator, + keys_to_check=keys_to_check, + ref=body) + + def test_create_idp_remote_none(self): + """Creates an IdP with a None remote_ids.""" + + keys_to_check = list(self.idp_keys) + keys_to_check.append('remote_ids') + body = self.default_body.copy() + body['description'] = uuid.uuid4().hex + body['remote_ids'] = None + resp = self._create_default_idp(body=body) + expected = body.copy() + expected['remote_ids'] = [] + self.assertValidResponse(resp, 'identity_provider', dummy_validator, + keys_to_check=keys_to_check, + ref=expected) + + def test_update_idp_remote_ids(self): + """Update IdP's remote_ids parameter.""" + body = self.default_body.copy() + body['remote_ids'] = [uuid.uuid4().hex] + default_resp = self._create_default_idp(body=body) + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp.get('id') + url = self.base_url(suffix=idp_id) + self.assertIsNotNone(idp_id) + + body['remote_ids'] = [uuid.uuid4().hex, uuid.uuid4().hex] + + body = {'identity_provider': body} + resp = self.patch(url, body=body) + updated_idp = self._fetch_attribute_from_response(resp, + 'identity_provider') + body = body['identity_provider'] + self.assertEqual(sorted(body['remote_ids']), + sorted(updated_idp.get('remote_ids'))) + + resp = self.get(url) + returned_idp = self._fetch_attribute_from_response(resp, + 'identity_provider') + self.assertEqual(sorted(body['remote_ids']), + sorted(returned_idp.get('remote_ids'))) + + def test_update_idp_clean_remote_ids(self): + """Update IdP's remote_ids parameter with an empty list.""" + body = self.default_body.copy() + body['remote_ids'] = [uuid.uuid4().hex] + default_resp = self._create_default_idp(body=body) + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp.get('id') + url = self.base_url(suffix=idp_id) + self.assertIsNotNone(idp_id) + + body['remote_ids'] = [] + + body = {'identity_provider': body} + resp = self.patch(url, body=body) + updated_idp = self._fetch_attribute_from_response(resp, + 'identity_provider') + body = body['identity_provider'] + self.assertEqual(sorted(body['remote_ids']), + sorted(updated_idp.get('remote_ids'))) + + resp = self.get(url) + returned_idp = self._fetch_attribute_from_response(resp, + 'identity_provider') + self.assertEqual(sorted(body['remote_ids']), + sorted(returned_idp.get('remote_ids'))) + def test_list_idps(self, iterations=5): """Lists all available IdentityProviders. @@ -899,6 +1049,33 @@ class FederatedIdentityProviderTests(FederationTests): self.delete(url) self.get(url, expected_status=404) + def test_delete_idp_also_deletes_assigned_protocols(self): + """Deleting an IdP will delete its assigned protocol.""" + + # create default IdP + default_resp = self._create_default_idp() + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp['id'] + protocol_id = uuid.uuid4().hex + + url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s') + idp_url = self.base_url(suffix=idp_id) + + # assign protocol to IdP + kwargs = {'expected_status': 201} + resp, idp_id, proto = self._assign_protocol_to_idp( + url=url, + idp_id=idp_id, + proto=protocol_id, + **kwargs) + + # removing IdP will remove the assigned protocol as well + self.assertEqual(1, len(self.federation_api.list_protocols(idp_id))) + self.delete(idp_url) + self.get(idp_url, expected_status=404) + self.assertEqual(0, len(self.federation_api.list_protocols(idp_id))) + def test_delete_nonexisting_idp(self): """Delete nonexisting IdP. @@ -918,7 +1095,7 @@ class FederatedIdentityProviderTests(FederationTests): self.assertIsNotNone(idp_id) _enabled = not default_idp.get('enabled') - body = {'remote_id': uuid.uuid4().hex, + body = {'remote_ids': [uuid.uuid4().hex, uuid.uuid4().hex], 'description': uuid.uuid4().hex, 'enabled': _enabled} @@ -928,13 +1105,21 @@ class FederatedIdentityProviderTests(FederationTests): 'identity_provider') body = body['identity_provider'] for key in body.keys(): - self.assertEqual(body[key], updated_idp.get(key)) + if isinstance(body[key], list): + self.assertEqual(sorted(body[key]), + sorted(updated_idp.get(key))) + else: + self.assertEqual(body[key], updated_idp.get(key)) resp = self.get(url) updated_idp = self._fetch_attribute_from_response(resp, 'identity_provider') for key in body.keys(): - self.assertEqual(body[key], updated_idp.get(key)) + if isinstance(body[key], list): + self.assertEqual(sorted(body[key]), + sorted(updated_idp.get(key))) + else: + self.assertEqual(body[key], updated_idp.get(key)) def test_update_idp_immutable_attributes(self): """Update IdP's immutable parameters. @@ -1126,7 +1311,7 @@ class MappingCRUDTests(FederationTests): self.assertIsNotNone(entity.get('id')) self.assertIsNotNone(entity.get('rules')) if ref: - self.assertEqual(jsonutils.loads(entity['rules']), ref['rules']) + self.assertEqual(entity['rules'], ref['rules']) return entity def _create_default_mapping_entry(self): @@ -1262,594 +1447,11 @@ class MappingCRUDTests(FederationTests): self.put(url, expected_status=400, body={'mapping': mapping}) -class MappingRuleEngineTests(FederationTests): - """A class for testing the mapping rule engine.""" - - def assertValidMappedUserObject(self, mapped_properties, - user_type='ephemeral', - domain_id=None): - """Check whether mapped properties object has 'user' within. - - According to today's rules, RuleProcessor does not have to issue user's - id or name. What's actually required is user's type and for ephemeral - users that would be service domain named 'Federated'. - """ - self.assertIn('user', mapped_properties, - message='Missing user object in mapped properties') - user = mapped_properties['user'] - self.assertIn('type', user) - self.assertEqual(user_type, user['type']) - self.assertIn('domain', user) - domain = user['domain'] - domain_name_or_id = domain.get('id') or domain.get('name') - domain_ref = domain_id or federation.FEDERATED_DOMAIN_KEYWORD - self.assertEqual(domain_ref, domain_name_or_id) - - def test_rule_engine_any_one_of_and_direct_mapping(self): - """Should return user's name and group id EMPLOYEE_GROUP_ID. - - The ADMIN_ASSERTION should successfully have a match in MAPPING_LARGE. - They will test the case where `any_one_of` is valid, and there is - a direct mapping for the users name. - - """ - - mapping = mapping_fixtures.MAPPING_LARGE - assertion = mapping_fixtures.ADMIN_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - values = rp.process(assertion) - - fn = assertion.get('FirstName') - ln = assertion.get('LastName') - full_name = '%s %s' % (fn, ln) - group_ids = values.get('group_ids') - user_name = values.get('user', {}).get('name') - - self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) - self.assertEqual(full_name, user_name) - - def test_rule_engine_no_regex_match(self): - """Should deny authorization, the email of the tester won't match. - - This will not match since the email in the assertion will fail - the regex test. It is set to match any @example.com address. - But the incoming value is set to eviltester@example.org. - RuleProcessor should return list of empty group_ids. - - """ - - mapping = mapping_fixtures.MAPPING_LARGE - assertion = mapping_fixtures.BAD_TESTER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - - self.assertValidMappedUserObject(mapped_properties) - self.assertIsNone(mapped_properties['user'].get('name')) - self.assertListEqual(list(), mapped_properties['group_ids']) - - def test_rule_engine_regex_many_groups(self): - """Should return group CONTRACTOR_GROUP_ID. - - The TESTER_ASSERTION should successfully have a match in - MAPPING_TESTER_REGEX. This will test the case where many groups - are in the assertion, and a regex value is used to try and find - a match. - - """ - - mapping = mapping_fixtures.MAPPING_TESTER_REGEX - assertion = mapping_fixtures.TESTER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - values = rp.process(assertion) - - self.assertValidMappedUserObject(values) - user_name = assertion.get('UserName') - group_ids = values.get('group_ids') - name = values.get('user', {}).get('name') - - self.assertEqual(user_name, name) - self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) - - def test_rule_engine_any_one_of_many_rules(self): - """Should return group CONTRACTOR_GROUP_ID. - - The CONTRACTOR_ASSERTION should successfully have a match in - MAPPING_SMALL. This will test the case where many rules - must be matched, including an `any_one_of`, and a direct - mapping. - - """ - - mapping = mapping_fixtures.MAPPING_SMALL - assertion = mapping_fixtures.CONTRACTOR_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - values = rp.process(assertion) - - self.assertValidMappedUserObject(values) - user_name = assertion.get('UserName') - group_ids = values.get('group_ids') - name = values.get('user', {}).get('name') - - self.assertEqual(user_name, name) - self.assertIn(mapping_fixtures.CONTRACTOR_GROUP_ID, group_ids) - - def test_rule_engine_not_any_of_and_direct_mapping(self): - """Should return user's name and email. - - The CUSTOMER_ASSERTION should successfully have a match in - MAPPING_LARGE. This will test the case where a requirement - has `not_any_of`, and direct mapping to a username, no group. - - """ - - mapping = mapping_fixtures.MAPPING_LARGE - assertion = mapping_fixtures.CUSTOMER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - values = rp.process(assertion) - - self.assertValidMappedUserObject(values) - user_name = assertion.get('UserName') - group_ids = values.get('group_ids') - name = values.get('user', {}).get('name') - - self.assertEqual(user_name, name) - self.assertEqual([], group_ids,) - - def test_rule_engine_not_any_of_many_rules(self): - """Should return group EMPLOYEE_GROUP_ID. - - The EMPLOYEE_ASSERTION should successfully have a match in - MAPPING_SMALL. This will test the case where many remote - rules must be matched, including a `not_any_of`. - - """ - - mapping = mapping_fixtures.MAPPING_SMALL - assertion = mapping_fixtures.EMPLOYEE_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - values = rp.process(assertion) - - self.assertValidMappedUserObject(values) - user_name = assertion.get('UserName') - group_ids = values.get('group_ids') - name = values.get('user', {}).get('name') - - self.assertEqual(user_name, name) - self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) - - def test_rule_engine_not_any_of_regex_verify_pass(self): - """Should return group DEVELOPER_GROUP_ID. - - The DEVELOPER_ASSERTION should successfully have a match in - MAPPING_DEVELOPER_REGEX. This will test the case where many - remote rules must be matched, including a `not_any_of`, with - regex set to True. - - """ - - mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX - assertion = mapping_fixtures.DEVELOPER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - values = rp.process(assertion) - - self.assertValidMappedUserObject(values) - user_name = assertion.get('UserName') - group_ids = values.get('group_ids') - name = values.get('user', {}).get('name') - - self.assertEqual(user_name, name) - self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) - - def test_rule_engine_not_any_of_regex_verify_fail(self): - """Should deny authorization. - - The email in the assertion will fail the regex test. - It is set to reject any @example.org address, but the - incoming value is set to evildeveloper@example.org. - RuleProcessor should return list of empty group_ids. - - """ - - mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX - assertion = mapping_fixtures.BAD_DEVELOPER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - - self.assertValidMappedUserObject(mapped_properties) - self.assertIsNone(mapped_properties['user'].get('name')) - self.assertListEqual(list(), mapped_properties['group_ids']) - - def _rule_engine_regex_match_and_many_groups(self, assertion): - """Should return group DEVELOPER_GROUP_ID and TESTER_GROUP_ID. - - A helper function injecting assertion passed as an argument. - Expect DEVELOPER_GROUP_ID and TESTER_GROUP_ID in the results. - - """ - - mapping = mapping_fixtures.MAPPING_LARGE - rp = mapping_utils.RuleProcessor(mapping['rules']) - values = rp.process(assertion) - - user_name = assertion.get('UserName') - group_ids = values.get('group_ids') - name = values.get('user', {}).get('name') - - self.assertValidMappedUserObject(values) - self.assertEqual(user_name, name) - self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) - self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) - - def test_rule_engine_regex_match_and_many_groups(self): - """Should return group DEVELOPER_GROUP_ID and TESTER_GROUP_ID. - - The TESTER_ASSERTION should successfully have a match in - MAPPING_LARGE. This will test a successful regex match - for an `any_one_of` evaluation type, and will have many - groups returned. - - """ - self._rule_engine_regex_match_and_many_groups( - mapping_fixtures.TESTER_ASSERTION) - - def test_rule_engine_discards_nonstring_objects(self): - """Check whether RuleProcessor discards non string objects. - - Despite the fact that assertion is malformed and contains - non string objects, RuleProcessor should correctly discard them and - successfully have a match in MAPPING_LARGE. - - """ - self._rule_engine_regex_match_and_many_groups( - mapping_fixtures.MALFORMED_TESTER_ASSERTION) - - def test_rule_engine_fails_after_discarding_nonstring(self): - """Check whether RuleProcessor discards non string objects. - - Expect RuleProcessor to discard non string object, which - is required for a correct rule match. RuleProcessor will result with - empty list of groups. - - """ - mapping = mapping_fixtures.MAPPING_SMALL - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.CONTRACTOR_MALFORMED_ASSERTION - mapped_properties = rp.process(assertion) - self.assertValidMappedUserObject(mapped_properties) - self.assertIsNone(mapped_properties['user'].get('name')) - self.assertListEqual(list(), mapped_properties['group_ids']) - - def test_rule_engine_returns_group_names(self): - """Check whether RuleProcessor returns group names with their domains. - - RuleProcessor should return 'group_names' entry with a list of - dictionaries with two entries 'name' and 'domain' identifying group by - its name and domain. - - """ - mapping = mapping_fixtures.MAPPING_GROUP_NAMES - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.EMPLOYEE_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - reference = { - mapping_fixtures.DEVELOPER_GROUP_NAME: - { - "name": mapping_fixtures.DEVELOPER_GROUP_NAME, - "domain": { - "name": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_NAME - } - }, - mapping_fixtures.TESTER_GROUP_NAME: - { - "name": mapping_fixtures.TESTER_GROUP_NAME, - "domain": { - "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID - } - } - } - for rule in mapped_properties['group_names']: - self.assertDictEqual(reference.get(rule.get('name')), rule) - - def test_rule_engine_whitelist_and_direct_groups_mapping(self): - """Should return user's groups Developer and Contractor. - - The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match - in MAPPING_GROUPS_WHITELIST. It will test the case where 'whitelist' - correctly filters out Manager and only allows Developer and Contractor. - - """ - - mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST - assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - - reference = { - mapping_fixtures.DEVELOPER_GROUP_NAME: - { - "name": mapping_fixtures.DEVELOPER_GROUP_NAME, - "domain": { - "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID - } - }, - mapping_fixtures.CONTRACTOR_GROUP_NAME: - { - "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, - "domain": { - "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID - } - } - } - for rule in mapped_properties['group_names']: - self.assertDictEqual(reference.get(rule.get('name')), rule) - - self.assertEqual('tbo', mapped_properties['user']['name']) - self.assertEqual([], mapped_properties['group_ids']) - - def test_rule_engine_blacklist_and_direct_groups_mapping(self): - """Should return user's group Developer. - - The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match - in MAPPING_GROUPS_BLACKLIST. It will test the case where 'blacklist' - correctly filters out Manager and Developer and only allows Contractor. - - """ - - mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST - assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - - reference = { - mapping_fixtures.CONTRACTOR_GROUP_NAME: - { - "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, - "domain": { - "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID - } - } - } - for rule in mapped_properties['group_names']: - self.assertDictEqual(reference.get(rule.get('name')), rule) - self.assertEqual('tbo', mapped_properties['user']['name']) - self.assertEqual([], mapped_properties['group_ids']) - - def test_rule_engine_blacklist_and_direct_groups_mapping_multiples(self): - """Tests matching multiple values before the blacklist. - - Verifies that the local indexes are correct when matching multiple - remote values for a field when the field occurs before the blacklist - entry in the remote rules. - - """ - - mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MULTIPLES - assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - - reference = { - mapping_fixtures.CONTRACTOR_GROUP_NAME: - { - "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, - "domain": { - "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID - } - } - } - for rule in mapped_properties['group_names']: - self.assertDictEqual(reference.get(rule.get('name')), rule) - self.assertEqual('tbo', mapped_properties['user']['name']) - self.assertEqual([], mapped_properties['group_ids']) - - def test_rule_engine_whitelist_direct_group_mapping_missing_domain(self): - """Test if the local rule is rejected upon missing domain value - - This is a variation with a ``whitelist`` filter. - - """ - mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_MISSING_DOMAIN - assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) - self.assertRaises(exception.ValidationError, rp.process, assertion) - - def test_rule_engine_blacklist_direct_group_mapping_missing_domain(self): - """Test if the local rule is rejected upon missing domain value - - This is a variation with a ``blacklist`` filter. - - """ - mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MISSING_DOMAIN - assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) - self.assertRaises(exception.ValidationError, rp.process, assertion) - - def test_rule_engine_no_groups_allowed(self): - """Should return user mapped to no groups. - - The EMPLOYEE_ASSERTION should successfully have a match - in MAPPING_GROUPS_WHITELIST, but 'whitelist' should filter out - the group values from the assertion and thus map to no groups. - - """ - mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST - assertion = mapping_fixtures.EMPLOYEE_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertListEqual(mapped_properties['group_names'], []) - self.assertListEqual(mapped_properties['group_ids'], []) - self.assertEqual('tbo', mapped_properties['user']['name']) - - def test_mapping_federated_domain_specified(self): - """Test mapping engine when domain 'ephemeral' is explicitely set. - - For that, we use mapping rule MAPPING_EPHEMERAL_USER and assertion - EMPLOYEE_ASSERTION - - """ - mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.EMPLOYEE_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - - def test_create_user_object_with_bad_mapping(self): - """Test if user object is created even with bad mapping. - - User objects will be created by mapping engine always as long as there - is corresponding local rule. This test shows, that even with assertion - where no group names nor ids are matched, but there is 'blind' rule for - mapping user, such object will be created. - - In this test MAPPING_EHPEMERAL_USER expects UserName set to jsmith - whereas value from assertion is 'tbo'. - - """ - mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.CONTRACTOR_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - - self.assertNotIn('id', mapped_properties['user']) - self.assertNotIn('name', mapped_properties['user']) - - def test_set_ephemeral_domain_to_ephemeral_users(self): - """Test auto assigning service domain to ephemeral users. - - Test that ephemeral users will always become members of federated - service domain. The check depends on ``type`` value which must be set - to ``ephemeral`` in case of ephemeral user. - - """ - mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.CONTRACTOR_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - - def test_local_user_local_domain(self): - """Test that local users can have non-service domains assigned.""" - mapping = mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.CONTRACTOR_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject( - mapped_properties, user_type='local', - domain_id=mapping_fixtures.LOCAL_DOMAIN) - - def test_user_identifications_name(self): - """Test varius mapping options and how users are identified. - - This test calls mapped.setup_username() for propagating user object. - - Test plan: - - Check if the user has proper domain ('federated') set - - Check if the user has property type set ('ephemeral') - - Check if user's name is properly mapped from the assertion - - Check if user's id is properly set and equal to name, as it was not - explicitely specified in the mapping. - - """ - mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.CONTRACTOR_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username({}, mapped_properties) - self.assertEqual('jsmith', mapped_properties['user']['id']) - self.assertEqual('jsmith', mapped_properties['user']['name']) - - def test_user_identifications_name_and_federated_domain(self): - """Test varius mapping options and how users are identified. - - This test calls mapped.setup_username() for propagating user object. - - Test plan: - - Check if the user has proper domain ('federated') set - - Check if the user has propert type set ('ephemeral') - - Check if user's name is properly mapped from the assertion - - Check if user's id is properly set and equal to name, as it was not - explicitely specified in the mapping. - - """ - mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.EMPLOYEE_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username({}, mapped_properties) - self.assertEqual('tbo', mapped_properties['user']['name']) - self.assertEqual('tbo', mapped_properties['user']['id']) - - def test_user_identification_id(self): - """Test varius mapping options and how users are identified. - - This test calls mapped.setup_username() for propagating user object. - - Test plan: - - Check if the user has proper domain ('federated') set - - Check if the user has propert type set ('ephemeral') - - Check if user's id is properly mapped from the assertion - - Check if user's name is properly set and equal to id, as it was not - explicitely specified in the mapping. - - """ - mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.ADMIN_ASSERTION - mapped_properties = rp.process(assertion) - context = {'environment': {}} - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username(context, mapped_properties) - self.assertEqual('bob', mapped_properties['user']['name']) - self.assertEqual('bob', mapped_properties['user']['id']) - - def test_user_identification_id_and_name(self): - """Test varius mapping options and how users are identified. - - This test calls mapped.setup_username() for propagating user object. - - Test plan: - - Check if the user has proper domain ('federated') set - - Check if the user has proper type set ('ephemeral') - - Check if user's name is properly mapped from the assertion - - Check if user's id is properly set and and equal to value hardcoded - in the mapping - - """ - mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.CUSTOMER_ASSERTION - mapped_properties = rp.process(assertion) - context = {'environment': {}} - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username(context, mapped_properties) - self.assertEqual('bwilliams', mapped_properties['user']['name']) - self.assertEqual('abc123', mapped_properties['user']['id']) - - class FederatedTokenTests(FederationTests, FederatedSetupMixin): def auth_plugin_config_override(self): methods = ['saml2'] - method_classes = {'saml2': 'keystone.auth.plugins.saml2.Saml2'} - super(FederatedTokenTests, self).auth_plugin_config_override( - methods, **method_classes) + super(FederatedTokenTests, self).auth_plugin_config_override(methods) def setUp(self): super(FederatedTokenTests, self).setUp() @@ -1923,7 +1525,8 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): def test_issue_unscoped_token_with_remote_no_attribute(self): r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, environment={ - self.REMOTE_ID_ATTR: self.REMOTE_ID + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] }) self.assertIsNotNone(r.headers.get('X-Subject-Token')) @@ -1932,7 +1535,18 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): remote_id_attribute=self.REMOTE_ID_ATTR) r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, environment={ - self.REMOTE_ID_ATTR: self.REMOTE_ID + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] + }) + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + + def test_issue_unscoped_token_with_saml2_remote(self): + self.config_fixture.config(group='saml2', + remote_id_attribute=self.REMOTE_ID_ATTR) + r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] }) self.assertIsNotNone(r.headers.get('X-Subject-Token')) @@ -1946,6 +1560,25 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self.REMOTE_ID_ATTR: uuid.uuid4().hex }) + def test_issue_unscoped_token_with_remote_default_overwritten(self): + """Test that protocol remote_id_attribute has higher priority. + + Make sure the parameter stored under ``protocol`` section has higher + priority over parameter from default ``federation`` configuration + section. + + """ + self.config_fixture.config(group='saml2', + remote_id_attribute=self.REMOTE_ID_ATTR) + self.config_fixture.config(group='federation', + remote_id_attribute=uuid.uuid4().hex) + r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: + self.REMOTE_IDS[0] + }) + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + def test_issue_unscoped_token_with_remote_unavailable(self): self.config_fixture.config(group='federation', remote_id_attribute=self.REMOTE_ID_ATTR) @@ -1979,7 +1612,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): context = { 'environment': { 'malformed_object': object(), - 'another_bad_idea': tuple(xrange(10)), + 'another_bad_idea': tuple(range(10)), 'yet_another_bad_param': dict(zip(uuid.uuid4().hex, range(32))) } @@ -2156,6 +1789,44 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self.assertEqual(projects_ref, projects, 'match failed for url %s' % url) + # TODO(samueldmq): Create another test class for role inheritance tests. + # The advantage would be to reduce the complexity of this test class and + # have tests specific to this fuctionality grouped, easing readability and + # maintenability. + def test_list_projects_for_inherited_project_assignment(self): + # Enable os_inherit extension + self.config_fixture.config(group='os_inherit', enabled=True) + + # Create a subproject + subproject_inherited = self.new_project_ref( + domain_id=self.domainD['id'], + parent_id=self.project_inherited['id']) + self.resource_api.create_project(subproject_inherited['id'], + subproject_inherited) + + # Create an inherited role assignment + self.assignment_api.create_grant( + role_id=self.role_employee['id'], + group_id=self.group_employees['id'], + project_id=self.project_inherited['id'], + inherited_to_projects=True) + + # Define expected projects from employee assertion, which contain + # the created subproject + expected_project_ids = [self.project_all['id'], + self.proj_employees['id'], + subproject_inherited['id']] + + # Assert expected projects for both available URLs + for url in ('/OS-FEDERATION/projects', '/auth/projects'): + r = self.get(url, token=self.tokens['EMPLOYEE_ASSERTION']) + project_ids = [project['id'] for project in r.result['projects']] + + self.assertEqual(len(expected_project_ids), len(project_ids)) + for expected_project_id in expected_project_ids: + self.assertIn(expected_project_id, project_ids, + 'Projects match failed for url %s' % url) + def test_list_domains(self): urls = ('/OS-FEDERATION/domains', '/auth/domains') @@ -2325,7 +1996,6 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): "remote": [ { "type": "REMOTE_USER_GROUPS", - "blacklist": ["noblacklist"] } ] } @@ -2333,10 +2003,290 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): } self.federation_api.update_mapping(self.mapping['id'], rules) + def test_empty_blacklist_passess_all_values(self): + """Test a mapping with empty blacklist specified + + Not adding a ``blacklist`` keyword to the mapping rules has the same + effect as adding an empty ``blacklist``. + In both cases, the mapping engine will not discard any groups that are + associated with apache environment variables. + + This test checks scenario where an empty blacklist was specified. + Expected result is to allow any value. + + The test scenario is as follows: + - Create group ``EXISTS`` + - Create group ``NO_EXISTS`` + - Set mapping rules for existing IdP with a blacklist + that passes through as REMOTE_USER_GROUPS + - Issue unscoped token with groups ``EXISTS`` and ``NO_EXISTS`` + assigned + + """ + + domain_id = self.domainA['id'] + domain_name = self.domainA['name'] + + # Add a group "EXISTS" + group_exists = self.new_group_ref(domain_id=domain_id) + group_exists['name'] = 'EXISTS' + group_exists = self.identity_api.create_group(group_exists) + + # Add a group "NO_EXISTS" + group_no_exists = self.new_group_ref(domain_id=domain_id) + group_no_exists['name'] = 'NO_EXISTS' + group_no_exists = self.identity_api.create_group(group_no_exists) + + group_ids = set([group_exists['id'], group_no_exists['id']]) + + rules = { + 'rules': [ + { + "local": [ + { + "user": { + "name": "{0}", + "id": "{0}" + } + } + ], + "remote": [ + { + "type": "REMOTE_USER" + } + ] + }, + { + "local": [ + { + "groups": "{0}", + "domain": {"name": domain_name} + } + ], + "remote": [ + { + "type": "REMOTE_USER_GROUPS", + "blacklist": [] + } + ] + } + ] + } + self.federation_api.update_mapping(self.mapping['id'], rules) + r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') + assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] + self.assertEqual(len(group_ids), len(assigned_group_ids)) + for group in assigned_group_ids: + self.assertIn(group['id'], group_ids) + + def test_not_adding_blacklist_passess_all_values(self): + """Test a mapping without blacklist specified. + + Not adding a ``blacklist`` keyword to the mapping rules has the same + effect as adding an empty ``blacklist``. In both cases all values will + be accepted and passed. + + This test checks scenario where an blacklist was not specified. + Expected result is to allow any value. + + The test scenario is as follows: + - Create group ``EXISTS`` + - Create group ``NO_EXISTS`` + - Set mapping rules for existing IdP with a blacklist + that passes through as REMOTE_USER_GROUPS + - Issue unscoped token with on groups ``EXISTS`` and ``NO_EXISTS`` + assigned + + """ + + domain_id = self.domainA['id'] + domain_name = self.domainA['name'] + + # Add a group "EXISTS" + group_exists = self.new_group_ref(domain_id=domain_id) + group_exists['name'] = 'EXISTS' + group_exists = self.identity_api.create_group(group_exists) + + # Add a group "NO_EXISTS" + group_no_exists = self.new_group_ref(domain_id=domain_id) + group_no_exists['name'] = 'NO_EXISTS' + group_no_exists = self.identity_api.create_group(group_no_exists) + + group_ids = set([group_exists['id'], group_no_exists['id']]) + + rules = { + 'rules': [ + { + "local": [ + { + "user": { + "name": "{0}", + "id": "{0}" + } + } + ], + "remote": [ + { + "type": "REMOTE_USER" + } + ] + }, + { + "local": [ + { + "groups": "{0}", + "domain": {"name": domain_name} + } + ], + "remote": [ + { + "type": "REMOTE_USER_GROUPS", + } + ] + } + ] + } + self.federation_api.update_mapping(self.mapping['id'], rules) + r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') + assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] + self.assertEqual(len(group_ids), len(assigned_group_ids)) + for group in assigned_group_ids: + self.assertIn(group['id'], group_ids) + + def test_empty_whitelist_discards_all_values(self): + """Test that empty whitelist blocks all the values + + Not adding a ``whitelist`` keyword to the mapping value is different + than adding empty whitelist. The former case will simply pass all the + values, whereas the latter would discard all the values. + + This test checks scenario where an empty whitelist was specified. + The expected result is that no groups are matched. + + The test scenario is as follows: + - Create group ``EXISTS`` + - Set mapping rules for existing IdP with an empty whitelist + that whould discard any values from the assertion + - Try issuing unscoped token, expect server to raise + ``exception.MissingGroups`` as no groups were matched and ephemeral + user does not have any group assigned. + + """ + domain_id = self.domainA['id'] + domain_name = self.domainA['name'] + group = self.new_group_ref(domain_id=domain_id) + group['name'] = 'EXISTS' + group = self.identity_api.create_group(group) + rules = { + 'rules': [ + { + "local": [ + { + "user": { + "name": "{0}", + "id": "{0}" + } + } + ], + "remote": [ + { + "type": "REMOTE_USER" + } + ] + }, + { + "local": [ + { + "groups": "{0}", + "domain": {"name": domain_name} + } + ], + "remote": [ + { + "type": "REMOTE_USER_GROUPS", + "whitelist": [] + } + ] + } + ] + } + self.federation_api.update_mapping(self.mapping['id'], rules) + + self.assertRaises(exception.MissingGroups, + self._issue_unscoped_token, + assertion='UNMATCHED_GROUP_ASSERTION') + + def test_not_setting_whitelist_accepts_all_values(self): + """Test that not setting whitelist passes + + Not adding a ``whitelist`` keyword to the mapping value is different + than adding empty whitelist. The former case will simply pass all the + values, whereas the latter would discard all the values. + + This test checks a scenario where a ``whitelist`` was not specified. + Expected result is that no groups are ignored. + + The test scenario is as follows: + - Create group ``EXISTS`` + - Set mapping rules for existing IdP with an empty whitelist + that whould discard any values from the assertion + - Issue an unscoped token and make sure ephemeral user is a member of + two groups. + + """ + domain_id = self.domainA['id'] + domain_name = self.domainA['name'] + + # Add a group "EXISTS" + group_exists = self.new_group_ref(domain_id=domain_id) + group_exists['name'] = 'EXISTS' + group_exists = self.identity_api.create_group(group_exists) + + # Add a group "NO_EXISTS" + group_no_exists = self.new_group_ref(domain_id=domain_id) + group_no_exists['name'] = 'NO_EXISTS' + group_no_exists = self.identity_api.create_group(group_no_exists) + + group_ids = set([group_exists['id'], group_no_exists['id']]) + + rules = { + 'rules': [ + { + "local": [ + { + "user": { + "name": "{0}", + "id": "{0}" + } + } + ], + "remote": [ + { + "type": "REMOTE_USER" + } + ] + }, + { + "local": [ + { + "groups": "{0}", + "domain": {"name": domain_name} + } + ], + "remote": [ + { + "type": "REMOTE_USER_GROUPS", + } + ] + } + ] + } + self.federation_api.update_mapping(self.mapping['id'], rules) r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] - self.assertEqual(1, len(assigned_group_ids)) - self.assertEqual(group['id'], assigned_group_ids[0]['id']) + self.assertEqual(len(group_ids), len(assigned_group_ids)) + for group in assigned_group_ids: + self.assertIn(group['id'], group_ids) def test_assertion_prefix_parameter(self): """Test parameters filtering based on the prefix. @@ -2416,27 +2366,24 @@ class FernetFederatedTokenTests(FederationTests, FederatedSetupMixin): super(FernetFederatedTokenTests, self).load_fixtures(fixtures) self.load_federation_sample_data() + def config_overrides(self): + super(FernetFederatedTokenTests, self).config_overrides() + self.config_fixture.config(group='token', provider='fernet') + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + def auth_plugin_config_override(self): methods = ['saml2', 'token', 'password'] - method_classes = dict( - password='keystone.auth.plugins.password.Password', - token='keystone.auth.plugins.token.Token', - saml2='keystone.auth.plugins.saml2.Saml2') super(FernetFederatedTokenTests, - self).auth_plugin_config_override(methods, **method_classes) - self.config_fixture.config( - group='token', - provider='keystone.token.providers.fernet.Provider') - self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + self).auth_plugin_config_override(methods) def test_federated_unscoped_token(self): resp = self._issue_unscoped_token() - self.assertEqual(186, len(resp.headers['X-Subject-Token'])) + self.assertEqual(204, len(resp.headers['X-Subject-Token'])) def test_federated_unscoped_token_with_multiple_groups(self): assertion = 'ANOTHER_CUSTOMER_ASSERTION' resp = self._issue_unscoped_token(assertion=assertion) - self.assertEqual(204, len(resp.headers['X-Subject-Token'])) + self.assertEqual(232, len(resp.headers['X-Subject-Token'])) def test_validate_federated_unscoped_token(self): resp = self._issue_unscoped_token() @@ -2481,11 +2428,8 @@ class FederatedTokenTestsMethodToken(FederatedTokenTests): def auth_plugin_config_override(self): methods = ['saml2', 'token'] - method_classes = dict( - token='keystone.auth.plugins.token.Token', - saml2='keystone.auth.plugins.saml2.Saml2') super(FederatedTokenTests, - self).auth_plugin_config_override(methods, **method_classes) + self).auth_plugin_config_override(methods) class JsonHomeTests(FederationTests, test_v3.JsonHomeTestMixin): @@ -2520,12 +2464,20 @@ class SAMLGenerationTests(FederationTests): SP_AUTH_URL = ('http://beta.com:5000/v3/OS-FEDERATION/identity_providers' '/BETA/protocols/saml2/auth') + + ASSERTION_FILE = 'signed_saml2_assertion.xml' + + # The values of the following variables match the attributes values found + # in ASSERTION_FILE ISSUER = 'https://acme.com/FIM/sps/openstack/saml20' RECIPIENT = 'http://beta.com/Shibboleth.sso/SAML2/POST' SUBJECT = 'test_user' + SUBJECT_DOMAIN = 'user_domain' ROLES = ['admin', 'member'] PROJECT = 'development' + PROJECT_DOMAIN = 'project_domain' SAML_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2' + ECP_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2/ecp' ASSERTION_VERSION = "2.0" SERVICE_PROVDIER_ID = 'ACME' @@ -2535,6 +2487,7 @@ class SAMLGenerationTests(FederationTests): 'enabled': True, 'description': uuid.uuid4().hex, 'sp_url': self.RECIPIENT, + 'relay_state_prefix': CONF.saml.relay_state_prefix, } return ref @@ -2542,9 +2495,11 @@ class SAMLGenerationTests(FederationTests): def setUp(self): super(SAMLGenerationTests, self).setUp() self.signed_assertion = saml2.create_class_from_xml_string( - saml.Assertion, _load_xml('signed_saml2_assertion.xml')) + saml.Assertion, _load_xml(self.ASSERTION_FILE)) self.sp = self.sp_ref() - self.federation_api.create_sp(self.SERVICE_PROVDIER_ID, self.sp) + url = '/OS-FEDERATION/service_providers/' + self.SERVICE_PROVDIER_ID + self.put(url, body={'service_provider': self.sp}, + expected_status=201) def test_samlize_token_values(self): """Test the SAML generator produces a SAML object. @@ -2558,8 +2513,10 @@ class SAMLGenerationTests(FederationTests): return_value=self.signed_assertion): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, - self.SUBJECT, self.ROLES, - self.PROJECT) + self.SUBJECT, + self.SUBJECT_DOMAIN, + self.ROLES, self.PROJECT, + self.PROJECT_DOMAIN) assertion = response.assertion self.assertIsNotNone(assertion) @@ -2571,14 +2528,24 @@ class SAMLGenerationTests(FederationTests): user_attribute = assertion.attribute_statement[0].attribute[0] self.assertEqual(self.SUBJECT, user_attribute.attribute_value[0].text) - role_attribute = assertion.attribute_statement[0].attribute[1] + user_domain_attribute = ( + assertion.attribute_statement[0].attribute[1]) + self.assertEqual(self.SUBJECT_DOMAIN, + user_domain_attribute.attribute_value[0].text) + + role_attribute = assertion.attribute_statement[0].attribute[2] for attribute_value in role_attribute.attribute_value: self.assertIn(attribute_value.text, self.ROLES) - project_attribute = assertion.attribute_statement[0].attribute[2] + project_attribute = assertion.attribute_statement[0].attribute[3] self.assertEqual(self.PROJECT, project_attribute.attribute_value[0].text) + project_domain_attribute = ( + assertion.attribute_statement[0].attribute[4]) + self.assertEqual(self.PROJECT_DOMAIN, + project_domain_attribute.attribute_value[0].text) + def test_verify_assertion_object(self): """Test that the Assertion object is built properly. @@ -2590,8 +2557,10 @@ class SAMLGenerationTests(FederationTests): side_effect=lambda x: x): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, - self.SUBJECT, self.ROLES, - self.PROJECT) + self.SUBJECT, + self.SUBJECT_DOMAIN, + self.ROLES, self.PROJECT, + self.PROJECT_DOMAIN) assertion = response.assertion self.assertEqual(self.ASSERTION_VERSION, assertion.version) @@ -2607,8 +2576,10 @@ class SAMLGenerationTests(FederationTests): return_value=self.signed_assertion): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, - self.SUBJECT, self.ROLES, - self.PROJECT) + self.SUBJECT, + self.SUBJECT_DOMAIN, + self.ROLES, self.PROJECT, + self.PROJECT_DOMAIN) saml_str = response.to_string() response = etree.fromstring(saml_str) @@ -2621,13 +2592,19 @@ class SAMLGenerationTests(FederationTests): user_attribute = assertion[4][0] self.assertEqual(self.SUBJECT, user_attribute[0].text) - role_attribute = assertion[4][1] + user_domain_attribute = assertion[4][1] + self.assertEqual(self.SUBJECT_DOMAIN, user_domain_attribute[0].text) + + role_attribute = assertion[4][2] for attribute_value in role_attribute: self.assertIn(attribute_value.text, self.ROLES) - project_attribute = assertion[4][2] + project_attribute = assertion[4][3] self.assertEqual(self.PROJECT, project_attribute[0].text) + project_domain_attribute = assertion[4][4] + self.assertEqual(self.PROJECT_DOMAIN, project_domain_attribute[0].text) + def test_assertion_using_explicit_namespace_prefixes(self): def mocked_subprocess_check_output(*popenargs, **kwargs): # the last option is the assertion file to be signed @@ -2642,8 +2619,10 @@ class SAMLGenerationTests(FederationTests): side_effect=mocked_subprocess_check_output): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, - self.SUBJECT, self.ROLES, - self.PROJECT) + self.SUBJECT, + self.SUBJECT_DOMAIN, + self.ROLES, self.PROJECT, + self.PROJECT_DOMAIN) assertion_xml = response.assertion.to_string() # make sure we have the proper tag and prefix for the assertion # namespace @@ -2666,8 +2645,9 @@ class SAMLGenerationTests(FederationTests): generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, - self.SUBJECT, self.ROLES, - self.PROJECT) + self.SUBJECT, self.SUBJECT_DOMAIN, + self.ROLES, self.PROJECT, + self.PROJECT_DOMAIN) signature = response.assertion.signature self.assertIsNotNone(signature) @@ -2770,12 +2750,18 @@ class SAMLGenerationTests(FederationTests): user_attribute = assertion[4][0] self.assertIsInstance(user_attribute[0].text, str) - role_attribute = assertion[4][1] + user_domain_attribute = assertion[4][1] + self.assertIsInstance(user_domain_attribute[0].text, str) + + role_attribute = assertion[4][2] self.assertIsInstance(role_attribute[0].text, str) - project_attribute = assertion[4][2] + project_attribute = assertion[4][3] self.assertIsInstance(project_attribute[0].text, str) + project_domain_attribute = assertion[4][4] + self.assertIsInstance(project_domain_attribute[0].text, str) + def test_invalid_scope_body(self): """Test that missing the scope in request body raises an exception. @@ -2839,6 +2825,104 @@ class SAMLGenerationTests(FederationTests): self.SERVICE_PROVDIER_ID) self.post(self.SAML_GENERATION_ROUTE, body=body, expected_status=404) + def test_generate_ecp_route(self): + """Test that the ECP generation endpoint produces XML. + + The ECP endpoint /v3/auth/OS-FEDERATION/saml2/ecp should take the same + input as the SAML generation endpoint (scoped token ID + Service + Provider ID). + The controller should return a SAML assertion that is wrapped in a + SOAP envelope. + """ + + self.config_fixture.config(group='saml', idp_entity_id=self.ISSUER) + token_id = self._fetch_valid_token() + body = self._create_generate_saml_request(token_id, + self.SERVICE_PROVDIER_ID) + + with mock.patch.object(keystone_idp, '_sign_assertion', + return_value=self.signed_assertion): + http_response = self.post(self.ECP_GENERATION_ROUTE, body=body, + response_content_type='text/xml', + expected_status=200) + + env_response = etree.fromstring(http_response.result) + header = env_response[0] + + # Verify the relay state starts with 'ss:mem' + prefix = CONF.saml.relay_state_prefix + self.assertThat(header[0].text, matchers.StartsWith(prefix)) + + # Verify that the content in the body matches the expected assertion + body = env_response[1] + response = body[0] + issuer = response[0] + assertion = response[2] + + self.assertEqual(self.RECIPIENT, response.get('Destination')) + self.assertEqual(self.ISSUER, issuer.text) + + user_attribute = assertion[4][0] + self.assertIsInstance(user_attribute[0].text, str) + + user_domain_attribute = assertion[4][1] + self.assertIsInstance(user_domain_attribute[0].text, str) + + role_attribute = assertion[4][2] + self.assertIsInstance(role_attribute[0].text, str) + + project_attribute = assertion[4][3] + self.assertIsInstance(project_attribute[0].text, str) + + project_domain_attribute = assertion[4][4] + self.assertIsInstance(project_domain_attribute[0].text, str) + + @mock.patch('saml2.create_class_from_xml_string') + @mock.patch('oslo_utils.fileutils.write_to_tempfile') + @mock.patch('subprocess.check_output') + def test__sign_assertion(self, check_output_mock, + write_to_tempfile_mock, create_class_mock): + write_to_tempfile_mock.return_value = 'tmp_path' + check_output_mock.return_value = 'fakeoutput' + + keystone_idp._sign_assertion(self.signed_assertion) + + create_class_mock.assert_called_with(saml.Assertion, 'fakeoutput') + + @mock.patch('oslo_utils.fileutils.write_to_tempfile') + @mock.patch('subprocess.check_output') + def test__sign_assertion_exc(self, check_output_mock, + write_to_tempfile_mock): + # If the command fails the command output is logged. + + write_to_tempfile_mock.return_value = 'tmp_path' + + sample_returncode = 1 + sample_output = self.getUniqueString() + check_output_mock.side_effect = subprocess.CalledProcessError( + returncode=sample_returncode, cmd=CONF.saml.xmlsec1_binary, + output=sample_output) + + # FIXME(blk-u): This should raise exception.SAMLSigningError instead, + # but fails with TypeError due to concatenating string to Message, see + # bug 1484735. + self.assertRaises(TypeError, + keystone_idp._sign_assertion, + self.signed_assertion) + + @mock.patch('oslo_utils.fileutils.write_to_tempfile') + def test__sign_assertion_fileutils_exc(self, write_to_tempfile_mock): + exception_msg = 'fake' + write_to_tempfile_mock.side_effect = Exception(exception_msg) + + logger_fixture = self.useFixture(fixtures.LoggerFixture()) + self.assertRaises(exception.SAMLSigningError, + keystone_idp._sign_assertion, + self.signed_assertion) + expected_log = ( + 'Error when signing assertion, reason: %s\n' % exception_msg) + self.assertEqual(expected_log, logger_fixture.output) + class IdPMetadataGenerationTests(FederationTests): """A class for testing Identity Provider Metadata generation.""" @@ -2976,7 +3060,8 @@ class ServiceProviderTests(FederationTests): MEMBER_NAME = 'service_provider' COLLECTION_NAME = 'service_providers' SERVICE_PROVIDER_ID = 'ACME' - SP_KEYS = ['auth_url', 'id', 'enabled', 'description', 'sp_url'] + SP_KEYS = ['auth_url', 'id', 'enabled', 'description', + 'relay_state_prefix', 'sp_url'] def setUp(self): super(FederationTests, self).setUp() @@ -2993,6 +3078,7 @@ class ServiceProviderTests(FederationTests): 'enabled': True, 'description': uuid.uuid4().hex, 'sp_url': 'https://' + uuid.uuid4().hex + '.com', + 'relay_state_prefix': CONF.saml.relay_state_prefix } return ref @@ -3019,6 +3105,29 @@ class ServiceProviderTests(FederationTests): self.assertValidEntity(resp.result['service_provider'], keys_to_check=self.SP_KEYS) + def test_create_sp_relay_state_default(self): + """Create an SP without relay state, should default to `ss:mem`.""" + url = self.base_url(suffix=uuid.uuid4().hex) + sp = self.sp_ref() + del sp['relay_state_prefix'] + resp = self.put(url, body={'service_provider': sp}, + expected_status=201) + sp_result = resp.result['service_provider'] + self.assertEqual(CONF.saml.relay_state_prefix, + sp_result['relay_state_prefix']) + + def test_create_sp_relay_state_non_default(self): + """Create an SP with custom relay state.""" + url = self.base_url(suffix=uuid.uuid4().hex) + sp = self.sp_ref() + non_default_prefix = uuid.uuid4().hex + sp['relay_state_prefix'] = non_default_prefix + resp = self.put(url, body={'service_provider': sp}, + expected_status=201) + sp_result = resp.result['service_provider'] + self.assertEqual(non_default_prefix, + sp_result['relay_state_prefix']) + def test_create_service_provider_fail(self): """Try adding SP object with unallowed attribute.""" url = self.base_url(suffix=uuid.uuid4().hex) @@ -3108,6 +3217,18 @@ class ServiceProviderTests(FederationTests): self.patch(url, body={'service_provider': new_sp_ref}, expected_status=404) + def test_update_sp_relay_state(self): + """Update an SP with custome relay state.""" + new_sp_ref = self.sp_ref() + non_default_prefix = uuid.uuid4().hex + new_sp_ref['relay_state_prefix'] = non_default_prefix + url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) + resp = self.patch(url, body={'service_provider': new_sp_ref}, + expected_status=200) + sp_result = resp.result['service_provider'] + self.assertEqual(non_default_prefix, + sp_result['relay_state_prefix']) + def test_delete_service_provider(self): url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) self.delete(url, expected_status=204) @@ -3125,6 +3246,7 @@ class WebSSOTests(FederatedTokenTests): SSO_TEMPLATE_PATH = os.path.join(core.dirs.etc(), SSO_TEMPLATE_NAME) TRUSTED_DASHBOARD = 'http://horizon.com' ORIGIN = urllib.parse.quote_plus(TRUSTED_DASHBOARD) + PROTOCOL_REMOTE_ID_ATTR = uuid.uuid4().hex def setUp(self): super(WebSSOTests, self).setUp() @@ -3145,7 +3267,19 @@ class WebSSOTests(FederatedTokenTests): self.assertIn(self.TRUSTED_DASHBOARD, resp.body) def test_federated_sso_auth(self): - environment = {self.REMOTE_ID_ATTR: self.REMOTE_ID} + environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]} + context = {'environment': environment} + query_string = {'origin': self.ORIGIN} + self._inject_assertion(context, 'EMPLOYEE_ASSERTION', query_string) + resp = self.api.federated_sso_auth(context, self.PROTOCOL) + self.assertIn(self.TRUSTED_DASHBOARD, resp.body) + + def test_federated_sso_auth_with_protocol_specific_remote_id(self): + self.config_fixture.config( + group=self.PROTOCOL, + remote_id_attribute=self.PROTOCOL_REMOTE_ID_ATTR) + + environment = {self.PROTOCOL_REMOTE_ID_ATTR: self.REMOTE_IDS[0]} context = {'environment': environment} query_string = {'origin': self.ORIGIN} self._inject_assertion(context, 'EMPLOYEE_ASSERTION', query_string) @@ -3162,7 +3296,7 @@ class WebSSOTests(FederatedTokenTests): context, self.PROTOCOL) def test_federated_sso_missing_query(self): - environment = {self.REMOTE_ID_ATTR: self.REMOTE_ID} + environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]} context = {'environment': environment} self._inject_assertion(context, 'EMPLOYEE_ASSERTION') self.assertRaises(exception.ValidationError, @@ -3178,7 +3312,7 @@ class WebSSOTests(FederatedTokenTests): context, self.PROTOCOL) def test_federated_sso_untrusted_dashboard(self): - environment = {self.REMOTE_ID_ATTR: self.REMOTE_ID} + environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0]} context = {'environment': environment} query_string = {'origin': uuid.uuid4().hex} self._inject_assertion(context, 'EMPLOYEE_ASSERTION', query_string) @@ -3229,6 +3363,7 @@ class K2KServiceCatalogTests(FederationTests): def sp_response(self, id, ref): ref.pop('enabled') ref.pop('description') + ref.pop('relay_state_prefix') ref['id'] = id return ref @@ -3238,6 +3373,7 @@ class K2KServiceCatalogTests(FederationTests): 'enabled': True, 'description': uuid.uuid4().hex, 'sp_url': uuid.uuid4().hex, + 'relay_state_prefix': CONF.saml.relay_state_prefix, } return ref diff --git a/keystone-moon/keystone/tests/unit/test_v3_filters.py b/keystone-moon/keystone/tests/unit/test_v3_filters.py index 4ad44657..668a2308 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_filters.py +++ b/keystone-moon/keystone/tests/unit/test_v3_filters.py @@ -17,6 +17,7 @@ import uuid from oslo_config import cfg from oslo_serialization import jsonutils +from six.moves import range from keystone.tests.unit import filtering from keystone.tests.unit.ksfixtures import temporaryfile @@ -331,12 +332,6 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): super(IdentityTestListLimitCase, self).setUp() - self._set_policy({"identity:list_users": [], - "identity:list_groups": [], - "identity:list_projects": [], - "identity:list_services": [], - "identity:list_policies": []}) - # Create 10 entries for each of the entities we are going to test self.ENTITY_TYPES = ['user', 'group', 'project'] self.entity_lists = {} @@ -398,6 +393,7 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): else: plural = '%ss' % entity + self._set_policy({"identity:list_%s" % plural: []}) self.config_fixture.config(list_limit=5) self.config_fixture.config(group=driver, list_limit=None) r = self.get('/%s' % plural, auth=self.auth) @@ -435,6 +431,7 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): def test_no_limit(self): """Check truncated attribute not set when list not limited.""" + self._set_policy({"identity:list_services": []}) r = self.get('/services', auth=self.auth) self.assertEqual(10, len(r.result.get('services'))) self.assertIsNone(r.result.get('truncated')) @@ -445,6 +442,7 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): # Test this by overriding the general limit with a higher # driver-specific limit (allowing all entities to be returned # in the collection), which should result in a non truncated list + self._set_policy({"identity:list_services": []}) self.config_fixture.config(list_limit=5) self.config_fixture.config(group='catalog', list_limit=10) r = self.get('/services', auth=self.auth) diff --git a/keystone-moon/keystone/tests/unit/test_v3_identity.py b/keystone-moon/keystone/tests/unit/test_v3_identity.py index ac077297..e0090829 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_identity.py +++ b/keystone-moon/keystone/tests/unit/test_v3_identity.py @@ -12,8 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import logging import uuid +import fixtures from oslo_config import cfg from testtools import matchers @@ -434,6 +436,38 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.delete('/groups/%(group_id)s' % { 'group_id': self.group_id}) + def test_create_user_password_not_logged(self): + # When a user is created, the password isn't logged at any level. + + log_fix = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + + ref = self.new_user_ref(domain_id=self.domain_id) + self.post( + '/users', + body={'user': ref}) + + self.assertNotIn(ref['password'], log_fix.output) + + def test_update_password_not_logged(self): + # When admin modifies user password, the password isn't logged at any + # level. + + log_fix = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + + # bootstrap a user as admin + user_ref = self.new_user_ref(domain_id=self.domain['id']) + password = user_ref['password'] + user_ref = self.identity_api.create_user(user_ref) + + # administrative password reset + new_password = uuid.uuid4().hex + self.patch('/users/%s' % user_ref['id'], + body={'user': {'password': new_password}}, + expected_status=200) + + self.assertNotIn(password, log_fix.output) + self.assertNotIn(new_password, log_fix.output) + class IdentityV3toV2MethodsTestCase(tests.TestCase): """Test users V3 to V2 conversion methods.""" @@ -444,27 +478,26 @@ class IdentityV3toV2MethodsTestCase(tests.TestCase): self.user_id = uuid.uuid4().hex self.default_project_id = uuid.uuid4().hex self.tenant_id = uuid.uuid4().hex - self.domain_id = uuid.uuid4().hex # User with only default_project_id in ref self.user1 = {'id': self.user_id, 'name': self.user_id, 'default_project_id': self.default_project_id, - 'domain_id': self.domain_id} + 'domain_id': CONF.identity.default_domain_id} # User without default_project_id or tenantId in ref self.user2 = {'id': self.user_id, 'name': self.user_id, - 'domain_id': self.domain_id} + 'domain_id': CONF.identity.default_domain_id} # User with both tenantId and default_project_id in ref self.user3 = {'id': self.user_id, 'name': self.user_id, 'default_project_id': self.default_project_id, 'tenantId': self.tenant_id, - 'domain_id': self.domain_id} + 'domain_id': CONF.identity.default_domain_id} # User with only tenantId in ref self.user4 = {'id': self.user_id, 'name': self.user_id, 'tenantId': self.tenant_id, - 'domain_id': self.domain_id} + 'domain_id': CONF.identity.default_domain_id} # Expected result if the user is meant to have a tenantId element self.expected_user = {'id': self.user_id, @@ -582,3 +615,18 @@ class UserSelfServiceChangingPasswordsTestCase(test_v3.RestfulTestCase): self.change_password(password=uuid.uuid4().hex, original_password=self.user_ref['password'], expected_status=401) + + def test_changing_password_not_logged(self): + # When a user changes their password, the password isn't logged at any + # level. + + log_fix = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + + # change password + new_password = uuid.uuid4().hex + self.change_password(password=new_password, + original_password=self.user_ref['password'], + expected_status=204) + + self.assertNotIn(self.user_ref['password'], log_fix.output) + self.assertNotIn(new_password, log_fix.output) diff --git a/keystone-moon/keystone/tests/unit/test_v3_oauth1.py b/keystone-moon/keystone/tests/unit/test_v3_oauth1.py index 608162d8..6c063c5e 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_oauth1.py +++ b/keystone-moon/keystone/tests/unit/test_v3_oauth1.py @@ -241,16 +241,6 @@ class ConsumerCRUDTests(OAuth1Tests): class OAuthFlowTests(OAuth1Tests): - def auth_plugin_config_override(self): - methods = ['password', 'token', 'oauth1'] - method_classes = { - 'password': 'keystone.auth.plugins.password.Password', - 'token': 'keystone.auth.plugins.token.Token', - 'oauth1': 'keystone.auth.plugins.oauth1.OAuth', - } - super(OAuthFlowTests, self).auth_plugin_config_override( - methods, **method_classes) - def test_oauth_flow(self): consumer = self._create_single_consumer() consumer_id = consumer['id'] diff --git a/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py b/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py index 5710d973..48226cd4 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py +++ b/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py @@ -17,6 +17,7 @@ from oslo_utils import timeutils import six from testtools import matchers +from keystone.common import utils from keystone.contrib.revoke import model from keystone.tests.unit import test_v3 from keystone.token import provider @@ -25,7 +26,7 @@ from keystone.token import provider def _future_time_string(): expire_delta = datetime.timedelta(seconds=1000) future_time = timeutils.utcnow() + expire_delta - return timeutils.isotime(future_time) + return utils.isotime(future_time) class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): @@ -55,13 +56,13 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): self.assertTrue( before_time <= event_issued_before, 'invalid event issued_before time; %s is not later than %s.' % ( - timeutils.isotime(event_issued_before, subsecond=True), - timeutils.isotime(before_time, subsecond=True))) + utils.isotime(event_issued_before, subsecond=True), + utils.isotime(before_time, subsecond=True))) self.assertTrue( event_issued_before <= after_time, 'invalid event issued_before time; %s is not earlier than %s.' % ( - timeutils.isotime(event_issued_before, subsecond=True), - timeutils.isotime(after_time, subsecond=True))) + utils.isotime(event_issued_before, subsecond=True), + utils.isotime(after_time, subsecond=True))) del (event['issued_before']) self.assertEqual(sample, event) @@ -76,7 +77,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): expires_at = provider.default_expire_time() sample = self._blank_event() sample['user_id'] = six.text_type(user_id) - sample['expires_at'] = six.text_type(timeutils.isotime(expires_at)) + sample['expires_at'] = six.text_type(utils.isotime(expires_at)) before_time = timeutils.utcnow() self.revoke_api.revoke_by_expiration(user_id, expires_at) resp = self.get('/OS-REVOKE/events') diff --git a/keystone-moon/keystone/tests/unit/test_v3_protection.py b/keystone-moon/keystone/tests/unit/test_v3_protection.py index 2b2c96d1..458c61de 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_protection.py +++ b/keystone-moon/keystone/tests/unit/test_v3_protection.py @@ -391,23 +391,18 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): # Given a non-admin user token, the token can be used to validate # itself. # This is GET /v3/auth/tokens, with X-Auth-Token == X-Subject-Token - # FIXME(blk-u): This test fails, a user can't validate their own token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], password=self.just_a_user['password']) token = self.get_requested_token(auth) - # FIXME(blk-u): remove expected_status=403. self.get('/auth/tokens', token=token, - headers={'X-Subject-Token': token}, expected_status=403) + headers={'X-Subject-Token': token}) def test_user_validate_user_token(self): # A user can validate one of their own tokens. # This is GET /v3/auth/tokens - # FIXME(blk-u): This test fails, a user can't validate their own token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -415,9 +410,8 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): token1 = self.get_requested_token(auth) token2 = self.get_requested_token(auth) - # FIXME(blk-u): remove expected_status=403. self.get('/auth/tokens', token=token1, - headers={'X-Subject-Token': token2}, expected_status=403) + headers={'X-Subject-Token': token2}) def test_user_validate_other_user_token_rejected(self): # A user cannot validate another user's token. @@ -458,23 +452,18 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): # Given a non-admin user token, the token can be used to check # itself. # This is HEAD /v3/auth/tokens, with X-Auth-Token == X-Subject-Token - # FIXME(blk-u): This test fails, a user can't check the same token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], password=self.just_a_user['password']) token = self.get_requested_token(auth) - # FIXME(blk-u): change to expected_status=200 self.head('/auth/tokens', token=token, - headers={'X-Subject-Token': token}, expected_status=403) + headers={'X-Subject-Token': token}, expected_status=200) def test_user_check_user_token(self): # A user can check one of their own tokens. # This is HEAD /v3/auth/tokens - # FIXME(blk-u): This test fails, a user can't check the same token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -482,9 +471,8 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): token1 = self.get_requested_token(auth) token2 = self.get_requested_token(auth) - # FIXME(blk-u): change to expected_status=200 self.head('/auth/tokens', token=token1, - headers={'X-Subject-Token': token2}, expected_status=403) + headers={'X-Subject-Token': token2}, expected_status=200) def test_user_check_other_user_token_rejected(self): # A user cannot check another user's token. @@ -526,23 +514,18 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): # Given a non-admin user token, the token can be used to revoke # itself. # This is DELETE /v3/auth/tokens, with X-Auth-Token == X-Subject-Token - # FIXME(blk-u): This test fails, a user can't revoke the same token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], password=self.just_a_user['password']) token = self.get_requested_token(auth) - # FIXME(blk-u): remove expected_status=403 self.delete('/auth/tokens', token=token, - headers={'X-Subject-Token': token}, expected_status=403) + headers={'X-Subject-Token': token}) def test_user_revoke_user_token(self): # A user can revoke one of their own tokens. # This is DELETE /v3/auth/tokens - # FIXME(blk-u): This test fails, a user can't revoke the same token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -550,9 +533,8 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): token1 = self.get_requested_token(auth) token2 = self.get_requested_token(auth) - # FIXME(blk-u): remove expected_status=403 self.delete('/auth/tokens', token=token1, - headers={'X-Subject-Token': token2}, expected_status=403) + headers={'X-Subject-Token': token2}) def test_user_revoke_other_user_token_rejected(self): # A user cannot revoke another user's token. @@ -591,7 +573,8 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): headers={'X-Subject-Token': user_token}) -class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase): +class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, + test_v3.AssignmentTestMixin): """Test policy enforcement of the sample v3 cloud policy file.""" def setUp(self): @@ -905,6 +888,141 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase): self._test_grants('projects', self.project['id']) + def test_cloud_admin_list_assignments_of_domain(self): + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + domain_id=self.admin_domain['id']) + + collection_url = self.build_role_assignment_query_url( + domain_id=self.domainA['id']) + r = self.get(collection_url, auth=self.auth) + self.assertValidRoleAssignmentListResponse( + r, expected_length=2, resource_url=collection_url) + + domainA_admin_entity = self.build_role_assignment_entity( + domain_id=self.domainA['id'], + user_id=self.domain_admin_user['id'], + role_id=self.admin_role['id'], + inherited_to_projects=False) + domainA_user_entity = self.build_role_assignment_entity( + domain_id=self.domainA['id'], + user_id=self.just_a_user['id'], + role_id=self.role['id'], + inherited_to_projects=False) + + self.assertRoleAssignmentInListResponse(r, domainA_admin_entity) + self.assertRoleAssignmentInListResponse(r, domainA_user_entity) + + def test_domain_admin_list_assignments_of_domain(self): + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + collection_url = self.build_role_assignment_query_url( + domain_id=self.domainA['id']) + r = self.get(collection_url, auth=self.auth) + self.assertValidRoleAssignmentListResponse( + r, expected_length=2, resource_url=collection_url) + + domainA_admin_entity = self.build_role_assignment_entity( + domain_id=self.domainA['id'], + user_id=self.domain_admin_user['id'], + role_id=self.admin_role['id'], + inherited_to_projects=False) + domainA_user_entity = self.build_role_assignment_entity( + domain_id=self.domainA['id'], + user_id=self.just_a_user['id'], + role_id=self.role['id'], + inherited_to_projects=False) + + self.assertRoleAssignmentInListResponse(r, domainA_admin_entity) + self.assertRoleAssignmentInListResponse(r, domainA_user_entity) + + def test_domain_admin_list_assignments_of_another_domain_failed(self): + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + collection_url = self.build_role_assignment_query_url( + domain_id=self.domainB['id']) + self.get(collection_url, auth=self.auth, expected_status=403) + + def test_domain_user_list_assignments_of_domain_failed(self): + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + collection_url = self.build_role_assignment_query_url( + domain_id=self.domainA['id']) + self.get(collection_url, auth=self.auth, expected_status=403) + + def test_cloud_admin_list_assignments_of_project(self): + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + domain_id=self.admin_domain['id']) + + collection_url = self.build_role_assignment_query_url( + project_id=self.project['id']) + r = self.get(collection_url, auth=self.auth) + self.assertValidRoleAssignmentListResponse( + r, expected_length=2, resource_url=collection_url) + + project_admin_entity = self.build_role_assignment_entity( + project_id=self.project['id'], + user_id=self.project_admin_user['id'], + role_id=self.admin_role['id'], + inherited_to_projects=False) + project_user_entity = self.build_role_assignment_entity( + project_id=self.project['id'], + user_id=self.just_a_user['id'], + role_id=self.role['id'], + inherited_to_projects=False) + + self.assertRoleAssignmentInListResponse(r, project_admin_entity) + self.assertRoleAssignmentInListResponse(r, project_user_entity) + + @tests.utils.wip('waiting on bug #1437407') + def test_domain_admin_list_assignments_of_project(self): + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + collection_url = self.build_role_assignment_query_url( + project_id=self.project['id']) + r = self.get(collection_url, auth=self.auth) + self.assertValidRoleAssignmentListResponse( + r, expected_length=2, resource_url=collection_url) + + project_admin_entity = self.build_role_assignment_entity( + project_id=self.project['id'], + user_id=self.project_admin_user['id'], + role_id=self.admin_role['id'], + inherited_to_projects=False) + project_user_entity = self.build_role_assignment_entity( + project_id=self.project['id'], + user_id=self.just_a_user['id'], + role_id=self.role['id'], + inherited_to_projects=False) + + self.assertRoleAssignmentInListResponse(r, project_admin_entity) + self.assertRoleAssignmentInListResponse(r, project_user_entity) + + def test_domain_user_list_assignments_of_project_failed(self): + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + collection_url = self.build_role_assignment_query_url( + project_id=self.project['id']) + self.get(collection_url, auth=self.auth, expected_status=403) + def test_cloud_admin(self): self.auth = self.build_authentication_request( user_id=self.domain_admin_user['id'], @@ -921,6 +1039,14 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase): self._test_domain_management() + def test_domain_admin_get_domain(self): + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + entity_url = '/domains/%s' % self.domainA['id'] + self.get(entity_url, auth=self.auth, expected_status=200) + def test_list_user_credentials(self): self.credential_user = self.new_credential_ref(self.just_a_user['id']) self.credential_api.create_credential(self.credential_user['id'], @@ -982,23 +1108,18 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase): # Given a non-admin user token, the token can be used to validate # itself. # This is GET /v3/auth/tokens, with X-Auth-Token == X-Subject-Token - # FIXME(blk-u): This test fails, a user can't validate their own token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], password=self.just_a_user['password']) token = self.get_requested_token(auth) - # FIXME(blk-u): remove expected_status=403. self.get('/auth/tokens', token=token, - headers={'X-Subject-Token': token}, expected_status=403) + headers={'X-Subject-Token': token}) def test_user_validate_user_token(self): # A user can validate one of their own tokens. # This is GET /v3/auth/tokens - # FIXME(blk-u): This test fails, a user can't validate their own token, - # see bug 1421825. auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -1006,9 +1127,8 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase): token1 = self.get_requested_token(auth) token2 = self.get_requested_token(auth) - # FIXME(blk-u): remove expected_status=403. self.get('/auth/tokens', token=token1, - headers={'X-Subject-Token': token2}, expected_status=403) + headers={'X-Subject-Token': token2}) def test_user_validate_other_user_token_rejected(self): # A user cannot validate another user's token. diff --git a/keystone-moon/keystone/tests/unit/test_validation.py b/keystone-moon/keystone/tests/unit/test_validation.py index f83cabcb..f7a224a0 100644 --- a/keystone-moon/keystone/tests/unit/test_validation.py +++ b/keystone-moon/keystone/tests/unit/test_validation.py @@ -13,6 +13,7 @@ import uuid +import six import testtools from keystone.assignment import schema as assignment_schema @@ -24,8 +25,10 @@ from keystone.contrib.endpoint_filter import schema as endpoint_filter_schema from keystone.contrib.federation import schema as federation_schema from keystone.credential import schema as credential_schema from keystone import exception +from keystone.identity import schema as identity_schema from keystone.policy import schema as policy_schema from keystone.resource import schema as resource_schema +from keystone.tests import unit from keystone.trust import schema as trust_schema """Example model to validate create requests against. Assume that this is @@ -96,7 +99,84 @@ _VALID_FILTERS = [{'interface': 'admin'}, _INVALID_FILTERS = ['some string', 1, 0, True, False] -class EntityValidationTestCase(testtools.TestCase): +class ValidatedDecoratorTests(unit.BaseTestCase): + + entity_schema = { + 'type': 'object', + 'properties': { + 'name': parameter_types.name, + }, + 'required': ['name'], + } + + valid_entity = { + 'name': uuid.uuid4().hex, + } + + invalid_entity = {} + + @validation.validated(entity_schema, 'entity') + def do_something(self, entity): + pass + + @validation.validated(entity_create, 'entity') + def create_entity(self, entity): + pass + + @validation.validated(entity_update, 'entity') + def update_entity(self, entity_id, entity): + pass + + def _assert_call_entity_method_fails(self, method, *args, **kwargs): + e = self.assertRaises(exception.ValidationError, method, + *args, **kwargs) + + self.assertIn('Expecting to find entity in request body', + six.text_type(e)) + + def test_calling_with_valid_entity_kwarg_succeeds(self): + self.do_something(entity=self.valid_entity) + + def test_calling_with_invalid_entity_kwarg_fails(self): + self.assertRaises(exception.ValidationError, + self.do_something, + entity=self.invalid_entity) + + def test_calling_with_valid_entity_arg_succeeds(self): + self.do_something(self.valid_entity) + + def test_calling_with_invalid_entity_arg_fails(self): + self.assertRaises(exception.ValidationError, + self.do_something, + self.invalid_entity) + + def test_using_the_wrong_name_with_the_decorator_fails(self): + with testtools.ExpectedException(TypeError): + @validation.validated(self.entity_schema, 'entity_') + def function(entity): + pass + + def test_create_entity_no_request_body_with_decorator(self): + """Test the case when request body is not provided.""" + self._assert_call_entity_method_fails(self.create_entity) + + def test_create_entity_empty_request_body_with_decorator(self): + """Test the case when client passing in an empty entity reference.""" + self._assert_call_entity_method_fails(self.create_entity, entity={}) + + def test_update_entity_no_request_body_with_decorator(self): + """Test the case when request body is not provided.""" + self._assert_call_entity_method_fails(self.update_entity, + uuid.uuid4().hex) + + def test_update_entity_empty_request_body_with_decorator(self): + """Test the case when client passing in an empty entity reference.""" + self._assert_call_entity_method_fails(self.update_entity, + uuid.uuid4().hex, + entity={}) + + +class EntityValidationTestCase(unit.BaseTestCase): def setUp(self): super(EntityValidationTestCase, self).setUp() @@ -226,7 +306,7 @@ class EntityValidationTestCase(testtools.TestCase): def test_create_entity_with_invalid_id_strings(self): """Exception raised when using invalid id strings.""" long_string = 'A' * 65 - invalid_id_strings = ['', long_string, 'this,should,fail'] + invalid_id_strings = ['', long_string] for invalid_id in invalid_id_strings: request_to_validate = {'name': self.resource_name, 'id_string': invalid_id} @@ -299,7 +379,7 @@ class EntityValidationTestCase(testtools.TestCase): request_to_validate) -class ProjectValidationTestCase(testtools.TestCase): +class ProjectValidationTestCase(unit.BaseTestCase): """Test for V3 Project API validation.""" def setUp(self): @@ -426,7 +506,7 @@ class ProjectValidationTestCase(testtools.TestCase): request_to_validate) -class DomainValidationTestCase(testtools.TestCase): +class DomainValidationTestCase(unit.BaseTestCase): """Test for V3 Domain API validation.""" def setUp(self): @@ -524,7 +604,7 @@ class DomainValidationTestCase(testtools.TestCase): request_to_validate) -class RoleValidationTestCase(testtools.TestCase): +class RoleValidationTestCase(unit.BaseTestCase): """Test for V3 Role API validation.""" def setUp(self): @@ -578,7 +658,7 @@ class RoleValidationTestCase(testtools.TestCase): request_to_validate) -class PolicyValidationTestCase(testtools.TestCase): +class PolicyValidationTestCase(unit.BaseTestCase): """Test for V3 Policy API validation.""" def setUp(self): @@ -653,7 +733,7 @@ class PolicyValidationTestCase(testtools.TestCase): request_to_validate) -class CredentialValidationTestCase(testtools.TestCase): +class CredentialValidationTestCase(unit.BaseTestCase): """Test for V3 Credential API validation.""" def setUp(self): @@ -770,7 +850,7 @@ class CredentialValidationTestCase(testtools.TestCase): self.update_credential_validator.validate(request_to_validate) -class RegionValidationTestCase(testtools.TestCase): +class RegionValidationTestCase(unit.BaseTestCase): """Test for V3 Region API validation.""" def setUp(self): @@ -804,6 +884,14 @@ class RegionValidationTestCase(testtools.TestCase): 'parent_region_id': uuid.uuid4().hex} self.create_region_validator.validate(request_to_validate) + def test_validate_region_create_fails_with_invalid_region_id(self): + """Exception raised when passing invalid `id` in request.""" + request_to_validate = {'id': 1234, + 'description': 'US East Region'} + self.assertRaises(exception.SchemaValidationError, + self.create_region_validator.validate, + request_to_validate) + def test_validate_region_create_succeeds_with_extra_parameters(self): """Validate create region request with extra values.""" request_to_validate = {'other_attr': uuid.uuid4().hex} @@ -830,7 +918,7 @@ class RegionValidationTestCase(testtools.TestCase): request_to_validate) -class ServiceValidationTestCase(testtools.TestCase): +class ServiceValidationTestCase(unit.BaseTestCase): """Test for V3 Service API validation.""" def setUp(self): @@ -985,7 +1073,7 @@ class ServiceValidationTestCase(testtools.TestCase): request_to_validate) -class EndpointValidationTestCase(testtools.TestCase): +class EndpointValidationTestCase(unit.BaseTestCase): """Test for V3 Endpoint API validation.""" def setUp(self): @@ -1096,6 +1184,26 @@ class EndpointValidationTestCase(testtools.TestCase): self.create_endpoint_validator.validate, request_to_validate) + def test_validate_endpoint_create_fails_with_invalid_region_id(self): + """Exception raised when passing invalid `region(_id)` in request.""" + request_to_validate = {'interface': 'admin', + 'region_id': 1234, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + + request_to_validate = {'interface': 'admin', + 'region': 1234, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + def test_validate_endpoint_update_fails_with_invalid_enabled(self): """Exception raised when `enabled` is boolean-like value.""" for invalid_enabled in _INVALID_ENABLED_FORMATS: @@ -1163,8 +1271,28 @@ class EndpointValidationTestCase(testtools.TestCase): self.update_endpoint_validator.validate, request_to_validate) + def test_validate_endpoint_update_fails_with_invalid_region_id(self): + """Exception raised when passing invalid `region(_id)` in request.""" + request_to_validate = {'interface': 'admin', + 'region_id': 1234, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} -class EndpointGroupValidationTestCase(testtools.TestCase): + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_validator.validate, + request_to_validate) + + request_to_validate = {'interface': 'admin', + 'region': 1234, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_validator.validate, + request_to_validate) + + +class EndpointGroupValidationTestCase(unit.BaseTestCase): """Test for V3 Endpoint Group API validation.""" def setUp(self): @@ -1269,7 +1397,7 @@ class EndpointGroupValidationTestCase(testtools.TestCase): request_to_validate) -class TrustValidationTestCase(testtools.TestCase): +class TrustValidationTestCase(unit.BaseTestCase): """Test for V3 Trust API validation.""" _valid_roles = ['member', uuid.uuid4().hex, str(uuid.uuid4())] @@ -1360,6 +1488,13 @@ class TrustValidationTestCase(testtools.TestCase): 'remaining_uses': 2} self.create_trust_validator.validate(request_to_validate) + def test_validate_trust_with_period_in_user_id_string(self): + """Validate trust request with a period in the user id string.""" + request_to_validate = {'trustor_user_id': 'john.smith', + 'trustee_user_id': 'joe.developer', + 'impersonation': False} + self.create_trust_validator.validate(request_to_validate) + def test_validate_trust_with_invalid_expires_at_fails(self): """Validate trust request with invalid `expires_at` fails.""" request_to_validate = {'trustor_user_id': uuid.uuid4().hex, @@ -1399,7 +1534,7 @@ class TrustValidationTestCase(testtools.TestCase): self.create_trust_validator.validate(request_to_validate) -class ServiceProviderValidationTestCase(testtools.TestCase): +class ServiceProviderValidationTestCase(unit.BaseTestCase): """Test for V3 Service Provider API validation.""" def setUp(self): @@ -1561,3 +1696,182 @@ class ServiceProviderValidationTestCase(testtools.TestCase): self.assertRaises(exception.SchemaValidationError, self.update_sp_validator.validate, request_to_validate) + + +class UserValidationTestCase(unit.BaseTestCase): + """Test for V3 User API validation.""" + + def setUp(self): + super(UserValidationTestCase, self).setUp() + + self.user_name = uuid.uuid4().hex + + create = identity_schema.user_create + update = identity_schema.user_update + self.create_user_validator = validators.SchemaValidator(create) + self.update_user_validator = validators.SchemaValidator(update) + + def test_validate_user_create_request_succeeds(self): + """Test that validating a user create request succeeds.""" + request_to_validate = {'name': self.user_name} + self.create_user_validator.validate(request_to_validate) + + def test_validate_user_create_with_all_valid_parameters_succeeds(self): + """Test that validating a user create request succeeds.""" + request_to_validate = {'name': self.user_name, + 'default_project_id': uuid.uuid4().hex, + 'domain_id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True, + 'email': uuid.uuid4().hex, + 'password': uuid.uuid4().hex} + self.create_user_validator.validate(request_to_validate) + + def test_validate_user_create_fails_without_name(self): + """Exception raised when validating a user without name.""" + request_to_validate = {'email': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_user_validator.validate, + request_to_validate) + + def test_validate_user_create_fails_with_name_of_zero_length(self): + """Exception raised when validating a username with length of zero.""" + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.create_user_validator.validate, + request_to_validate) + + def test_validate_user_create_fails_with_name_of_wrong_type(self): + """Exception raised when validating a username of wrong type.""" + request_to_validate = {'name': True} + self.assertRaises(exception.SchemaValidationError, + self.create_user_validator.validate, + request_to_validate) + + def test_validate_user_create_succeeds_with_valid_enabled_formats(self): + """Validate acceptable enabled formats in create user requests.""" + for enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'name': self.user_name, + 'enabled': enabled} + self.create_user_validator.validate(request_to_validate) + + def test_validate_user_create_fails_with_invalid_enabled_formats(self): + """Exception raised when enabled is not an acceptable format.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'name': self.user_name, + 'enabled': invalid_enabled} + self.assertRaises(exception.SchemaValidationError, + self.create_user_validator.validate, + request_to_validate) + + def test_validate_user_create_succeeds_with_extra_attributes(self): + """Validate extra parameters on user create requests.""" + request_to_validate = {'name': self.user_name, + 'other_attr': uuid.uuid4().hex} + self.create_user_validator.validate(request_to_validate) + + def test_validate_user_create_succeeds_with_password_of_zero_length(self): + """Validate empty password on user create requests.""" + request_to_validate = {'name': self.user_name, + 'password': ''} + self.create_user_validator.validate(request_to_validate) + + def test_validate_user_create_succeeds_with_null_password(self): + """Validate that password is nullable on create user.""" + request_to_validate = {'name': self.user_name, + 'password': None} + self.create_user_validator.validate(request_to_validate) + + def test_validate_user_create_fails_with_invalid_password_type(self): + """Exception raised when user password is of the wrong type.""" + request_to_validate = {'name': self.user_name, + 'password': True} + self.assertRaises(exception.SchemaValidationError, + self.create_user_validator.validate, + request_to_validate) + + def test_validate_user_create_succeeds_with_null_description(self): + """Validate that description can be nullable on create user.""" + request_to_validate = {'name': self.user_name, + 'description': None} + self.create_user_validator.validate(request_to_validate) + + def test_validate_user_update_succeeds(self): + """Validate an update user request.""" + request_to_validate = {'email': uuid.uuid4().hex} + self.update_user_validator.validate(request_to_validate) + + def test_validate_user_update_fails_with_no_parameters(self): + """Exception raised when updating nothing.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_user_validator.validate, + request_to_validate) + + def test_validate_user_update_succeeds_with_extra_parameters(self): + """Validate user update requests with extra parameters.""" + request_to_validate = {'other_attr': uuid.uuid4().hex} + self.update_user_validator.validate(request_to_validate) + + +class GroupValidationTestCase(unit.BaseTestCase): + """Test for V3 Group API validation.""" + + def setUp(self): + super(GroupValidationTestCase, self).setUp() + + self.group_name = uuid.uuid4().hex + + create = identity_schema.group_create + update = identity_schema.group_update + self.create_group_validator = validators.SchemaValidator(create) + self.update_group_validator = validators.SchemaValidator(update) + + def test_validate_group_create_succeeds(self): + """Validate create group requests.""" + request_to_validate = {'name': self.group_name} + self.create_group_validator.validate(request_to_validate) + + def test_validate_group_create_succeeds_with_all_parameters(self): + """Validate create group requests with all parameters.""" + request_to_validate = {'name': self.group_name, + 'description': uuid.uuid4().hex, + 'domain_id': uuid.uuid4().hex} + self.create_group_validator.validate(request_to_validate) + + def test_validate_group_create_fails_without_group_name(self): + """Exception raised when group name is not provided in request.""" + request_to_validate = {'description': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_group_validator.validate, + request_to_validate) + + def test_validate_group_create_fails_when_group_name_is_too_short(self): + """Exception raised when group name is equal to zero.""" + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.create_group_validator.validate, + request_to_validate) + + def test_validate_group_create_succeeds_with_extra_parameters(self): + """Validate extra attributes on group create requests.""" + request_to_validate = {'name': self.group_name, + 'other_attr': uuid.uuid4().hex} + self.create_group_validator.validate(request_to_validate) + + def test_validate_group_update_succeeds(self): + """Validate group update requests.""" + request_to_validate = {'description': uuid.uuid4().hex} + self.update_group_validator.validate(request_to_validate) + + def test_validate_group_update_fails_with_no_parameters(self): + """Exception raised when no parameters passed in on update.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_group_validator.validate, + request_to_validate) + + def test_validate_group_update_succeeds_with_extra_parameters(self): + """Validate group update requests with extra parameters.""" + request_to_validate = {'other_attr': uuid.uuid4().hex} + self.update_group_validator.validate(request_to_validate) diff --git a/keystone-moon/keystone/tests/unit/test_versions.py b/keystone-moon/keystone/tests/unit/test_versions.py index 6fe692ad..7f722f94 100644 --- a/keystone-moon/keystone/tests/unit/test_versions.py +++ b/keystone-moon/keystone/tests/unit/test_versions.py @@ -25,6 +25,7 @@ from testtools import matchers as tt_matchers from keystone.common import json_home from keystone import controllers from keystone.tests import unit as tests +from keystone.tests.unit import utils CONF = cfg.CONF @@ -71,9 +72,9 @@ v3_MEDIA_TYPES = [ ] v3_EXPECTED_RESPONSE = { - "id": "v3.0", + "id": "v3.4", "status": "stable", - "updated": "2013-03-06T00:00:00Z", + "updated": "2015-03-30T00:00:00Z", "links": [ { "rel": "self", @@ -161,7 +162,8 @@ ENDPOINT_GROUP_ID_PARAMETER_RELATION = ( BASE_IDP_PROTOCOL = '/OS-FEDERATION/identity_providers/{idp_id}/protocols' BASE_EP_POLICY = '/policies/{policy_id}/OS-ENDPOINT-POLICY' -BASE_EP_FILTER = '/OS-EP-FILTER/endpoint_groups/{endpoint_group_id}' +BASE_EP_FILTER_PREFIX = '/OS-EP-FILTER' +BASE_EP_FILTER = BASE_EP_FILTER_PREFIX + '/endpoint_groups/{endpoint_group_id}' BASE_ACCESS_TOKEN = ( '/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}') @@ -352,6 +354,8 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { 'href': '/OS-FEDERATION/projects'}, _build_federation_rel(resource_name='saml2'): { 'href': '/auth/OS-FEDERATION/saml2'}, + _build_federation_rel(resource_name='ecp'): { + 'href': '/auth/OS-FEDERATION/saml2/ecp'}, _build_federation_rel(resource_name='metadata'): { 'href': '/OS-FEDERATION/saml2/metadata'}, _build_federation_rel(resource_name='identity_providers'): { @@ -474,6 +478,12 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { 'href-template': BASE_EP_FILTER + '/endpoints', 'href-vars': {'endpoint_group_id': ENDPOINT_GROUP_ID_PARAMETER_RELATION, }}, + _build_ep_filter_rel(resource_name='project_endpoint_groups'): + { + 'href-template': (BASE_EP_FILTER_PREFIX + '/projects/{project_id}' + + '/endpoint_groups'), + 'href-vars': {'project_id': + json_home.Parameters.PROJECT_ID, }}, _build_ep_filter_rel(resource_name='project_endpoint'): { 'href-template': ('/OS-EP-FILTER/projects/{project_id}' @@ -635,9 +645,11 @@ class VersionTestCase(tests.TestCase): def config_overrides(self): super(VersionTestCase, self).config_overrides() - port = random.randint(10000, 30000) - self.config_fixture.config(group='eventlet_server', public_port=port, - admin_port=port) + admin_port = random.randint(10000, 30000) + public_port = random.randint(40000, 60000) + self.config_fixture.config(group='eventlet_server', + public_port=public_port, + admin_port=admin_port) def _paste_in_port(self, response, port): for link in response['links']: @@ -651,7 +663,7 @@ class VersionTestCase(tests.TestCase): data = jsonutils.loads(resp.body) expected = VERSIONS_RESPONSE for version in expected['versions']['values']: - if version['id'] == 'v3.0': + if version['id'].startswith('v3'): self._paste_in_port( version, 'http://localhost:%s/v3/' % CONF.eventlet_server.public_port) @@ -668,7 +680,7 @@ class VersionTestCase(tests.TestCase): data = jsonutils.loads(resp.body) expected = VERSIONS_RESPONSE for version in expected['versions']['values']: - if version['id'] == 'v3.0': + if version['id'].startswith('v3'): self._paste_in_port( version, 'http://localhost:%s/v3/' % CONF.eventlet_server.admin_port) @@ -689,7 +701,7 @@ class VersionTestCase(tests.TestCase): expected = VERSIONS_RESPONSE for version in expected['versions']['values']: # localhost happens to be the site url for tests - if version['id'] == 'v3.0': + if version['id'].startswith('v3'): self._paste_in_port( version, 'http://localhost/v3/') elif version['id'] == 'v2.0': @@ -741,8 +753,9 @@ class VersionTestCase(tests.TestCase): CONF.eventlet_server.public_port) self.assertEqual(expected, data) + @utils.wip('waiting on bug #1381961') def test_admin_version_v3(self): - client = tests.TestClient(self.public_app) + client = tests.TestClient(self.admin_app) resp = client.get('/v3/') self.assertEqual(200, resp.status_int) data = jsonutils.loads(resp.body) @@ -931,9 +944,11 @@ class VersionSingleAppTestCase(tests.TestCase): def config_overrides(self): super(VersionSingleAppTestCase, self).config_overrides() - port = random.randint(10000, 30000) - self.config_fixture.config(group='eventlet_server', public_port=port, - admin_port=port) + admin_port = random.randint(10000, 30000) + public_port = random.randint(40000, 60000) + self.config_fixture.config(group='eventlet_server', + public_port=public_port, + admin_port=admin_port) def _paste_in_port(self, response, port): for link in response['links']: @@ -941,6 +956,11 @@ class VersionSingleAppTestCase(tests.TestCase): link['href'] = port def _test_version(self, app_name): + def app_port(): + if app_name == 'admin': + return CONF.eventlet_server.admin_port + else: + return CONF.eventlet_server.public_port app = self.loadapp('keystone', app_name) client = tests.TestClient(app) resp = client.get('/') @@ -948,14 +968,12 @@ class VersionSingleAppTestCase(tests.TestCase): data = jsonutils.loads(resp.body) expected = VERSIONS_RESPONSE for version in expected['versions']['values']: - if version['id'] == 'v3.0': + if version['id'].startswith('v3'): self._paste_in_port( - version, 'http://localhost:%s/v3/' % - CONF.eventlet_server.public_port) + version, 'http://localhost:%s/v3/' % app_port()) elif version['id'] == 'v2.0': self._paste_in_port( - version, 'http://localhost:%s/v2.0/' % - CONF.eventlet_server.public_port) + version, 'http://localhost:%s/v2.0/' % app_port()) self.assertThat(data, _VersionsEqual(expected)) def test_public(self): @@ -978,9 +996,11 @@ class VersionInheritEnabledTestCase(tests.TestCase): def config_overrides(self): super(VersionInheritEnabledTestCase, self).config_overrides() - port = random.randint(10000, 30000) - self.config_fixture.config(group='eventlet_server', public_port=port, - admin_port=port) + admin_port = random.randint(10000, 30000) + public_port = random.randint(40000, 60000) + self.config_fixture.config(group='eventlet_server', + public_port=public_port, + admin_port=admin_port) self.config_fixture.config(group='os_inherit', enabled=True) @@ -1021,7 +1041,7 @@ class VersionBehindSslTestCase(tests.TestCase): def _get_expected(self, host): expected = VERSIONS_RESPONSE for version in expected['versions']['values']: - if version['id'] == 'v3.0': + if version['id'].startswith('v3'): self._paste_in_port(version, host + 'v3/') elif version['id'] == 'v2.0': self._paste_in_port(version, host + 'v2.0/') diff --git a/keystone-moon/keystone/tests/unit/test_wsgi.py b/keystone-moon/keystone/tests/unit/test_wsgi.py index 1785dd00..62156bd5 100644 --- a/keystone-moon/keystone/tests/unit/test_wsgi.py +++ b/keystone-moon/keystone/tests/unit/test_wsgi.py @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,6 +18,7 @@ import gettext import socket import uuid +import eventlet import mock import oslo_i18n from oslo_serialization import jsonutils @@ -49,6 +52,22 @@ class FakeAttributeCheckerApp(wsgi.Application): self._require_attributes(ref, attr) +class RouterTest(tests.TestCase): + def setUp(self): + self.router = wsgi.RoutersBase() + super(RouterTest, self).setUp() + + def test_invalid_status(self): + fake_mapper = uuid.uuid4().hex + fake_controller = uuid.uuid4().hex + fake_path = uuid.uuid4().hex + fake_rel = uuid.uuid4().hex + self.assertRaises(exception.Error, + self.router._add_resource, + fake_mapper, fake_controller, fake_path, fake_rel, + status=uuid.uuid4().hex) + + class BaseWSGITest(tests.TestCase): def setUp(self): self.app = FakeApp() @@ -185,6 +204,26 @@ class ApplicationTest(BaseWSGITest): self.assertEqual(401, resp.status_int) + def test_improperly_encoded_params(self): + class FakeApp(wsgi.Application): + def index(self, context): + return context['query_string'] + # this is high bit set ASCII, copy & pasted from Windows. + # aka code page 1252. It is not valid UTF8. + req = self._make_request(url='/?name=nonexit%E8nt') + self.assertRaises(exception.ValidationError, req.get_response, + FakeApp()) + + def test_properly_encoded_params(self): + class FakeApp(wsgi.Application): + def index(self, context): + return context['query_string'] + # nonexitènt encoded as UTF-8 + req = self._make_request(url='/?name=nonexit%C3%A8nt') + resp = req.get_response(FakeApp()) + self.assertEqual({'name': u'nonexit\xe8nt'}, + jsonutils.loads(resp.body)) + class ExtensionRouterTest(BaseWSGITest): def test_extensionrouter_local_config(self): @@ -425,3 +464,43 @@ class ServerTest(tests.TestCase): 1) self.assertTrue(mock_listen.called) + + def test_client_socket_timeout(self): + # mocking server method of eventlet.wsgi to check it is called with + # configured 'client_socket_timeout' value. + for socket_timeout in range(1, 10): + self.config_fixture.config(group='eventlet_server', + client_socket_timeout=socket_timeout) + server = environment.Server(mock.MagicMock(), host=self.host, + port=self.port) + with mock.patch.object(eventlet.wsgi, 'server') as mock_server: + fake_application = uuid.uuid4().hex + fake_socket = uuid.uuid4().hex + server._run(fake_application, fake_socket) + mock_server.assert_called_once_with( + fake_socket, + fake_application, + debug=mock.ANY, + socket_timeout=socket_timeout, + log=mock.ANY, + keepalive=mock.ANY) + + def test_wsgi_keep_alive(self): + # mocking server method of eventlet.wsgi to check it is called with + # configured 'wsgi_keep_alive' value. + wsgi_keepalive = False + self.config_fixture.config(group='eventlet_server', + wsgi_keep_alive=wsgi_keepalive) + + server = environment.Server(mock.MagicMock(), host=self.host, + port=self.port) + with mock.patch.object(eventlet.wsgi, 'server') as mock_server: + fake_application = uuid.uuid4().hex + fake_socket = uuid.uuid4().hex + server._run(fake_application, fake_socket) + mock_server.assert_called_once_with(fake_socket, + fake_application, + debug=mock.ANY, + socket_timeout=mock.ANY, + log=mock.ANY, + keepalive=wsgi_keepalive) diff --git a/keystone-moon/keystone/tests/unit/tests/test_core.py b/keystone-moon/keystone/tests/unit/tests/test_core.py index 86c91a8d..2de51c52 100644 --- a/keystone-moon/keystone/tests/unit/tests/test_core.py +++ b/keystone-moon/keystone/tests/unit/tests/test_core.py @@ -47,16 +47,7 @@ class TestTestCase(tests.TestCase): lambda: warnings.warn('test sa warning error', exc.SAWarning), matchers.raises(exc.SAWarning)) - def test_deprecations(self): - # If any deprecation warnings occur during testing it's raised as - # exception. - - def use_deprecated(): - # DeprecationWarning: BaseException.message has been deprecated as - # of Python 2.6 - try: - raise Exception('something') - except Exception as e: - e.message - - self.assertThat(use_deprecated, matchers.raises(DeprecationWarning)) + def test_deprecation_warnings_are_raised_as_exceptions_in_tests(self): + self.assertThat( + lambda: warnings.warn('this is deprecated', DeprecationWarning), + matchers.raises(DeprecationWarning)) diff --git a/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py b/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py index 23fc0214..4101369c 100644 --- a/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py +++ b/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py @@ -11,17 +11,21 @@ # under the License. import datetime +import hashlib +import os import uuid from oslo_utils import timeutils from keystone.common import config +from keystone.common import utils from keystone import exception from keystone.tests import unit as tests from keystone.tests.unit import ksfixtures from keystone.token import provider from keystone.token.providers import fernet from keystone.token.providers.fernet import token_formatters +from keystone.token.providers.fernet import utils as fernet_utils CONF = config.CONF @@ -33,21 +37,21 @@ class TestFernetTokenProvider(tests.TestCase): self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) self.provider = fernet.Provider() - def test_get_token_id_raises_not_implemented(self): - """Test that an exception is raised when calling _get_token_id.""" - token_data = {} - self.assertRaises(exception.NotImplemented, - self.provider._get_token_id, token_data) + def test_supports_bind_authentication_returns_false(self): + self.assertFalse(self.provider._supports_bind_authentication) - def test_invalid_v3_token_raises_401(self): + def test_needs_persistence_returns_false(self): + self.assertFalse(self.provider.needs_persistence()) + + def test_invalid_v3_token_raises_404(self): self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.provider.validate_v3_token, uuid.uuid4().hex) - def test_invalid_v2_token_raises_401(self): + def test_invalid_v2_token_raises_404(self): self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.provider.validate_v2_token, uuid.uuid4().hex) @@ -69,7 +73,7 @@ class TestPayloads(tests.TestCase): def test_time_string_to_int_conversions(self): payload_cls = token_formatters.BasePayload - expected_time_str = timeutils.isotime() + expected_time_str = utils.isotime(subsecond=True) time_obj = timeutils.parse_isotime(expected_time_str) expected_time_int = ( (timeutils.normalize_time(time_obj) - @@ -86,7 +90,7 @@ class TestPayloads(tests.TestCase): def test_unscoped_payload(self): exp_user_id = uuid.uuid4().hex exp_methods = ['password'] - exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) exp_audit_ids = [provider.random_urlsafe_str()] payload = token_formatters.UnscopedPayload.assemble( @@ -104,7 +108,7 @@ class TestPayloads(tests.TestCase): exp_user_id = uuid.uuid4().hex exp_methods = ['password'] exp_project_id = uuid.uuid4().hex - exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) exp_audit_ids = [provider.random_urlsafe_str()] payload = token_formatters.ProjectScopedPayload.assemble( @@ -124,7 +128,7 @@ class TestPayloads(tests.TestCase): exp_user_id = uuid.uuid4().hex exp_methods = ['password'] exp_domain_id = uuid.uuid4().hex - exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) exp_audit_ids = [provider.random_urlsafe_str()] payload = token_formatters.DomainScopedPayload.assemble( @@ -144,7 +148,7 @@ class TestPayloads(tests.TestCase): exp_user_id = uuid.uuid4().hex exp_methods = ['password'] exp_domain_id = CONF.identity.default_domain_id - exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) exp_audit_ids = [provider.random_urlsafe_str()] payload = token_formatters.DomainScopedPayload.assemble( @@ -164,7 +168,128 @@ class TestPayloads(tests.TestCase): exp_user_id = uuid.uuid4().hex exp_methods = ['password'] exp_project_id = uuid.uuid4().hex - exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + exp_trust_id = uuid.uuid4().hex + + payload = token_formatters.TrustScopedPayload.assemble( + exp_user_id, exp_methods, exp_project_id, exp_expires_at, + exp_audit_ids, exp_trust_id) + + (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( + token_formatters.TrustScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_project_id, project_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + self.assertEqual(exp_trust_id, trust_id) + + def test_unscoped_payload_with_non_uuid_user_id(self): + exp_user_id = 'someNonUuidUserId' + exp_methods = ['password'] + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.UnscopedPayload.assemble( + exp_user_id, exp_methods, exp_expires_at, exp_audit_ids) + + (user_id, methods, expires_at, audit_ids) = ( + token_formatters.UnscopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_project_scoped_payload_with_non_uuid_user_id(self): + exp_user_id = 'someNonUuidUserId' + exp_methods = ['password'] + exp_project_id = uuid.uuid4().hex + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.ProjectScopedPayload.assemble( + exp_user_id, exp_methods, exp_project_id, exp_expires_at, + exp_audit_ids) + + (user_id, methods, project_id, expires_at, audit_ids) = ( + token_formatters.ProjectScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_project_id, project_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_project_scoped_payload_with_non_uuid_project_id(self): + exp_user_id = uuid.uuid4().hex + exp_methods = ['password'] + exp_project_id = 'someNonUuidProjectId' + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.ProjectScopedPayload.assemble( + exp_user_id, exp_methods, exp_project_id, exp_expires_at, + exp_audit_ids) + + (user_id, methods, project_id, expires_at, audit_ids) = ( + token_formatters.ProjectScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_project_id, project_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_domain_scoped_payload_with_non_uuid_user_id(self): + exp_user_id = 'someNonUuidUserId' + exp_methods = ['password'] + exp_domain_id = uuid.uuid4().hex + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.DomainScopedPayload.assemble( + exp_user_id, exp_methods, exp_domain_id, exp_expires_at, + exp_audit_ids) + + (user_id, methods, domain_id, expires_at, audit_ids) = ( + token_formatters.DomainScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_domain_id, domain_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_trust_scoped_payload_with_non_uuid_user_id(self): + exp_user_id = 'someNonUuidUserId' + exp_methods = ['password'] + exp_project_id = uuid.uuid4().hex + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + exp_trust_id = uuid.uuid4().hex + + payload = token_formatters.TrustScopedPayload.assemble( + exp_user_id, exp_methods, exp_project_id, exp_expires_at, + exp_audit_ids, exp_trust_id) + + (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( + token_formatters.TrustScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_project_id, project_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + self.assertEqual(exp_trust_id, trust_id) + + def test_trust_scoped_payload_with_non_uuid_project_id(self): + exp_user_id = uuid.uuid4().hex + exp_methods = ['password'] + exp_project_id = 'someNonUuidProjectId' + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) exp_audit_ids = [provider.random_urlsafe_str()] exp_trust_id = uuid.uuid4().hex @@ -181,3 +306,218 @@ class TestPayloads(tests.TestCase): self.assertEqual(exp_expires_at, expires_at) self.assertEqual(exp_audit_ids, audit_ids) self.assertEqual(exp_trust_id, trust_id) + + def test_federated_payload_with_non_uuid_ids(self): + exp_user_id = 'someNonUuidUserId' + exp_methods = ['password'] + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}], + 'idp_id': uuid.uuid4().hex, + 'protocol_id': uuid.uuid4().hex} + + payload = token_formatters.FederatedUnscopedPayload.assemble( + exp_user_id, exp_methods, exp_expires_at, exp_audit_ids, + exp_federated_info) + + (user_id, methods, expires_at, audit_ids, federated_info) = ( + token_formatters.FederatedUnscopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + self.assertEqual(exp_federated_info['group_ids'][0]['id'], + federated_info['group_ids'][0]['id']) + self.assertEqual(exp_federated_info['idp_id'], + federated_info['idp_id']) + self.assertEqual(exp_federated_info['protocol_id'], + federated_info['protocol_id']) + + def test_federated_project_scoped_payload(self): + exp_user_id = 'someNonUuidUserId' + exp_methods = ['token'] + exp_project_id = uuid.uuid4().hex + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}], + 'idp_id': uuid.uuid4().hex, + 'protocol_id': uuid.uuid4().hex} + + payload = token_formatters.FederatedProjectScopedPayload.assemble( + exp_user_id, exp_methods, exp_project_id, exp_expires_at, + exp_audit_ids, exp_federated_info) + + (user_id, methods, project_id, expires_at, audit_ids, + federated_info) = ( + token_formatters.FederatedProjectScopedPayload.disassemble( + payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_project_id, project_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + self.assertDictEqual(exp_federated_info, federated_info) + + def test_federated_domain_scoped_payload(self): + exp_user_id = 'someNonUuidUserId' + exp_methods = ['token'] + exp_domain_id = uuid.uuid4().hex + exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) + exp_audit_ids = [provider.random_urlsafe_str()] + exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}], + 'idp_id': uuid.uuid4().hex, + 'protocol_id': uuid.uuid4().hex} + + payload = token_formatters.FederatedDomainScopedPayload.assemble( + exp_user_id, exp_methods, exp_domain_id, exp_expires_at, + exp_audit_ids, exp_federated_info) + + (user_id, methods, domain_id, expires_at, audit_ids, + federated_info) = ( + token_formatters.FederatedDomainScopedPayload.disassemble( + payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_domain_id, domain_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + self.assertDictEqual(exp_federated_info, federated_info) + + +class TestFernetKeyRotation(tests.TestCase): + def setUp(self): + super(TestFernetKeyRotation, self).setUp() + + # A collection of all previously-seen signatures of the key + # repository's contents. + self.key_repo_signatures = set() + + @property + def keys(self): + """Key files converted to numbers.""" + return sorted( + int(x) for x in os.listdir(CONF.fernet_tokens.key_repository)) + + @property + def key_repository_size(self): + """The number of keys in the key repository.""" + return len(self.keys) + + @property + def key_repository_signature(self): + """Create a "thumbprint" of the current key repository. + + Because key files are renamed, this produces a hash of the contents of + the key files, ignoring their filenames. + + The resulting signature can be used, for example, to ensure that you + have a unique set of keys after you perform a key rotation (taking a + static set of keys, and simply shuffling them, would fail such a test). + + """ + # Load the keys into a list. + keys = fernet_utils.load_keys() + + # Sort the list of keys by the keys themselves (they were previously + # sorted by filename). + keys.sort() + + # Create the thumbprint using all keys in the repository. + signature = hashlib.sha1() + for key in keys: + signature.update(key) + return signature.hexdigest() + + def assertRepositoryState(self, expected_size): + """Validate the state of the key repository.""" + self.assertEqual(expected_size, self.key_repository_size) + self.assertUniqueRepositoryState() + + def assertUniqueRepositoryState(self): + """Ensures that the current key repo state has not been seen before.""" + # This is assigned to a variable because it takes some work to + # calculate. + signature = self.key_repository_signature + + # Ensure the signature is not in the set of previously seen signatures. + self.assertNotIn(signature, self.key_repo_signatures) + + # Add the signature to the set of repository signatures to validate + # that we don't see it again later. + self.key_repo_signatures.add(signature) + + def test_rotation(self): + # Initializing a key repository results in this many keys. We don't + # support max_active_keys being set any lower. + min_active_keys = 2 + + # Simulate every rotation strategy up to "rotating once a week while + # maintaining a year's worth of keys." + for max_active_keys in range(min_active_keys, 52 + 1): + self.config_fixture.config(group='fernet_tokens', + max_active_keys=max_active_keys) + + # Ensure that resetting the key repository always results in 2 + # active keys. + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + # Validate the initial repository state. + self.assertRepositoryState(expected_size=min_active_keys) + + # The repository should be initialized with a staged key (0) and a + # primary key (1). The next key is just auto-incremented. + exp_keys = [0, 1] + next_key_number = exp_keys[-1] + 1 # keep track of next key + self.assertEqual(exp_keys, self.keys) + + # Rotate the keys just enough times to fully populate the key + # repository. + for rotation in range(max_active_keys - min_active_keys): + fernet_utils.rotate_keys() + self.assertRepositoryState(expected_size=rotation + 3) + + exp_keys.append(next_key_number) + next_key_number += 1 + self.assertEqual(exp_keys, self.keys) + + # We should have a fully populated key repository now. + self.assertEqual(max_active_keys, self.key_repository_size) + + # Rotate an additional number of times to ensure that we maintain + # the desired number of active keys. + for rotation in range(10): + fernet_utils.rotate_keys() + self.assertRepositoryState(expected_size=max_active_keys) + + exp_keys.pop(1) + exp_keys.append(next_key_number) + next_key_number += 1 + self.assertEqual(exp_keys, self.keys) + + def test_non_numeric_files(self): + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + evil_file = os.path.join(CONF.fernet_tokens.key_repository, '99.bak') + with open(evil_file, 'w'): + pass + fernet_utils.rotate_keys() + self.assertTrue(os.path.isfile(evil_file)) + keys = 0 + for x in os.listdir(CONF.fernet_tokens.key_repository): + if x == '99.bak': + continue + keys += 1 + self.assertEqual(3, keys) + + +class TestLoadKeys(tests.TestCase): + def test_non_numeric_files(self): + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + evil_file = os.path.join(CONF.fernet_tokens.key_repository, '~1') + with open(evil_file, 'w'): + pass + keys = fernet_utils.load_keys() + self.assertEqual(2, len(keys)) + self.assertTrue(len(keys[0])) diff --git a/keystone-moon/keystone/tests/unit/token/test_pki_provider.py b/keystone-moon/keystone/tests/unit/token/test_pki_provider.py new file mode 100644 index 00000000..dad31266 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_pki_provider.py @@ -0,0 +1,26 @@ +# 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.tests import unit as tests +from keystone.token.providers import pki + + +class TestPkiTokenProvider(tests.TestCase): + def setUp(self): + super(TestPkiTokenProvider, self).setUp() + self.provider = pki.Provider() + + def test_supports_bind_authentication_returns_true(self): + self.assertTrue(self.provider._supports_bind_authentication) + + def test_need_persistence_return_true(self): + self.assertIs(True, self.provider.needs_persistence()) diff --git a/keystone-moon/keystone/tests/unit/token/test_pkiz_provider.py b/keystone-moon/keystone/tests/unit/token/test_pkiz_provider.py new file mode 100644 index 00000000..4a492bc1 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_pkiz_provider.py @@ -0,0 +1,26 @@ +# 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.tests import unit as tests +from keystone.token.providers import pkiz + + +class TestPkizTokenProvider(tests.TestCase): + def setUp(self): + super(TestPkizTokenProvider, self).setUp() + self.provider = pkiz.Provider() + + def test_supports_bind_authentication_returns_true(self): + self.assertTrue(self.provider._supports_bind_authentication) + + def test_need_persistence_return_true(self): + self.assertIs(True, self.provider.needs_persistence()) diff --git a/keystone-moon/keystone/tests/unit/token/test_provider.py b/keystone-moon/keystone/tests/unit/token/test_provider.py index e5910690..be831484 100644 --- a/keystone-moon/keystone/tests/unit/token/test_provider.py +++ b/keystone-moon/keystone/tests/unit/token/test_provider.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import urllib +import six +from six.moves import urllib from keystone.tests import unit from keystone.token import provider @@ -19,11 +20,11 @@ from keystone.token import provider class TestRandomStrings(unit.BaseTestCase): def test_strings_are_url_safe(self): s = provider.random_urlsafe_str() - self.assertEqual(s, urllib.quote_plus(s)) + self.assertEqual(s, urllib.parse.quote_plus(s)) def test_strings_can_be_converted_to_bytes(self): s = provider.random_urlsafe_str() - self.assertTrue(isinstance(s, basestring)) + self.assertTrue(isinstance(s, six.string_types)) b = provider.random_urlsafe_str_to_bytes(s) self.assertTrue(isinstance(b, bytes)) diff --git a/keystone-moon/keystone/tests/unit/token/test_token_model.py b/keystone-moon/keystone/tests/unit/token/test_token_model.py index b2474289..3959d901 100644 --- a/keystone-moon/keystone/tests/unit/token/test_token_model.py +++ b/keystone-moon/keystone/tests/unit/token/test_token_model.py @@ -15,7 +15,9 @@ import uuid from oslo_config import cfg from oslo_utils import timeutils +from six.moves import range +from keystone.contrib.federation import constants as federation_constants from keystone import exception from keystone.models import token_model from keystone.tests.unit import core @@ -127,7 +129,7 @@ class TestKeystoneTokenModel(core.TestCase): self.assertIsNone(token_data.federation_protocol_id) self.assertIsNone(token_data.federation_idp_id) - token_data['user'][token_model.federation.FEDERATION] = federation_data + token_data['user'][federation_constants.FEDERATION] = federation_data self.assertTrue(token_data.is_federated_user) self.assertEqual([x['id'] for x in federation_data['groups']], @@ -149,7 +151,7 @@ class TestKeystoneTokenModel(core.TestCase): self.assertIsNone(token_data.federation_protocol_id) self.assertIsNone(token_data.federation_idp_id) - token_data['user'][token_model.federation.FEDERATION] = federation_data + token_data['user'][federation_constants.FEDERATION] = federation_data # Federated users should not exist in V2, the data should remain empty self.assertFalse(token_data.is_federated_user) diff --git a/keystone-moon/keystone/tests/unit/token/test_uuid_provider.py b/keystone-moon/keystone/tests/unit/token/test_uuid_provider.py new file mode 100644 index 00000000..b49427f0 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_uuid_provider.py @@ -0,0 +1,26 @@ +# 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.tests import unit as tests +from keystone.token.providers import uuid + + +class TestUuidTokenProvider(tests.TestCase): + def setUp(self): + super(TestUuidTokenProvider, self).setUp() + self.provider = uuid.Provider() + + def test_supports_bind_authentication_returns_true(self): + self.assertTrue(self.provider._supports_bind_authentication) + + def test_need_persistence_return_true(self): + self.assertIs(True, self.provider.needs_persistence()) diff --git a/keystone-moon/keystone/token/controllers.py b/keystone-moon/keystone/token/controllers.py index 3304acb5..ff65e733 100644 --- a/keystone-moon/keystone/token/controllers.py +++ b/keystone-moon/keystone/token/controllers.py @@ -15,6 +15,7 @@ import datetime import sys +from keystone.common import utils from keystoneclient.common import cms from oslo_config import cfg from oslo_log import log @@ -118,7 +119,8 @@ class Auth(controller.V2Controller): # format. user_ref = self.v3_to_v2_user(user_ref) if tenant_ref: - tenant_ref = self.filter_domain_id(tenant_ref) + tenant_ref = self.v3_to_v2_project(tenant_ref) + auth_token_data = self._get_auth_token_data(user_ref, tenant_ref, metadata_ref, @@ -193,8 +195,9 @@ class Auth(controller.V2Controller): if not CONF.trust.enabled and 'trust_id' in auth: raise exception.Forbidden('Trusts are disabled.') elif CONF.trust.enabled and 'trust_id' in auth: - trust_ref = self.trust_api.get_trust(auth['trust_id']) - if trust_ref is None: + try: + trust_ref = self.trust_api.get_trust(auth['trust_id']) + except exception.TrustNotFound: raise exception.Forbidden() if user_id != trust_ref['trustee_user_id']: raise exception.Forbidden() @@ -203,7 +206,7 @@ class Auth(controller.V2Controller): raise exception.Forbidden() if ('expires' in trust_ref) and (trust_ref['expires']): expiry = trust_ref['expires'] - if expiry < timeutils.parse_isotime(timeutils.isotime()): + if expiry < timeutils.parse_isotime(utils.isotime()): raise exception.Forbidden() user_id = trust_ref['trustor_user_id'] trustor_user_ref = self.identity_api.get_user( @@ -385,7 +388,8 @@ class Auth(controller.V2Controller): role_list = self.assignment_api.get_roles_for_user_and_project( user_id, tenant_id) except exception.ProjectNotFound: - pass + msg = _('Project ID not found: %(t_id)s') % {'t_id': tenant_id} + raise exception.Unauthorized(msg) if not role_list: msg = _('User %(u_id)s is unauthorized for tenant %(t_id)s') @@ -460,7 +464,7 @@ class Auth(controller.V2Controller): for t in tokens: expires = t['expires'] if expires and isinstance(expires, datetime.datetime): - t['expires'] = timeutils.isotime(expires) + t['expires'] = utils.isotime(expires) data = {'revoked': tokens} json_data = jsonutils.dumps(data) signed_text = cms.cms_sign_text(json_data, @@ -508,8 +512,8 @@ class Auth(controller.V2Controller): return {} endpoints = [] - for region_name, region_ref in six.iteritems(catalog_ref): - for service_type, service_ref in six.iteritems(region_ref): + for region_name, region_ref in catalog_ref.items(): + for service_type, service_ref in region_ref.items(): endpoints.append({ 'id': service_ref.get('id'), 'name': service_ref.get('name'), diff --git a/keystone-moon/keystone/token/persistence/__init__.py b/keystone-moon/keystone/token/persistence/__init__.py index 29ad5653..89ec875d 100644 --- a/keystone-moon/keystone/token/persistence/__init__.py +++ b/keystone-moon/keystone/token/persistence/__init__.py @@ -13,4 +13,4 @@ from keystone.token.persistence.core import * # noqa -__all__ = ['Manager', 'Driver', 'backends'] +__all__ = ['Manager', 'Driver'] diff --git a/keystone-moon/keystone/token/persistence/backends/kvs.py b/keystone-moon/keystone/token/persistence/backends/kvs.py index b4807bf1..1bd08a31 100644 --- a/keystone-moon/keystone/token/persistence/backends/kvs.py +++ b/keystone-moon/keystone/token/persistence/backends/kvs.py @@ -22,6 +22,7 @@ from oslo_utils import timeutils import six from keystone.common import kvs +from keystone.common import utils from keystone import exception from keystone.i18n import _, _LE, _LW from keystone import token @@ -56,10 +57,8 @@ class Token(token.persistence.Driver): # is instantiated. LOG.warn(_LW('It is recommended to only use the base ' 'key-value-store implementation for the token driver ' - 'for testing purposes. Please use ' - 'keystone.token.persistence.backends.memcache.Token ' - 'or keystone.token.persistence.backends.sql.Token ' - 'instead.')) + "for testing purposes. Please use 'memcache' or " + "'sql' instead.")) def _prefix_token_id(self, token_id): return 'token-%s' % token_id.encode('utf-8') @@ -108,7 +107,7 @@ class Token(token.persistence.Driver): # concern about the backend, always store the value(s) in the # index as the isotime (string) version so this is where the string is # built. - expires_str = timeutils.isotime(data_copy['expires'], subsecond=True) + expires_str = utils.isotime(data_copy['expires'], subsecond=True) self._set_key(ptk, data_copy) user_id = data['user']['id'] @@ -207,8 +206,8 @@ class Token(token.persistence.Driver): 'revocation list.'), data['id']) return - revoked_token_data['expires'] = timeutils.isotime(expires, - subsecond=True) + revoked_token_data['expires'] = utils.isotime(expires, + subsecond=True) revoked_token_data['id'] = data['id'] token_list = self._get_key_or_default(self.revocation_key, default=[]) diff --git a/keystone-moon/keystone/token/persistence/backends/sql.py b/keystone-moon/keystone/token/persistence/backends/sql.py index fc70fb92..08c3a216 100644 --- a/keystone-moon/keystone/token/persistence/backends/sql.py +++ b/keystone-moon/keystone/token/persistence/backends/sql.py @@ -127,6 +127,7 @@ class Token(token.persistence.Driver): """ session = sql.get_session() + token_list = [] with session.begin(): now = timeutils.utcnow() query = session.query(TokenModel) @@ -148,6 +149,9 @@ class Token(token.persistence.Driver): continue token_ref.valid = False + token_list.append(token_ref.id) + + return token_list def _tenant_matches(self, tenant_id, token_ref_dict): return ((tenant_id is None) or diff --git a/keystone-moon/keystone/token/persistence/core.py b/keystone-moon/keystone/token/persistence/core.py index 19f0df35..15a58085 100644 --- a/keystone-moon/keystone/token/persistence/core.py +++ b/keystone-moon/keystone/token/persistence/core.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Main entry point into the Token persistence service.""" +"""Main entry point into the Token Persistence service.""" import abc import copy @@ -27,6 +27,7 @@ from keystone.common import dependency from keystone.common import manager from keystone import exception from keystone.i18n import _LW +from keystone.token import utils CONF = cfg.CONF @@ -39,13 +40,15 @@ REVOCATION_MEMOIZE = cache.get_memoization_decorator( @dependency.requires('assignment_api', 'identity_api', 'resource_api', 'token_provider_api', 'trust_api') class PersistenceManager(manager.Manager): - """Default pivot point for the Token backend. + """Default pivot point for the Token Persistence backend. See :mod:`keystone.common.manager.Manager` for more details on how this dynamically calls the backend. """ + driver_namespace = 'keystone.token.persistence' + def __init__(self): super(PersistenceManager, self).__init__(CONF.token.driver) @@ -62,7 +65,7 @@ class PersistenceManager(manager.Manager): # context['token_id'] will in-fact be None. This also saves # a round-trip to the backend if we don't have a token_id. raise exception.TokenNotFound(token_id='') - unique_id = self.token_provider_api.unique_id(token_id) + unique_id = utils.generate_unique_id(token_id) token_ref = self._get_token(unique_id) # NOTE(morganfainberg): Lift expired checking to the manager, there is # no reason to make the drivers implement this check. With caching, @@ -77,7 +80,7 @@ class PersistenceManager(manager.Manager): return self.driver.get_token(token_id) def create_token(self, token_id, data): - unique_id = self.token_provider_api.unique_id(token_id) + unique_id = utils.generate_unique_id(token_id) data_copy = copy.deepcopy(data) data_copy['id'] = unique_id ret = self.driver.create_token(unique_id, data_copy) @@ -91,7 +94,7 @@ class PersistenceManager(manager.Manager): def delete_token(self, token_id): if not CONF.token.revoke_by_id: return - unique_id = self.token_provider_api.unique_id(token_id) + unique_id = utils.generate_unique_id(token_id) self.driver.delete_token(unique_id) self._invalidate_individual_token_cache(unique_id) self.invalidate_revocation_list() @@ -100,11 +103,10 @@ class PersistenceManager(manager.Manager): consumer_id=None): if not CONF.token.revoke_by_id: return - token_list = self.driver._list_tokens(user_id, tenant_id, trust_id, - consumer_id) - self.driver.delete_tokens(user_id, tenant_id, trust_id, consumer_id) + token_list = self.driver.delete_tokens(user_id, tenant_id, trust_id, + consumer_id) for token_id in token_list: - unique_id = self.token_provider_api.unique_id(token_id) + unique_id = utils.generate_unique_id(token_id) self._invalidate_individual_token_cache(unique_id) self.invalidate_revocation_list() @@ -196,11 +198,6 @@ class PersistenceManager(manager.Manager): self.token_provider_api.invalidate_individual_token_cache(token_id) -# NOTE(morganfainberg): @dependency.optional() is required here to ensure the -# class-level optional dependency control attribute is populated as empty -# this is because of the override of .__getattr__ and ensures that if the -# optional dependency injector changes attributes, this class doesn't break. -@dependency.optional() @dependency.requires('token_provider_api') @dependency.provider('token_api') class Manager(object): @@ -306,7 +303,7 @@ class Driver(object): :type trust_id: string :param consumer_id: identity of the consumer :type consumer_id: string - :returns: None. + :returns: The tokens that have been deleted. :raises: keystone.exception.TokenNotFound """ @@ -322,6 +319,7 @@ class Driver(object): self.delete_token(token) except exception.NotFound: pass + return token_list @abc.abstractmethod def _list_tokens(self, user_id, tenant_id=None, trust_id=None, diff --git a/keystone-moon/keystone/token/provider.py b/keystone-moon/keystone/token/provider.py index fb41d4bb..1422e41f 100644 --- a/keystone-moon/keystone/token/provider.py +++ b/keystone-moon/keystone/token/provider.py @@ -20,7 +20,6 @@ import datetime import sys import uuid -from keystoneclient.common import cms from oslo_config import cfg from oslo_log import log from oslo_utils import timeutils @@ -34,6 +33,7 @@ from keystone.i18n import _, _LE from keystone.models import token_model from keystone import notifications from keystone.token import persistence +from keystone.token import utils CONF = cfg.CONF @@ -110,6 +110,8 @@ class Manager(manager.Manager): """ + driver_namespace = 'keystone.token.provider' + V2 = V2 V3 = V3 VERSIONS = VERSIONS @@ -145,7 +147,7 @@ class Manager(manager.Manager): ] } - for event, cb_info in six.iteritems(callbacks): + for event, cb_info in callbacks.items(): for resource_type, callback_fns in cb_info: notifications.register_event_callback(event, resource_type, callback_fns) @@ -164,18 +166,6 @@ class Manager(manager.Manager): self._persistence_manager = persistence.PersistenceManager() return self._persistence_manager - def unique_id(self, token_id): - """Return a unique ID for a token. - - The returned value is useful as the primary key of a database table, - memcache store, or other lookup table. - - :returns: Given a PKI token, returns it's hashed value. Otherwise, - returns the passed-in value (such as a UUID token ID or an - existing hash). - """ - return cms.cms_hash_token(token_id, mode=CONF.token.hash_algorithm) - def _create_token(self, token_id, token_data): try: if isinstance(token_data['expires'], six.string_types): @@ -192,7 +182,7 @@ class Manager(manager.Manager): six.reraise(*exc_info) def validate_token(self, token_id, belongs_to=None): - unique_id = self.unique_id(token_id) + unique_id = utils.generate_unique_id(token_id) # NOTE(morganfainberg): Ensure we never use the long-form token_id # (PKI) as part of the cache_key. token = self._validate_token(unique_id) @@ -211,7 +201,7 @@ class Manager(manager.Manager): self.revoke_api.check_token(token_values) def validate_v2_token(self, token_id, belongs_to=None): - unique_id = self.unique_id(token_id) + unique_id = utils.generate_unique_id(token_id) if self._needs_persistence: # NOTE(morganfainberg): Ensure we never use the long-form token_id # (PKI) as part of the cache_key. @@ -219,6 +209,7 @@ class Manager(manager.Manager): else: token_ref = token_id token = self._validate_v2_token(token_ref) + token['access']['token']['id'] = token_id self._token_belongs_to(token, belongs_to) self._is_valid_token(token) return token @@ -239,7 +230,7 @@ class Manager(manager.Manager): return self.check_revocation_v3(token) def validate_v3_token(self, token_id): - unique_id = self.unique_id(token_id) + unique_id = utils.generate_unique_id(token_id) # NOTE(lbragstad): Only go to persistent storage if we have a token to # fetch from the backend. If the Fernet token provider is being used # this step isn't necessary. The Fernet token reference is persisted in diff --git a/keystone-moon/keystone/token/providers/common.py b/keystone-moon/keystone/token/providers/common.py index 717e1495..b71458cd 100644 --- a/keystone-moon/keystone/token/providers/common.py +++ b/keystone-moon/keystone/token/providers/common.py @@ -14,17 +14,17 @@ from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_serialization import jsonutils -from oslo_utils import timeutils import six from six.moves.urllib import parse from keystone.common import controller as common_controller from keystone.common import dependency -from keystone.contrib import federation +from keystone.common import utils +from keystone.contrib.federation import constants as federation_constants from keystone import exception from keystone.i18n import _, _LE -from keystone.openstack.common import versionutils from keystone import token from keystone.token import provider @@ -37,18 +37,33 @@ CONF = cfg.CONF class V2TokenDataHelper(object): """Creates V2 token data.""" - def v3_to_v2_token(self, token_id, v3_token_data): + def v3_to_v2_token(self, v3_token_data): token_data = {} # Build v2 token v3_token = v3_token_data['token'] token = {} - token['id'] = token_id token['expires'] = v3_token.get('expires_at') token['issued_at'] = v3_token.get('issued_at') token['audit_ids'] = v3_token.get('audit_ids') + # Bail immediately if this is a domain-scoped token, which is not + # supported by the v2 API at all. + if 'domain' in v3_token: + raise exception.Unauthorized(_( + 'Domains are not supported by the v2 API. Please use the v3 ' + 'API instead.')) + + # Bail if this is a project-scoped token outside the default domain, + # which may result in a namespace collision with a project inside the + # default domain. if 'project' in v3_token: + if (v3_token['project']['domain']['id'] != + CONF.identity.default_domain_id): + raise exception.Unauthorized(_( + 'Project not found in the default domain (please use the ' + 'v3 API instead): %s') % v3_token['project']['id']) + # v3 token_data does not contain all tenant attributes tenant = self.resource_api.get_project( v3_token['project']['id']) @@ -58,14 +73,32 @@ class V2TokenDataHelper(object): # Build v2 user v3_user = v3_token['user'] + + # Bail if this is a token outside the default domain, + # which may result in a namespace collision with a project inside the + # default domain. + if ('domain' in v3_user and v3_user['domain']['id'] != + CONF.identity.default_domain_id): + raise exception.Unauthorized(_( + 'User not found in the default domain (please use the v3 API ' + 'instead): %s') % v3_user['id']) + user = common_controller.V2Controller.v3_to_v2_user(v3_user) + # Maintain Trust Data + if 'OS-TRUST:trust' in v3_token: + v3_trust_data = v3_token['OS-TRUST:trust'] + token_data['trust'] = { + 'trustee_user_id': v3_trust_data['trustee_user']['id'], + 'id': v3_trust_data['id'], + 'trustor_user_id': v3_trust_data['trustor_user']['id'], + 'impersonation': v3_trust_data['impersonation'] + } + # Set user roles user['roles'] = [] role_ids = [] for role in v3_token.get('roles', []): - # Filter role id since it's not included in v2 token response - role_ids.append(role.pop('id')) user['roles'].append(role) user['roles_links'] = [] @@ -99,7 +132,7 @@ class V2TokenDataHelper(object): expires = token_ref.get('expires', provider.default_expire_time()) if expires is not None: if not isinstance(expires, six.text_type): - expires = timeutils.isotime(expires) + expires = utils.isotime(expires) token_data = token_ref.get('token_data') if token_data: @@ -112,7 +145,7 @@ class V2TokenDataHelper(object): o = {'access': {'token': {'id': token_ref['id'], 'expires': expires, - 'issued_at': timeutils.strtime(), + 'issued_at': utils.strtime(), 'audit_ids': audit_info }, 'user': {'id': user_ref['id'], @@ -181,8 +214,8 @@ class V2TokenDataHelper(object): return [] services = {} - for region, region_ref in six.iteritems(catalog_ref): - for service, service_ref in six.iteritems(region_ref): + for region, region_ref in catalog_ref.items(): + for service, service_ref in region_ref.items(): new_service_ref = services.get(service, {}) new_service_ref['name'] = service_ref.pop('name') new_service_ref['type'] = service @@ -195,7 +228,7 @@ class V2TokenDataHelper(object): new_service_ref['endpoints'] = endpoints_ref services[service] = new_service_ref - return services.values() + return list(services.values()) @dependency.requires('assignment_api', 'catalog_api', 'federation_api', @@ -239,10 +272,26 @@ class V3TokenDataHelper(object): user_id, project_id) return [self.role_api.get_role(role_id) for role_id in roles] - def _populate_roles_for_groups(self, group_ids, - project_id=None, domain_id=None, - user_id=None): - def _check_roles(roles, user_id, project_id, domain_id): + def populate_roles_for_groups(self, token_data, group_ids, + project_id=None, domain_id=None, + user_id=None): + """Populate roles basing on provided groups and project/domain + + Used for ephemeral users with dynamically assigned groups. + This method does not return anything, yet it modifies token_data in + place. + + :param token_data: a dictionary used for building token response + :group_ids: list of group IDs a user is a member of + :project_id: project ID to scope to + :domain_id: domain ID to scope to + :user_id: user ID + + :raises: exception.Unauthorized - when no roles were found for a + (group_ids, project_id) or (group_ids, domain_id) pairs. + + """ + def check_roles(roles, user_id, project_id, domain_id): # User was granted roles so simply exit this function. if roles: return @@ -264,8 +313,8 @@ class V3TokenDataHelper(object): roles = self.assignment_api.get_roles_for_groups(group_ids, project_id, domain_id) - _check_roles(roles, user_id, project_id, domain_id) - return roles + check_roles(roles, user_id, project_id, domain_id) + token_data['roles'] = roles def _populate_user(self, token_data, user_id, trust): if 'user' in token_data: @@ -393,10 +442,10 @@ class V3TokenDataHelper(object): if not expires: expires = provider.default_expire_time() if not isinstance(expires, six.string_types): - expires = timeutils.isotime(expires, subsecond=True) + expires = utils.isotime(expires, subsecond=True) token_data['expires_at'] = expires token_data['issued_at'] = (issued_at or - timeutils.isotime(subsecond=True)) + utils.isotime(subsecond=True)) def _populate_audit_info(self, token_data, audit_info=None): if audit_info is None or isinstance(audit_info, six.string_types): @@ -420,7 +469,7 @@ class V3TokenDataHelper(object): versionutils.deprecated( what='passing token data with "extras"', as_of=versionutils.deprecated.KILO, - in_favor_of='well-defined APIs') + in_favor_of='well-defined APIs')(lambda: None)() token_data = {'methods': method_names, 'extras': extras} @@ -490,13 +539,21 @@ class BaseProvider(provider.Provider): return token_id, token_data def _is_mapped_token(self, auth_context): - return (federation.IDENTITY_PROVIDER in auth_context and - federation.PROTOCOL in auth_context) + return (federation_constants.IDENTITY_PROVIDER in auth_context and + federation_constants.PROTOCOL in auth_context) def issue_v3_token(self, user_id, method_names, expires_at=None, project_id=None, domain_id=None, auth_context=None, trust=None, metadata_ref=None, include_catalog=True, parent_audit_id=None): + if auth_context and auth_context.get('bind'): + # NOTE(lbragstad): Check if the token provider being used actually + # supports bind authentication methods before proceeding. + if not self._supports_bind_authentication: + raise exception.NotImplemented(_( + 'The configured token provider does not support bind ' + 'authentication.')) + # for V2, trust is stashed in metadata_ref if (CONF.trust.enabled and not trust and metadata_ref and 'trust_id' in metadata_ref): @@ -530,38 +587,30 @@ class BaseProvider(provider.Provider): return token_id, token_data def _handle_mapped_tokens(self, auth_context, project_id, domain_id): - def get_federated_domain(): - return (CONF.federation.federated_domain_name or - federation.FEDERATED_DOMAIN_KEYWORD) - - federated_domain = get_federated_domain() user_id = auth_context['user_id'] group_ids = auth_context['group_ids'] - idp = auth_context[federation.IDENTITY_PROVIDER] - protocol = auth_context[federation.PROTOCOL] + idp = auth_context[federation_constants.IDENTITY_PROVIDER] + protocol = auth_context[federation_constants.PROTOCOL] token_data = { 'user': { 'id': user_id, 'name': parse.unquote(user_id), - federation.FEDERATION: { + federation_constants.FEDERATION: { + 'groups': [{'id': x} for x in group_ids], 'identity_provider': {'id': idp}, 'protocol': {'id': protocol} }, 'domain': { - 'id': federated_domain, - 'name': federated_domain + 'id': CONF.federation.federated_domain_name, + 'name': CONF.federation.federated_domain_name } } } if project_id or domain_id: - roles = self.v3_token_data_helper._populate_roles_for_groups( - group_ids, project_id, domain_id, user_id) - token_data.update({'roles': roles}) - else: - token_data['user'][federation.FEDERATION].update({ - 'groups': [{'id': x} for x in group_ids] - }) + self.v3_token_data_helper.populate_roles_for_groups( + token_data, group_ids, project_id, domain_id, user_id) + return token_data def _verify_token_ref(self, token_ref): @@ -637,30 +686,10 @@ class BaseProvider(provider.Provider): # management layer is now pluggable, one can always provide # their own implementation to suit their needs. token_data = token_ref.get('token_data') - if (not token_data or - self.get_token_version(token_data) != - token.provider.V2): - # token is created by old v2 logic - metadata_ref = token_ref['metadata'] - roles_ref = [] - for role_id in metadata_ref.get('roles', []): - roles_ref.append(self.role_api.get_role(role_id)) - - # Get a service catalog if possible - # This is needed for on-behalf-of requests - catalog_ref = None - if token_ref.get('tenant'): - catalog_ref = self.catalog_api.get_catalog( - token_ref['user']['id'], - token_ref['tenant']['id']) - - trust_ref = None - if CONF.trust.enabled and 'trust_id' in metadata_ref: - trust_ref = self.trust_api.get_trust( - metadata_ref['trust_id']) - - token_data = self.v2_token_data_helper.format_token( - token_ref, roles_ref, catalog_ref, trust_ref) + if (self.get_token_version(token_data) != token.provider.V2): + # Validate the V3 token as V2 + token_data = self.v2_token_data_helper.v3_to_v2_token( + token_data) trust_id = token_data['access'].get('trust', {}).get('id') if trust_id: diff --git a/keystone-moon/keystone/token/providers/fernet/core.py b/keystone-moon/keystone/token/providers/fernet/core.py index b1da263b..1bbacb03 100644 --- a/keystone-moon/keystone/token/providers/fernet/core.py +++ b/keystone-moon/keystone/token/providers/fernet/core.py @@ -14,7 +14,8 @@ from oslo_config import cfg from oslo_log import log from keystone.common import dependency -from keystone.contrib import federation +from keystone.common import utils as ks_utils +from keystone.contrib.federation import constants as federation_constants from keystone import exception from keystone.i18n import _ from keystone.token import provider @@ -59,6 +60,9 @@ class Provider(common.BaseProvider): if token_ref.get('tenant'): project_id = token_ref['tenant']['id'] + # maintain expiration time across rescopes + expires = token_ref.get('expires') + parent_audit_id = token_ref.get('parent_audit_id') # If parent_audit_id is defined then a token authentication was made if parent_audit_id: @@ -80,136 +84,132 @@ class Provider(common.BaseProvider): project_id=project_id, token=token_ref, include_catalog=False, - audit_info=audit_ids) + audit_info=audit_ids, + expires=expires) expires_at = v3_token_data['token']['expires_at'] token_id = self.token_formatter.create_token(user_id, expires_at, audit_ids, methods=method_names, project_id=project_id) + self._build_issued_at_info(token_id, v3_token_data) # Convert v3 to v2 token data and build v2 catalog - token_data = self.v2_token_data_helper.v3_to_v2_token(token_id, - v3_token_data) + token_data = self.v2_token_data_helper.v3_to_v2_token(v3_token_data) + token_data['access']['token']['id'] = token_id + + return token_id, token_data + def issue_v3_token(self, *args, **kwargs): + token_id, token_data = super(Provider, self).issue_v3_token( + *args, **kwargs) + self._build_issued_at_info(token_id, token_data) return token_id, token_data + def _build_issued_at_info(self, token_id, token_data): + # NOTE(roxanaghe, lbragstad): We must use the creation time that + # Fernet builds into it's token. The Fernet spec details that the + # token creation time is built into the token, outside of the payload + # provided by Keystone. This is the reason why we don't pass the + # issued_at time in the payload. This also means that we shouldn't + # return a token reference with a creation time that we created + # when Fernet uses a different creation time. We should use the + # creation time provided by Fernet because it's the creation time + # that we have to rely on when we validate the token. + fernet_creation_datetime_obj = self.token_formatter.creation_time( + token_id) + token_data['token']['issued_at'] = ks_utils.isotime( + at=fernet_creation_datetime_obj, subsecond=True) + def _build_federated_info(self, token_data): """Extract everything needed for federated tokens. - This dictionary is passed to the FederatedPayload token formatter, - which unpacks the values and builds the Fernet token. + This dictionary is passed to federated token formatters, which unpack + the values and build federated Fernet tokens. """ - group_ids = token_data.get('user', {}).get( - federation.FEDERATION, {}).get('groups') - idp_id = token_data.get('user', {}).get( - federation.FEDERATION, {}).get('identity_provider', {}).get('id') - protocol_id = token_data.get('user', {}).get( - federation.FEDERATION, {}).get('protocol', {}).get('id') - if not group_ids: - group_ids = list() - federated_dict = dict(group_ids=group_ids, idp_id=idp_id, - protocol_id=protocol_id) - return federated_dict + idp_id = token_data['token'].get('user', {}).get( + federation_constants.FEDERATION, {}).get( + 'identity_provider', {}).get('id') + protocol_id = token_data['token'].get('user', {}).get( + federation_constants.FEDERATION, {}).get('protocol', {}).get('id') + # If we don't have an identity provider ID and a protocol ID, it's safe + # to assume we aren't dealing with a federated token. + if not (idp_id and protocol_id): + return None + + group_ids = token_data['token'].get('user', {}).get( + federation_constants.FEDERATION, {}).get('groups') + + return {'group_ids': group_ids, + 'idp_id': idp_id, + 'protocol_id': protocol_id} def _rebuild_federated_info(self, federated_dict, user_id): """Format federated information into the token reference. - The federated_dict is passed back from the FederatedPayload token - formatter. The responsibility of this method is to format the - information passed back from the token formatter into the token - reference before constructing the token data from the - V3TokenDataHelper. + The federated_dict is passed back from the federated token formatters. + The responsibility of this method is to format the information passed + back from the token formatter into the token reference before + constructing the token data from the V3TokenDataHelper. """ g_ids = federated_dict['group_ids'] idp_id = federated_dict['idp_id'] protocol_id = federated_dict['protocol_id'] - federated_info = dict(groups=g_ids, - identity_provider=dict(id=idp_id), - protocol=dict(id=protocol_id)) - token_dict = {'user': {federation.FEDERATION: federated_info}} - token_dict['user']['id'] = user_id - token_dict['user']['name'] = user_id + + federated_info = { + 'groups': g_ids, + 'identity_provider': {'id': idp_id}, + 'protocol': {'id': protocol_id} + } + + token_dict = { + 'user': { + federation_constants.FEDERATION: federated_info, + 'id': user_id, + 'name': user_id + } + } + return token_dict - def issue_v3_token(self, user_id, method_names, expires_at=None, - project_id=None, domain_id=None, auth_context=None, - trust=None, metadata_ref=None, include_catalog=True, - parent_audit_id=None): - """Issue a V3 formatted token. - - Here is where we need to detect what is given to us, and what kind of - token the user is expecting. Depending on the outcome of that, we can - pass all the information to be packed to the proper token format - handler. - - :param user_id: ID of the user - :param method_names: method of authentication - :param expires_at: token expiration time - :param project_id: ID of the project being scoped to - :param domain_id: ID of the domain being scoped to - :param auth_context: authentication context - :param trust: ID of the trust - :param metadata_ref: metadata reference - :param include_catalog: return the catalog in the response if True, - otherwise don't return the catalog - :param parent_audit_id: ID of the patent audit entity - :returns: tuple containing the id of the token and the token data + def _rebuild_federated_token_roles(self, token_dict, federated_dict, + user_id, project_id, domain_id): + """Populate roles based on (groups, project/domain) pair. - """ - # TODO(lbragstad): Currently, Fernet tokens don't support bind in the - # token format. Raise a 501 if we're dealing with bind. - if auth_context.get('bind'): - raise exception.NotImplemented() + We must populate roles from (groups, project/domain) as ephemeral users + don't exist in the backend. Upon success, a ``roles`` key will be added + to ``token_dict``. - token_ref = None - # NOTE(lbragstad): This determines if we are dealing with a federated - # token or not. The groups for the user will be in the returned token - # reference. - federated_dict = None - if auth_context and self._is_mapped_token(auth_context): - token_ref = self._handle_mapped_tokens( - auth_context, project_id, domain_id) - federated_dict = self._build_federated_info(token_ref) - - token_data = self.v3_token_data_helper.get_token_data( - user_id, - method_names, - auth_context.get('extras') if auth_context else None, - domain_id=domain_id, - project_id=project_id, - expires=expires_at, - trust=trust, - bind=auth_context.get('bind') if auth_context else None, - token=token_ref, - include_catalog=include_catalog, - audit_info=parent_audit_id) + :param token_dict: dictionary with data used for building token + :param federated_dict: federated information such as identity provider + protocol and set of group IDs + :param user_id: user ID + :param project_id: project ID the token is being scoped to + :param domain_id: domain ID the token is being scoped to - token = self.token_formatter.create_token( - user_id, - token_data['token']['expires_at'], - token_data['token']['audit_ids'], - methods=method_names, - domain_id=domain_id, - project_id=project_id, - trust_id=token_data['token'].get('OS-TRUST:trust', {}).get('id'), - federated_info=federated_dict) - return token, token_data + """ + group_ids = [x['id'] for x in federated_dict['group_ids']] + self.v3_token_data_helper.populate_roles_for_groups( + token_dict, group_ids, project_id, domain_id, user_id) def validate_v2_token(self, token_ref): """Validate a V2 formatted token. :param token_ref: reference describing the token to validate :returns: the token data + :raises keystone.exception.TokenNotFound: if token format is invalid :raises keystone.exception.Unauthorized: if v3 token is used """ - (user_id, methods, - audit_ids, domain_id, - project_id, trust_id, - federated_info, created_at, - expires_at) = self.token_formatter.validate_token(token_ref) + try: + (user_id, methods, + audit_ids, domain_id, + project_id, trust_id, + federated_info, created_at, + expires_at) = self.token_formatter.validate_token(token_ref) + except exception.ValidationError as e: + raise exception.TokenNotFound(e) if trust_id or domain_id or federated_info: msg = _('This is not a v2.0 Fernet token. Use v3 for trust, ' @@ -225,26 +225,36 @@ class Provider(common.BaseProvider): token=token_ref, include_catalog=False, audit_info=audit_ids) - return self.v2_token_data_helper.v3_to_v2_token(token_ref, - v3_token_data) + token_data = self.v2_token_data_helper.v3_to_v2_token(v3_token_data) + token_data['access']['token']['id'] = token_ref + return token_data def validate_v3_token(self, token): """Validate a V3 formatted token. :param token: a string describing the token to validate :returns: the token data - :raises keystone.exception.Unauthorized: if token format version isn't + :raises keystone.exception.TokenNotFound: if token format version isn't supported """ - (user_id, methods, audit_ids, domain_id, project_id, trust_id, - federated_info, created_at, expires_at) = ( - self.token_formatter.validate_token(token)) + try: + (user_id, methods, audit_ids, domain_id, project_id, trust_id, + federated_info, created_at, expires_at) = ( + self.token_formatter.validate_token(token)) + except exception.ValidationError as e: + raise exception.TokenNotFound(e) token_dict = None + trust_ref = None if federated_info: token_dict = self._rebuild_federated_info(federated_info, user_id) - trust_ref = self.trust_api.get_trust(trust_id) + if project_id or domain_id: + self._rebuild_federated_token_roles(token_dict, federated_info, + user_id, project_id, + domain_id) + if trust_id: + trust_ref = self.trust_api.get_trust(trust_id) return self.v3_token_data_helper.get_token_data( user_id, @@ -264,4 +274,21 @@ class Provider(common.BaseProvider): :type token_data: dict :raises keystone.exception.NotImplemented: when called """ - raise exception.NotImplemented() + return self.token_formatter.create_token( + token_data['token']['user']['id'], + token_data['token']['expires_at'], + token_data['token']['audit_ids'], + methods=token_data['token'].get('methods'), + domain_id=token_data['token'].get('domain', {}).get('id'), + project_id=token_data['token'].get('project', {}).get('id'), + trust_id=token_data['token'].get('OS-TRUST:trust', {}).get('id'), + federated_info=self._build_federated_info(token_data) + ) + + @property + def _supports_bind_authentication(self): + """Return if the token provider supports bind authentication methods. + + :returns: False + """ + return False diff --git a/keystone-moon/keystone/token/providers/fernet/token_formatters.py b/keystone-moon/keystone/token/providers/fernet/token_formatters.py index 50960923..d1dbb08c 100644 --- a/keystone-moon/keystone/token/providers/fernet/token_formatters.py +++ b/keystone-moon/keystone/token/providers/fernet/token_formatters.py @@ -21,11 +21,12 @@ from oslo_config import cfg from oslo_log import log from oslo_utils import timeutils import six -from six.moves import urllib +from six.moves import map, urllib from keystone.auth import plugins as auth_plugins +from keystone.common import utils as ks_utils from keystone import exception -from keystone.i18n import _ +from keystone.i18n import _, _LI from keystone.token import provider from keystone.token.providers.fernet import utils @@ -60,7 +61,7 @@ class TokenFormatter(object): if not keys: raise exception.KeysNotFound() - fernet_instances = [fernet.Fernet(key) for key in utils.load_keys()] + fernet_instances = [fernet.Fernet(key) for key in keys] return fernet.MultiFernet(fernet_instances) def pack(self, payload): @@ -74,8 +75,9 @@ class TokenFormatter(object): try: return self.crypto.decrypt(token) - except fernet.InvalidToken as e: - raise exception.Unauthorized(six.text_type(e)) + except fernet.InvalidToken: + raise exception.ValidationError( + _('This is not a recognized Fernet token')) @classmethod def creation_time(cls, fernet_token): @@ -115,9 +117,27 @@ class TokenFormatter(object): expires_at, audit_ids, trust_id) + elif project_id and federated_info: + version = FederatedProjectScopedPayload.version + payload = FederatedProjectScopedPayload.assemble( + user_id, + methods, + project_id, + expires_at, + audit_ids, + federated_info) + elif domain_id and federated_info: + version = FederatedDomainScopedPayload.version + payload = FederatedDomainScopedPayload.assemble( + user_id, + methods, + domain_id, + expires_at, + audit_ids, + federated_info) elif federated_info: - version = FederatedPayload.version - payload = FederatedPayload.assemble( + version = FederatedUnscopedPayload.version + payload = FederatedUnscopedPayload.assemble( user_id, methods, expires_at, @@ -151,6 +171,17 @@ class TokenFormatter(object): serialized_payload = msgpack.packb(versioned_payload) token = self.pack(serialized_payload) + # NOTE(lbragstad): We should warn against Fernet tokens that are over + # 255 characters in length. This is mostly due to persisting the tokens + # in a backend store of some kind that might have a limit of 255 + # characters. Even though Keystone isn't storing a Fernet token + # anywhere, we can't say it isn't being stored somewhere else with + # those kind of backend constraints. + if len(token) > 255: + LOG.info(_LI('Fernet token created with length of %d ' + 'characters, which exceeds 255 characters'), + len(token)) + return token def validate_token(self, token): @@ -181,21 +212,29 @@ class TokenFormatter(object): elif version == TrustScopedPayload.version: (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( TrustScopedPayload.disassemble(payload)) - elif version == FederatedPayload.version: + elif version == FederatedUnscopedPayload.version: (user_id, methods, expires_at, audit_ids, federated_info) = ( - FederatedPayload.disassemble(payload)) + FederatedUnscopedPayload.disassemble(payload)) + elif version == FederatedProjectScopedPayload.version: + (user_id, methods, project_id, expires_at, audit_ids, + federated_info) = FederatedProjectScopedPayload.disassemble( + payload) + elif version == FederatedDomainScopedPayload.version: + (user_id, methods, domain_id, expires_at, audit_ids, + federated_info) = FederatedDomainScopedPayload.disassemble( + payload) else: - # If the token_format is not recognized, raise Unauthorized. - raise exception.Unauthorized(_( + # If the token_format is not recognized, raise ValidationError. + raise exception.ValidationError(_( 'This is not a recognized Fernet payload version: %s') % version) # rather than appearing in the payload, the creation time is encoded # into the token format itself created_at = TokenFormatter.creation_time(token) - created_at = timeutils.isotime(at=created_at, subsecond=True) + created_at = ks_utils.isotime(at=created_at, subsecond=True) expires_at = timeutils.parse_isotime(expires_at) - expires_at = timeutils.isotime(at=expires_at, subsecond=True) + expires_at = ks_utils.isotime(at=expires_at, subsecond=True) return (user_id, methods, audit_ids, domain_id, project_id, trust_id, federated_info, created_at, expires_at) @@ -273,8 +312,8 @@ class BasePayload(object): :returns: a time formatted strings """ - time_object = datetime.datetime.utcfromtimestamp(int(time_int)) - return timeutils.isotime(time_object) + time_object = datetime.datetime.utcfromtimestamp(time_int) + return ks_utils.isotime(time_object, subsecond=True) @classmethod def attempt_convert_uuid_hex_to_bytes(cls, value): @@ -319,7 +358,7 @@ class UnscopedPayload(BasePayload): :returns: the payload of an unscoped token """ - b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) expires_at_int = cls._convert_time_string_to_int(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, @@ -335,7 +374,7 @@ class UnscopedPayload(BasePayload): audit_ids """ - user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0]) methods = auth_plugins.convert_integer_to_method_list(payload[1]) expires_at_str = cls._convert_int_to_time_string(payload[2]) audit_ids = list(map(provider.base64_encode, payload[3])) @@ -357,7 +396,7 @@ class DomainScopedPayload(BasePayload): :returns: the payload of a domain-scoped token """ - b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) try: b_domain_id = cls.convert_uuid_hex_to_bytes(domain_id) @@ -381,7 +420,7 @@ class DomainScopedPayload(BasePayload): expires_at_str, and audit_ids """ - user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0]) methods = auth_plugins.convert_integer_to_method_list(payload[1]) try: domain_id = cls.convert_uuid_bytes_to_hex(payload[2]) @@ -412,9 +451,9 @@ class ProjectScopedPayload(BasePayload): :returns: the payload of a project-scoped token """ - b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) - b_project_id = cls.convert_uuid_hex_to_bytes(project_id) + b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id) expires_at_int = cls._convert_time_string_to_int(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) @@ -429,9 +468,9 @@ class ProjectScopedPayload(BasePayload): expires_at_str, and audit_ids """ - user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0]) methods = auth_plugins.convert_integer_to_method_list(payload[1]) - project_id = cls.convert_uuid_bytes_to_hex(payload[2]) + project_id = cls.attempt_convert_uuid_bytes_to_hex(payload[2]) expires_at_str = cls._convert_int_to_time_string(payload[3]) audit_ids = list(map(provider.base64_encode, payload[4])) @@ -455,9 +494,9 @@ class TrustScopedPayload(BasePayload): :returns: the payload of a trust-scoped token """ - b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) - b_project_id = cls.convert_uuid_hex_to_bytes(project_id) + b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id) b_trust_id = cls.convert_uuid_hex_to_bytes(trust_id) expires_at_int = cls._convert_time_string_to_int(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, @@ -475,9 +514,9 @@ class TrustScopedPayload(BasePayload): expires_at_str, audit_ids, and trust_id """ - user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0]) methods = auth_plugins.convert_integer_to_method_list(payload[1]) - project_id = cls.convert_uuid_bytes_to_hex(payload[2]) + project_id = cls.attempt_convert_uuid_bytes_to_hex(payload[2]) expires_at_str = cls._convert_int_to_time_string(payload[3]) audit_ids = list(map(provider.base64_encode, payload[4])) trust_id = cls.convert_uuid_bytes_to_hex(payload[5]) @@ -486,10 +525,19 @@ class TrustScopedPayload(BasePayload): trust_id) -class FederatedPayload(BasePayload): +class FederatedUnscopedPayload(BasePayload): version = 4 @classmethod + def pack_group_id(cls, group_dict): + return cls.attempt_convert_uuid_hex_to_bytes(group_dict['id']) + + @classmethod + def unpack_group_id(cls, group_id_in_bytes): + group_id = cls.attempt_convert_uuid_bytes_to_hex(group_id_in_bytes) + return {'id': group_id} + + @classmethod def assemble(cls, user_id, methods, expires_at, audit_ids, federated_info): """Assemble the payload of a federated token. @@ -503,24 +551,24 @@ class FederatedPayload(BasePayload): :returns: the payload of a federated token """ - def pack_group_ids(group_dict): - return cls.convert_uuid_hex_to_bytes(group_dict['id']) b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) - b_group_ids = map(pack_group_ids, federated_info['group_ids']) + b_group_ids = list(map(cls.pack_group_id, + federated_info['group_ids'])) b_idp_id = cls.attempt_convert_uuid_hex_to_bytes( federated_info['idp_id']) protocol_id = federated_info['protocol_id'] expires_at_int = cls._convert_time_string_to_int(expires_at) - b_audit_ids = map(provider.random_urlsafe_str_to_bytes, audit_ids) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) return (b_user_id, methods, b_group_ids, b_idp_id, protocol_id, expires_at_int, b_audit_ids) @classmethod def disassemble(cls, payload): - """Validate a federated paylod. + """Validate a federated payload. :param token_string: a string representing the token :return: a tuple containing the user_id, auth methods, audit_ids, and @@ -529,17 +577,81 @@ class FederatedPayload(BasePayload): federated domain ID """ - def unpack_group_ids(group_id_in_bytes): - group_id = cls.convert_uuid_bytes_to_hex(group_id_in_bytes) - return {'id': group_id} user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0]) methods = auth_plugins.convert_integer_to_method_list(payload[1]) - group_ids = map(unpack_group_ids, payload[2]) + group_ids = list(map(cls.unpack_group_id, payload[2])) idp_id = cls.attempt_convert_uuid_bytes_to_hex(payload[3]) protocol_id = payload[4] expires_at_str = cls._convert_int_to_time_string(payload[5]) - audit_ids = map(provider.base64_encode, payload[6]) + audit_ids = list(map(provider.base64_encode, payload[6])) federated_info = dict(group_ids=group_ids, idp_id=idp_id, protocol_id=protocol_id) return (user_id, methods, expires_at_str, audit_ids, federated_info) + + +class FederatedScopedPayload(FederatedUnscopedPayload): + version = None + + @classmethod + def assemble(cls, user_id, methods, scope_id, expires_at, audit_ids, + federated_info): + """Assemble the project-scoped payload of a federated token. + + :param user_id: ID of the user in the token request + :param methods: list of authentication methods used + :param scope_id: ID of the project or domain ID to scope to + :param expires_at: datetime of the token's expiration + :param audit_ids: list of the token's audit IDs + :param federated_info: dictionary containing the identity provider ID, + protocol ID, federated domain ID and group IDs + :returns: the payload of a federated token + + """ + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + b_scope_id = cls.attempt_convert_uuid_hex_to_bytes(scope_id) + b_group_ids = list(map(cls.pack_group_id, + federated_info['group_ids'])) + b_idp_id = cls.attempt_convert_uuid_hex_to_bytes( + federated_info['idp_id']) + protocol_id = federated_info['protocol_id'] + expires_at_int = cls._convert_time_string_to_int(expires_at) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) + + return (b_user_id, methods, b_scope_id, b_group_ids, b_idp_id, + protocol_id, expires_at_int, b_audit_ids) + + @classmethod + def disassemble(cls, payload): + """Validate a project-scoped federated payload. + + :param token_string: a string representing the token + :returns: a tuple containing the user_id, auth methods, scope_id, + expiration time (as str), audit_ids, and a dictionary + containing federated information such as the the identity + provider ID, the protocol ID, the federated domain ID and + group IDs + + """ + user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0]) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + scope_id = cls.attempt_convert_uuid_bytes_to_hex(payload[2]) + group_ids = list(map(cls.unpack_group_id, payload[3])) + idp_id = cls.attempt_convert_uuid_bytes_to_hex(payload[4]) + protocol_id = payload[5] + expires_at_str = cls._convert_int_to_time_string(payload[6]) + audit_ids = list(map(provider.base64_encode, payload[7])) + federated_info = dict(idp_id=idp_id, protocol_id=protocol_id, + group_ids=group_ids) + return (user_id, methods, scope_id, expires_at_str, audit_ids, + federated_info) + + +class FederatedProjectScopedPayload(FederatedScopedPayload): + version = 5 + + +class FederatedDomainScopedPayload(FederatedScopedPayload): + version = 6 diff --git a/keystone-moon/keystone/token/providers/fernet/utils.py b/keystone-moon/keystone/token/providers/fernet/utils.py index 56624ee5..4235eda8 100644 --- a/keystone-moon/keystone/token/providers/fernet/utils.py +++ b/keystone-moon/keystone/token/providers/fernet/utils.py @@ -59,8 +59,8 @@ def _convert_to_integers(id_value): try: id_int = int(id_value) except ValueError as e: - msg = ('Unable to convert Keystone user or group ID. Error: %s', e) - LOG.error(msg) + msg = _LE('Unable to convert Keystone user or group ID. Error: %s') + LOG.error(msg, e) raise return id_int @@ -174,11 +174,16 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None): for filename in os.listdir(CONF.fernet_tokens.key_repository): path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) if os.path.isfile(path): - key_files[int(filename)] = path + try: + key_id = int(filename) + except ValueError: + pass + else: + key_files[key_id] = path LOG.info(_LI('Starting key rotation with %(count)s key files: %(list)s'), { 'count': len(key_files), - 'list': key_files.values()}) + 'list': list(key_files.values())}) # determine the number of the new primary key current_primary_key = max(key_files.keys()) @@ -199,20 +204,24 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None): # add a new key to the rotation, which will be the *next* primary _create_new_key(keystone_user_id, keystone_group_id) + max_active_keys = CONF.fernet_tokens.max_active_keys # check for bad configuration - if CONF.fernet_tokens.max_active_keys < 1: + if max_active_keys < 1: LOG.warning(_LW( '[fernet_tokens] max_active_keys must be at least 1 to maintain a ' 'primary key.')) - CONF.fernet_tokens.max_active_keys = 1 + max_active_keys = 1 # purge excess keys - keys = sorted(key_files.keys()) - excess_keys = ( - keys[:len(key_files) - CONF.fernet_tokens.max_active_keys + 1]) - LOG.info(_LI('Excess keys to purge: %s'), excess_keys) - for i in excess_keys: - os.remove(key_files[i]) + + # Note that key_files doesn't contain the new active key that was created, + # only the old active keys. + keys = sorted(key_files.keys(), reverse=True) + while len(keys) > (max_active_keys - 1): + index_to_purge = keys.pop() + key_to_purge = key_files[index_to_purge] + LOG.info(_LI('Excess key to purge: %s'), key_to_purge) + os.remove(key_to_purge) def load_keys(): @@ -232,12 +241,25 @@ def load_keys(): path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) if os.path.isfile(path): with open(path, 'r') as key_file: - keys[int(filename)] = key_file.read() - - LOG.info(_LI( - 'Loaded %(count)s encryption keys from: %(dir)s'), { - 'count': len(keys), - 'dir': CONF.fernet_tokens.key_repository}) + try: + key_id = int(filename) + except ValueError: + pass + else: + keys[key_id] = key_file.read() + + if len(keys) != CONF.fernet_tokens.max_active_keys: + # If there haven't been enough key rotations to reach max_active_keys, + # or if the configured value of max_active_keys has changed since the + # last rotation, then reporting the discrepancy might be useful. Once + # the number of keys matches max_active_keys, this log entry is too + # repetitive to be useful. + LOG.info(_LI( + 'Loaded %(count)d encryption keys (max_active_keys=%(max)d) from: ' + '%(dir)s'), { + 'count': len(keys), + 'max': CONF.fernet_tokens.max_active_keys, + 'dir': CONF.fernet_tokens.key_repository}) # return the encryption_keys, sorted by key number, descending return [keys[x] for x in sorted(keys.keys(), reverse=True)] diff --git a/keystone-moon/keystone/token/providers/pki.py b/keystone-moon/keystone/token/providers/pki.py index 61b42817..af8dc739 100644 --- a/keystone-moon/keystone/token/providers/pki.py +++ b/keystone-moon/keystone/token/providers/pki.py @@ -48,6 +48,14 @@ class Provider(common.BaseProvider): raise exception.UnexpectedError(_( 'Unable to sign token.')) + @property + def _supports_bind_authentication(self): + """Return if the token provider supports bind authentication methods. + + :returns: True + """ + return True + def needs_persistence(self): """Should the token be written to a backend.""" return True diff --git a/keystone-moon/keystone/token/providers/pkiz.py b/keystone-moon/keystone/token/providers/pkiz.py index b6f2944d..b4e31918 100644 --- a/keystone-moon/keystone/token/providers/pkiz.py +++ b/keystone-moon/keystone/token/providers/pkiz.py @@ -46,6 +46,14 @@ class Provider(common.BaseProvider): LOG.exception(ERROR_MESSAGE) raise exception.UnexpectedError(ERROR_MESSAGE) + @property + def _supports_bind_authentication(self): + """Return if the token provider supports bind authentication methods. + + :returns: True + """ + return True + def needs_persistence(self): """Should the token be written to a backend.""" return True diff --git a/keystone-moon/keystone/token/providers/uuid.py b/keystone-moon/keystone/token/providers/uuid.py index 15118d82..f9a91617 100644 --- a/keystone-moon/keystone/token/providers/uuid.py +++ b/keystone-moon/keystone/token/providers/uuid.py @@ -28,6 +28,14 @@ class Provider(common.BaseProvider): def _get_token_id(self, token_data): return uuid.uuid4().hex + @property + def _supports_bind_authentication(self): + """Return if the token provider supports bind authentication methods. + + :returns: True + """ + return True + def needs_persistence(self): """Should the token be written to a backend.""" return True diff --git a/keystone-moon/keystone/token/utils.py b/keystone-moon/keystone/token/utils.py new file mode 100644 index 00000000..96a09246 --- /dev/null +++ b/keystone-moon/keystone/token/utils.py @@ -0,0 +1,27 @@ +# 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 keystoneclient.common import cms +from oslo_config import cfg + + +def generate_unique_id(token_id): + """Return a unique ID for a token. + + The returned value is useful as the primary key of a database table, + memcache store, or other lookup table. + + :returns: Given a PKI token, returns it's hashed value. Otherwise, + returns the passed-in value (such as a UUID token ID or an + existing hash). + """ + return cms.cms_hash_token(token_id, mode=cfg.CONF.token.hash_algorithm) diff --git a/keystone-moon/keystone/trust/backends/sql.py b/keystone-moon/keystone/trust/backends/sql.py index 4f5ee2e5..95b18d40 100644 --- a/keystone-moon/keystone/trust/backends/sql.py +++ b/keystone-moon/keystone/trust/backends/sql.py @@ -16,6 +16,7 @@ import time from oslo_log import log from oslo_utils import timeutils +from six.moves import range from keystone.common import sql from keystone import exception @@ -135,15 +136,15 @@ class Trust(trust.Driver): query = query.filter_by(deleted_at=None) ref = query.first() if ref is None: - return None + raise exception.TrustNotFound(trust_id=trust_id) if ref.expires_at is not None and not deleted: now = timeutils.utcnow() if now > ref.expires_at: - return None + raise exception.TrustNotFound(trust_id=trust_id) # Do not return trusts that can't be used anymore if ref.remaining_uses is not None and not deleted: if ref.remaining_uses <= 0: - return None + raise exception.TrustNotFound(trust_id=trust_id) trust_dict = ref.to_dict() self._add_roles(trust_id, session, trust_dict) diff --git a/keystone-moon/keystone/trust/controllers.py b/keystone-moon/keystone/trust/controllers.py index 60e34ccd..39cf0110 100644 --- a/keystone-moon/keystone/trust/controllers.py +++ b/keystone-moon/keystone/trust/controllers.py @@ -16,18 +16,18 @@ import uuid from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_utils import timeutils import six from keystone import assignment from keystone.common import controller from keystone.common import dependency +from keystone.common import utils from keystone.common import validation from keystone import exception from keystone.i18n import _ -from keystone.models import token_model from keystone import notifications -from keystone.openstack.common import versionutils from keystone.trust import schema @@ -63,19 +63,15 @@ class TrustV3(controller.V3Controller): return super(TrustV3, cls).base_url(context, path=path) def _get_user_id(self, context): - if 'token_id' in context: - token_id = context['token_id'] - token_data = self.token_provider_api.validate_token(token_id) - token_ref = token_model.KeystoneToken(token_id=token_id, - token_data=token_data) - return token_ref.user_id - return None + try: + token_ref = utils.get_token_ref(context) + except exception.Unauthorized: + return None + return token_ref.user_id def get_trust(self, context, trust_id): user_id = self._get_user_id(context) trust = self.trust_api.get_trust(trust_id) - if not trust: - raise exception.TrustNotFound(trust_id=trust_id) _trustor_trustee_only(trust, user_id) self._fill_in_roles(context, trust, self.role_api.list_roles()) @@ -83,7 +79,7 @@ class TrustV3(controller.V3Controller): def _fill_in_roles(self, context, trust, all_roles): if trust.get('expires_at') is not None: - trust['expires_at'] = (timeutils.isotime + trust['expires_at'] = (utils.isotime (trust['expires_at'], subsecond=True)) @@ -126,15 +122,12 @@ class TrustV3(controller.V3Controller): @controller.protected() @validation.validated(schema.trust_create, 'trust') - def create_trust(self, context, trust=None): + def create_trust(self, context, trust): """Create a new trust. The user creating the trust must be the trustor. """ - if not trust: - raise exception.ValidationError(attribute='trust', - target='request') auth_context = context.get('environment', {}).get('KEYSTONE_AUTH_CONTEXT', {}) @@ -206,15 +199,16 @@ class TrustV3(controller.V3Controller): if not expiration_date.endswith('Z'): expiration_date += 'Z' try: - return timeutils.parse_isotime(expiration_date) + expiration_time = timeutils.parse_isotime(expiration_date) except ValueError: raise exception.ValidationTimeStampError() + if timeutils.is_older_than(expiration_time, 0): + raise exception.ValidationExpirationError() + return expiration_time 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(trust_id) - if not trust: - raise exception.TrustNotFound(trust_id=trust_id) user_id = self._get_user_id(context) _trustor_trustee_only(trust, user_id) if not any(role['id'] == role_id for role in trust['roles']): @@ -247,7 +241,7 @@ class TrustV3(controller.V3Controller): if 'roles' in trust: del trust['roles'] if trust.get('expires_at') is not None: - trust['expires_at'] = (timeutils.isotime + trust['expires_at'] = (utils.isotime (trust['expires_at'], subsecond=True)) return TrustV3.wrap_collection(context, trusts) @@ -255,9 +249,6 @@ class TrustV3(controller.V3Controller): @controller.protected() def delete_trust(self, context, trust_id): trust = self.trust_api.get_trust(trust_id) - if not trust: - raise exception.TrustNotFound(trust_id=trust_id) - user_id = self._get_user_id(context) _admin_trustor_only(context, trust, user_id) initiator = notifications._get_request_audit_info(context) @@ -266,8 +257,6 @@ class TrustV3(controller.V3Controller): @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=trust_id) user_id = self._get_user_id(context) _trustor_trustee_only(trust, user_id) return {'roles': trust['roles'], diff --git a/keystone-moon/keystone/trust/core.py b/keystone-moon/keystone/trust/core.py index de6b6d85..e091ff93 100644 --- a/keystone-moon/keystone/trust/core.py +++ b/keystone-moon/keystone/trust/core.py @@ -12,13 +12,14 @@ # License for the specific language governing permissions and limitations # under the License. -"""Main entry point into the Identity service.""" +"""Main entry point into the Trust service.""" import abc from oslo_config import cfg from oslo_log import log import six +from six.moves import zip from keystone.common import dependency from keystone.common import manager @@ -41,6 +42,9 @@ class Manager(manager.Manager): dynamically calls the backend. """ + + driver_namespace = 'keystone.trust' + _TRUST = "OS-TRUST:trust" def __init__(self): @@ -55,9 +59,9 @@ class Manager(manager.Manager): if not (0 < redelegation_depth <= max_redelegation_count): raise exception.Forbidden( _('Remaining redelegation depth of %(redelegation_depth)d' - ' out of allowed range of [0..%(max_count)d]'), - redelegation_depth=redelegation_depth, - max_count=max_redelegation_count) + ' out of allowed range of [0..%(max_count)d]') % + {'redelegation_depth': redelegation_depth, + 'max_count': max_redelegation_count}) # remaining_uses is None remaining_uses = trust.get('remaining_uses') @@ -139,13 +143,14 @@ class Manager(manager.Manager): if requested_count and requested_count > max_redelegation_count: raise exception.Forbidden( _('Requested redelegation depth of %(requested_count)d ' - 'is greater than allowed %(max_count)d'), - requested_count=requested_count, - max_count=max_redelegation_count) + 'is greater than allowed %(max_count)d') % + {'requested_count': requested_count, + 'max_count': max_redelegation_count}) # Decline remaining_uses - if 'remaining_uses' in trust: - exception.ValidationError(_('remaining_uses must not be set ' - 'if redelegation is allowed')) + if trust.get('remaining_uses') is not None: + raise exception.ValidationError( + _('remaining_uses must not be set if redelegation is ' + 'allowed')) if redelegated_trust: trust['redelegated_trust_id'] = redelegated_trust['id'] @@ -179,9 +184,6 @@ class Manager(manager.Manager): Recursively remove given and redelegated trusts """ trust = self.driver.get_trust(trust_id) - if not trust: - raise exception.TrustNotFound(trust_id) - trusts = self.driver.list_trusts_for_trustor( trust['trustor_user_id']) diff --git a/keystone-moon/keystone/trust/schema.py b/keystone-moon/keystone/trust/schema.py index 087cd1e9..673b786b 100644 --- a/keystone-moon/keystone/trust/schema.py +++ b/keystone-moon/keystone/trust/schema.py @@ -15,8 +15,11 @@ from keystone.common.validation import parameter_types _trust_properties = { - 'trustor_user_id': parameter_types.id_string, - 'trustee_user_id': parameter_types.id_string, + # NOTE(lbragstad): These are set as external_id_string because they have + # the ability to be read as LDAP user identifiers, which could be something + # other than uuid. + 'trustor_user_id': parameter_types.external_id_string, + 'trustee_user_id': parameter_types.external_id_string, 'impersonation': parameter_types.boolean, 'project_id': validation.nullable(parameter_types.id_string), 'remaining_uses': { |