diff options
| author | Henry Nash <henryn@linux.vnet.ibm.com> | 2013-06-21 22:50:40 +0100 |
|---|---|---|
| committer | Henry Nash <henryn@linux.vnet.ibm.com> | 2013-07-03 21:30:09 +0100 |
| commit | fa10d4945ca9658eff02b1d8e917fde50d6576ce (patch) | |
| tree | b5c50890695d8a503dc130a5c219cf543051cc2c /keystone/identity | |
| parent | 62d948a66b27ad2622a324bd9a070346f7b607d2 (diff) | |
| download | keystone-fa10d4945ca9658eff02b1d8e917fde50d6576ce.tar.gz keystone-fa10d4945ca9658eff02b1d8e917fde50d6576ce.tar.xz keystone-fa10d4945ca9658eff02b1d8e917fde50d6576ce.zip | |
Implement GET /role_assignment API call
Add support for the GET /role_assignment call as a first step
to making role_assignment a first class entity.
This patch also enables v3 collection filtering to match against
attributes of entities being returned in the list, using the same
dot notation (e.g. user.id) that we already support for policy file
checking against filters.
Limitations:
- The current implementation uses the standard v3 collections wrapper
mechanism for filtering. Given the potential numbers of role
assignments in a large system, this may have performance and resource
impacts. A future improvement would pass the filters into the
driver layer to keep the internal assignment processing to a minimum.
- The LDAP backend is not currently supported
Implements bp get-role-assignments
Change-Id: I6ff2ea780e39d7097a88214fbb3ddee1b924c30c
Diffstat (limited to 'keystone/identity')
| -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 |
5 files changed, 277 insertions, 0 deletions
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')) |
