diff options
| author | Jenkins <jenkins@review.openstack.org> | 2013-07-08 16:00:04 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2013-07-08 16:00:04 +0000 |
| commit | a5aa993b907660f0bced8a7ed03b346dbad73d59 (patch) | |
| tree | 15213e66d64da979b9864498294a42ba181f9d42 /keystone | |
| parent | 6450f75deffa9a63fc77dbf9d4d35ad7e11feaf2 (diff) | |
| parent | fa10d4945ca9658eff02b1d8e917fde50d6576ce (diff) | |
Merge "Implement GET /role_assignment API call"
Diffstat (limited to 'keystone')
| -rw-r--r-- | keystone/common/controller.py | 3 | ||||
| -rw-r--r-- | keystone/identity/backends/kvs.py | 39 | ||||
| -rw-r--r-- | keystone/identity/backends/sql.py | 37 | ||||
| -rw-r--r-- | keystone/identity/controllers.py | 193 | ||||
| -rw-r--r-- | keystone/identity/core.py | 4 | ||||
| -rw-r--r-- | keystone/identity/routers.py | 4 |
6 files changed, 279 insertions, 1 deletions
diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 13aeee57..3ca1bf8b 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -280,7 +280,8 @@ class V3Controller(V2Controller): if attr in context['query_string']: value = context['query_string'][attr] - return [r for r in refs if _attr_match(r[attr], value)] + return [r for r in refs if _attr_match( + flatten(r).get(attr), value)] return refs def _require_matching_id(self, value, ref): diff --git a/keystone/identity/backends/kvs.py b/keystone/identity/backends/kvs.py index 2eea08cf..77a86789 100644 --- a/keystone/identity/backends/kvs.py +++ b/keystone/identity/backends/kvs.py @@ -180,6 +180,45 @@ class Identity(kvs.Base, identity.Driver): else: self.update_metadata(user_id, tenant_id, metadata_ref) + def list_role_assignments(self): + """List the role assignments. + + The kvs backend stores role assignments as key-values: + + "metadata-{target}-{actor}", with the value being a role list + + i.e. "metadata-MyProjectID-MyUserID" [role1, role2] + + ...so we enumerate the list and extract the targets, actors + and roles. + + """ + assignment_list = [] + metadata_keys = filter(lambda x: x.startswith("metadata-"), + self.db.keys()) + for key in metadata_keys: + template = {} + meta_id1 = key.split('-')[1] + meta_id2 = key.split('-')[2] + try: + self.get_project(meta_id1) + template['project_id'] = meta_id1 + except exception.NotFound: + template['domain_id'] = meta_id1 + try: + self._get_user(meta_id2) + template['user_id'] = meta_id2 + except exception.NotFound: + template['group_id'] = meta_id2 + + entry = self.db.get(key) + for r in entry.get('roles', []): + role_assignment = template.copy() + role_assignment['role_id'] = r + assignment_list.append(role_assignment) + + return assignment_list + # CRUD def create_user(self, user_id, user): try: diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index f81feb1d..2f9d89f3 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -355,6 +355,43 @@ class Identity(sql.Base, identity.Driver): self.update_metadata(user_id, project_id, metadata_ref, domain_id, group_id) + def list_role_assignments(self): + + # TODO(henry-nash): The current implementation is really simulating + # us having a common role assignment table, rather than having the + # four different grant tables we have today. When we move to role + # assignment as a first class entity, we should create the single + # assignment table, simplifying the logic of this (and many other) + # functions. + + session = self.get_session() + assignment_list = [] + refs = session.query(UserDomainGrant).all() + for x in refs: + for r in x.data.get('roles', []): + assignment_list.append({'user_id': x.user_id, + 'domain_id': x.domain_id, + 'role_id': r}) + refs = session.query(UserProjectGrant).all() + for x in refs: + for r in x.data.get('roles', []): + assignment_list.append({'user_id': x.user_id, + 'project_id': x.project_id, + 'role_id': r}) + refs = session.query(GroupDomainGrant).all() + for x in refs: + for r in x.data.get('roles', []): + assignment_list.append({'group_id': x.group_id, + 'domain_id': x.domain_id, + 'role_id': r}) + refs = session.query(GroupProjectGrant).all() + for x in refs: + for r in x.data.get('roles', []): + assignment_list.append({'group_id': x.group_id, + 'project_id': x.project_id, + 'role_id': r}) + return assignment_list + def list_projects(self): session = self.get_session() tenant_refs = session.query(Project).all() diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index f798e3dc..9271f3d9 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -16,6 +16,7 @@ """Workflow Logic the Identity service.""" +import copy import urllib import urlparse import uuid @@ -794,3 +795,195 @@ class RoleV3(controller.V3Controller): self._delete_tokens_for_user(user_id) else: self._delete_tokens_for_group(group_id) + + +class RoleAssignmentV3(controller.V3Controller): + + # TODO(henry-nash): The current implementation does not provide a full + # first class entity for role-assignment. There is no role_assignment_id + # and only the list_role_assignment call is supported. Further, since it + # is not a first class entity, the links for the individual entities + # reference the individual role grant APIs. + + collection_name = 'role_assignments' + member_name = 'role_assignment' + + @classmethod + def wrap_member(cls, context, ref): + # NOTE(henry-nash): Since we are not yet a true collection, we override + # the wrapper as have already included the links in the entities + pass + + def _format_entity(self, entity): + formatted_entity = {} + 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']} + if 'project_id' in entity: + formatted_entity['scope'] = ( + {'project': {'id': entity['project_id']}}) + target_link = '/projects/%s' % entity['project_id'] + if 'domain_id' in entity: + formatted_entity['scope'] = ( + {'domain': {'id': entity['domain_id']}}) + target_link = '/domains/%s' % entity['domain_id'] + + formatted_entity.setdefault('links', {}) + formatted_entity['links']['assignment'] = ( + self.base_url(target_link + actor_link + + '/roles/%s' % entity['role_id'])) + return formatted_entity + + def _expand_indirect_assignments(self, 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. + + For any new entity created by virtue of group membership, add in an + additional link to that membership. + + """ + def _get_group_members(ref): + """Get a list of group members. + + Get the list of group members. If this fails with + GroupNotFound, then log this as a warning, but allow + overall processing to continue. + + """ + 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 + if 'domain' in ref: + target = 'Domain: %s' % ref['domain'].get('domain_id') + elif 'project' in ref: + target = 'Project: %s' % ref['project'].get('project_id') + else: + # Should always be a domain or project, but since to get + # here things have gone astray, let's be cautious. + target = 'Unknown' + LOG.warning( + _('Group %(group)s not found for role-assignment - ' + '%(target)s with Role: %(role)s') % { + 'group': ref['group_id'], 'target': target, + 'role': ref.get('role_id')}) + return members + + def _build_equivalent_user_assignment(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, modify the 'assignment' link + to match, and add a 'membership' link. + + """ + user_entry = copy.deepcopy(template) + user_entry['user'] = {'id': user['id']} + scope = user_entry.get('scope') + if 'domain' in scope: + target_link = ( + '/domains/%s' % scope['domain']['id']) + else: + target_link = ( + '/projects/%s' % scope['project']['id']) + user_entry['links']['assignment'] = ( + self.base_url('%s/users/%s/roles/%s' % + (target_link, m['id'], + user_entry['role']['id']))) + user_entry['links']['membership'] = ( + self.base_url('/groups/%s/users/%s' % + (group_id, user['id']))) + return user_entry + + # Scan the list of entities for any group assignments, expanding + # them into equivalent user entities. Due to potential large + # expansion of group entities, rather than modify the + # list we are enumerating, we build a new one as we go. + new_refs = [] + for r in refs: + if 'group' in r: + # As it is a group role assignment, first 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_equivalent_user_assignment( + m, group_id, base_entry) + new_refs.append(user_entry) + else: + new_refs.append(r) + + return new_refs + + def _query_filter_is_true(self, filter_value): + """Determine if bool query param is 'True'. + + We treat this the same way as we do for policy + enforcement: + + {bool_param}=0 is treated as False + + Any other value is considered to be equivalent to + True, including the absence of a value + + """ + + if (isinstance(filter_value, basestring) and + filter_value == '0'): + val = False + else: + val = True + return val + + @controller.filterprotected('group.id', 'role.id', + 'scope.domain.id', 'scope.project.id', + 'user.id') + def list_role_assignments(self, context, filters): + + # 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. + + refs = self.identity_api.list_role_assignments() + formatted_refs = [self._format_entity(x) for x in refs] + + if ('effective' in context['query_string'] and + self._query_filter_is_true( + context['query_string']['effective'])): + + formatted_refs = self._expand_indirect_assignments(formatted_refs) + + return self.wrap_collection(context, formatted_refs, filters) + + @controller.protected + def get_role_assignment(self, context): + raise exception.NotImplemented() + + @controller.protected + def update_role_assignment(self, context): + raise exception.NotImplemented() + + @controller.protected + def delete_role_assignment(self, context): + raise exception.NotImplemented() diff --git a/keystone/identity/core.py b/keystone/identity/core.py index a254470e..77870fda 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -504,6 +504,10 @@ class Driver(object): """ raise exception.NotImplemented() + def list_role_assignments(self): + + raise exception.NotImplemented() + # group crud def create_group(self, group_id, group): diff --git a/keystone/identity/routers.py b/keystone/identity/routers.py index 32eada5e..ab71eb4f 100644 --- a/keystone/identity/routers.py +++ b/keystone/identity/routers.py @@ -173,3 +173,7 @@ def append_v3_routers(mapper, routers): controller=role_controller, action='revoke_grant', conditions=dict(method=['DELETE'])) + + routers.append( + router.Router(controllers.RoleAssignmentV3(), + 'role_assignments', 'role_assignment')) |
