summaryrefslogtreecommitdiffstats
path: root/keystone
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-07-08 16:00:04 +0000
committerGerrit Code Review <review@openstack.org>2013-07-08 16:00:04 +0000
commita5aa993b907660f0bced8a7ed03b346dbad73d59 (patch)
tree15213e66d64da979b9864498294a42ba181f9d42 /keystone
parent6450f75deffa9a63fc77dbf9d4d35ad7e11feaf2 (diff)
parentfa10d4945ca9658eff02b1d8e917fde50d6576ce (diff)
Merge "Implement GET /role_assignment API call"
Diffstat (limited to 'keystone')
-rw-r--r--keystone/common/controller.py3
-rw-r--r--keystone/identity/backends/kvs.py39
-rw-r--r--keystone/identity/backends/sql.py37
-rw-r--r--keystone/identity/controllers.py193
-rw-r--r--keystone/identity/core.py4
-rw-r--r--keystone/identity/routers.py4
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'))