Enable listing of role assignments in a project hierarchy

This patch implements the manager and backend support for
listing assignments for a hierarchy. A follow-on patch will
provide the controller level support and rest API.

Implements: bp list-assignment-subtree
Change-Id: Ice6aa85cb012757f326c263de5866537ace99278
This commit is contained in:
Henry Nash 2015-08-01 14:55:31 +01:00
parent 40453931a9
commit 8ad30b3c9b
2 changed files with 349 additions and 42 deletions

View File

@ -416,7 +416,7 @@ class Manager(manager.Manager):
# this case.
def _expand_indirect_assignment(self, ref, user_id=None,
project_id=None):
project_id=None, subtree_ids=None):
"""Returns a list of expanded role assignments.
This methods is called for each discovered assignment that either needs
@ -426,6 +426,14 @@ class Manager(manager.Manager):
In all cases, if either user_id and/or project_id is specified, then we
filter the result on those values.
If project_id is specified and subtree_ids is None, then this
indicates that we are only interested in that one project. If
subtree_ids is not None, then this is an indicator that any
inherited assignments need to be expanded down the tree. The
actual subtree_ids don't need to be used as a filter here, since we
already ensured only those assignments that could affect them
were passed to this method.
"""
def create_group_assignment(base_ref, user_id):
"""Creates a group assignment from the provided ref."""
@ -478,14 +486,18 @@ class Manager(manager.Manager):
for m in self.identity_api.list_users_in_group(
ref['group_id'])]
def expand_inherited_assignment(ref, user_id, project_id=None):
def expand_inherited_assignment(ref, user_id, project_id, subtree_ids):
"""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
If this is a user role assignment on a specific target (i.e.
project_id is specified, but subtree_ids is None) then simply
format this as a single assignment (since we are effectively
filtering on project_id). If however, project_id is None or
subtree_ids is not None, then replace this one assignment with a
list of role assignments for that user on every project under
that target.
@ -542,10 +554,25 @@ class Manager(manager.Manager):
# 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.
# Since ref is an inherited assignment and we are filtering by
# project(s), we are only going to apply the assignment to the
# relevant project(s)
project_ids = [project_id]
if subtree_ids:
project_ids += subtree_ids
# If this is a domain inherited assignment, then we know
# that all the project_ids will get this assignment. If
# it's a project inherited assignment, and the assignment
# point is an ancestor of project_id, then we know that
# again all the project_ids will get the assignment. If,
# however, the assignment point is within the subtree,
# then only a partial tree will get the assignment.
if ref.get('project_id'):
if ref['project_id'] in project_ids:
project_ids = (
[x['id'] for x in
self.resource_api.list_projects_in_subtree(
ref['project_id'])])
elif ref.get('domain_id'):
# A domain inherited assignment, so apply it to all projects
# in this domain
@ -554,7 +581,7 @@ class Manager(manager.Manager):
self.resource_api.list_projects_in_domain(
ref['domain_id'])])
else:
# It must be a project assignment, so apply it to the subtree
# It must be a project assignment, so apply it to its subtree
project_ids = (
[x['id'] for x in
self.resource_api.list_projects_in_subtree(
@ -574,13 +601,15 @@ class Manager(manager.Manager):
return new_refs
if ref.get('inherited_to_projects') == 'projects':
return expand_inherited_assignment(ref, user_id, project_id)
return expand_inherited_assignment(ref, user_id, project_id,
subtree_ids)
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):
domain_id, project_id, subtree_ids,
inherited):
"""List role assignments in effective mode.
When using effective mode, besides the direct assignments, the indirect
@ -588,10 +617,11 @@ class Manager(manager.Manager):
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.
parameters. If subtree_ids is not None, then we also want to include
all subtree_ids in the filter as well. 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
@ -599,12 +629,12 @@ class Manager(manager.Manager):
"""
def list_role_assignments_for_actor(
role_id, inherited, user_id=None,
group_ids=None, project_id=None, domain_id=None):
role_id, inherited, user_id=None, group_ids=None,
project_id=None, subtree_ids=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).
for a given target (i.e. projects or domain).
:param role_id: List for a specific role, can be None meaning all
roles
@ -616,7 +646,16 @@ class Manager(manager.Manager):
: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
that affect at least this project, with
additionally any projects specified in
subtree_ids
:param subtree_ids: The list of projects in the subtree. If
specified, also include those assignments that
affect these projects. These projects are
guaranteed to be in the same domain as the
project specified in project_id. subtree_ids
can only be specified if project_id has also
been specified.
:param domain_id: If specified, only include those assignments
that affect this domain - by definition this will
not include any inherited assignments
@ -626,24 +665,31 @@ class Manager(manager.Manager):
response are included.
"""
# List direct project role assignments
project_ids = [project_id] if project_id else None
project_ids_of_interest = None
if project_id:
if subtree_ids:
project_ids_of_interest = subtree_ids + [project_id]
else:
project_ids_of_interest = [project_id]
# List direct project role assignments
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,
project_ids=project_ids_of_interest, 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.
# The project and any subtree are guaranteed to be owned by
# the same domain, so since we are filtering by these
# specific projects, then we can only get inherited
# assignments from their common domain or from any of
# their parents projects.
# List inherited assignments from the project's domain
proj_domain_id = self.resource_api.get_project(
@ -653,14 +699,18 @@ class Manager(manager.Manager):
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
# For inherited assignments from projects, since we know
# they are from the same tree the only places these can
# come from are from parents of the main project or
# inherited assignments on the project or subtree itself.
source_ids = [project['id'] for project in
self.resource_api.list_project_parents(
project_id)]
if parent_ids:
if subtree_ids:
source_ids += project_ids_of_interest
if source_ids:
inherited_refs += self.driver.list_role_assignments(
role_id=role_id, project_ids=parent_ids,
role_id=role_id, project_ids=source_ids,
user_id=user_id, group_ids=group_ids,
inherited_to_projects=True)
else:
@ -683,7 +733,8 @@ class Manager(manager.Manager):
# 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)
subtree_ids=subtree_ids, domain_id=domain_id,
inherited=inherited)
# And those from the user's groups
group_refs = []
@ -692,41 +743,52 @@ class Manager(manager.Manager):
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)
subtree_ids=subtree_ids, 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)
refs += self._expand_indirect_assignment(
ref=ref, user_id=user_id, project_id=project_id,
subtree_ids=subtree_ids)
return refs
def _list_direct_role_assignments(self, role_id, user_id, group_id,
domain_id, project_id, inherited):
domain_id, project_id, subtree_ids,
inherited):
"""List role assignments without applying expansion.
Returns a list of direct role assignments, where their attributes match
the provided filters.
the provided filters. If subtree_ids is not None, then we also want to
include all subtree_ids in the filter as well.
"""
group_ids = [group_id] if group_id else None
project_ids = [project_id] if project_id else None
project_ids_of_interest = None
if project_id:
if subtree_ids:
project_ids_of_interest = subtree_ids + [project_id]
else:
project_ids_of_interest = [project_id]
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,
domain_id=domain_id, project_ids=project_ids_of_interest,
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,
domain_id=None, project_id=None,
include_subtree=False, 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
inherited). If include_subtree is True, then assignments on all
descendants of the project specified by project_id are also included.
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
@ -746,12 +808,20 @@ class Manager(manager.Manager):
return []
inherited = False
subtree_ids = None
if project_id and include_subtree:
subtree_ids = (
[x['id'] for x in
self.resource_api.list_projects_in_subtree(project_id)])
if effective:
return self._list_effective_role_assignments(
role_id, user_id, group_id, domain_id, project_id, inherited)
role_id, user_id, group_id, domain_id, project_id,
subtree_ids, inherited)
else:
return self._list_direct_role_assignments(
role_id, user_id, group_id, domain_id, project_id, inherited)
role_id, user_id, group_id, domain_id, project_id,
subtree_ids, inherited)
def delete_tokens_for_role_assignments(self, role_id):
assignments = self.list_role_assignments(role_id=role_id)

View File

@ -377,7 +377,7 @@ class AssignmentTestHelperMixin(object):
for test in test_plan.get('tests', []):
args = {}
for param in test['params']:
if param in ['effective', 'inherited']:
if param in ['effective', 'inherited', 'include_subtree']:
# Just pass the value into the args
args[param] = test['params'][param]
else:
@ -6318,6 +6318,243 @@ class InheritanceTests(AssignmentTestHelperMixin):
self.execute_assignment_cases(
test_plan_with_os_inherit_disabled, test_data)
def test_list_assignments_for_tree(self):
"""Test we correctly list direct assignments for a tree"""
# Enable OS-INHERIT extension
self.config_fixture.config(group='os_inherit', enabled=True)
test_plan = {
# Create a domain with a project hierarchy 3 levels deep:
#
# project 0
# ____________|____________
# | |
# project 1 project 4
# ______|_____ ______|_____
# | | | |
# project 2 project 3 project 5 project 6
#
# Also, create 1 user and 4 roles.
'entities': {
'domains': {
'projects': {'project': [{'project': 2},
{'project': 2}]},
'users': 1},
'roles': 4},
'assignments': [
# Direct assignment to projects 1 and 2
{'user': 0, 'role': 0, 'project': 1},
{'user': 0, 'role': 1, 'project': 2},
# Also an inherited assignment on project 1
{'user': 0, 'role': 2, 'project': 1,
'inherited_to_projects': True},
# ...and two spoiler assignments, one to the root and one
# to project 4
{'user': 0, 'role': 0, 'project': 0},
{'user': 0, 'role': 3, 'project': 4}],
'tests': [
# List all assignments for project 1 and its subtree.
{'params': {'project': 1, 'include_subtree': True},
'results': [
# Only the actual assignments should be returned, no
# expansion of inherited assignments
{'user': 0, 'role': 0, 'project': 1},
{'user': 0, 'role': 1, 'project': 2},
{'user': 0, 'role': 2, 'project': 1,
'inherited_to_projects': 'projects'}]}
]
}
self.execute_assignment_plan(test_plan)
def test_list_effective_assignments_for_tree(self):
"""Test we correctly list effective assignments for a tree"""
# Enable OS-INHERIT extension
self.config_fixture.config(group='os_inherit', enabled=True)
test_plan = {
# Create a domain with a project hierarchy 3 levels deep:
#
# project 0
# ____________|____________
# | |
# project 1 project 4
# ______|_____ ______|_____
# | | | |
# project 2 project 3 project 5 project 6
#
# Also, create 1 user and 4 roles.
'entities': {
'domains': {
'projects': {'project': [{'project': 2},
{'project': 2}]},
'users': 1},
'roles': 4},
'assignments': [
# An inherited assignment on project 1
{'user': 0, 'role': 1, 'project': 1,
'inherited_to_projects': True},
# A direct assignment to project 2
{'user': 0, 'role': 2, 'project': 2},
# ...and two spoiler assignments, one to the root and one
# to project 4
{'user': 0, 'role': 0, 'project': 0},
{'user': 0, 'role': 3, 'project': 4}],
'tests': [
# List all effective assignments for project 1 and its subtree.
{'params': {'project': 1, 'effective': True,
'include_subtree': True},
'results': [
# The inherited assignment on project 1 should appear only
# on its children
{'user': 0, 'role': 1, 'project': 2,
'indirect': {'project': 1}},
{'user': 0, 'role': 1, 'project': 3,
'indirect': {'project': 1}},
# And finally the direct assignment on project 2
{'user': 0, 'role': 2, 'project': 2}]}
]
}
self.execute_assignment_plan(test_plan)
def test_list_effective_assignments_for_tree_with_mixed_assignments(self):
"""Test that we correctly combine assignments for a tree.
In this test we want to ensure that when asking for a list of
assignments in a subtree, any assignments inherited from above the
subtree are correctly combined with any assignments within the subtree
itself.
"""
# Enable OS-INHERIT extension
self.config_fixture.config(group='os_inherit', enabled=True)
test_plan = {
# Create a domain with a project hierarchy 3 levels deep:
#
# project 0
# ____________|____________
# | |
# project 1 project 4
# ______|_____ ______|_____
# | | | |
# project 2 project 3 project 5 project 6
#
# Also, create 2 users, 1 group and 4 roles.
'entities': {
'domains': {
'projects': {'project': [{'project': 2},
{'project': 2}]},
'users': 2, 'groups': 1},
'roles': 4},
# Both users are part of the same group
'group_memberships': [{'group': 0, 'users': [0, 1]}],
# We are going to ask for listing of assignment on project 1 and
# it's subtree. So first we'll add two inherited assignments above
# this (one user and one for a group that contains this user).
'assignments': [{'user': 0, 'role': 0, 'project': 0,
'inherited_to_projects': True},
{'group': 0, 'role': 1, 'project': 0,
'inherited_to_projects': True},
# Now an inherited assignment on project 1 itself,
# which should ONLY show up on its children
{'user': 0, 'role': 2, 'project': 1,
'inherited_to_projects': True},
# ...and a direct assignment on one of those
# children
{'user': 0, 'role': 3, 'project': 2},
# The rest are spoiler assignments
{'user': 0, 'role': 2, 'project': 5},
{'user': 0, 'role': 3, 'project': 4}],
'tests': [
# List all effective assignments for project 1 and its subtree.
{'params': {'project': 1, 'user': 0, 'effective': True,
'include_subtree': True},
'results': [
# First, we should see the inherited user assignment from
# project 0 on all projects in the subtree
{'user': 0, 'role': 0, 'project': 1,
'indirect': {'project': 0}},
{'user': 0, 'role': 0, 'project': 2,
'indirect': {'project': 0}},
{'user': 0, 'role': 0, 'project': 3,
'indirect': {'project': 0}},
# Also the inherited group assignment from project 0 on
# the subtree
{'user': 0, 'role': 1, 'project': 1,
'indirect': {'project': 0, 'group': 0}},
{'user': 0, 'role': 1, 'project': 2,
'indirect': {'project': 0, 'group': 0}},
{'user': 0, 'role': 1, 'project': 3,
'indirect': {'project': 0, 'group': 0}},
# The inherited assignment on project 1 should appear only
# on its children
{'user': 0, 'role': 2, 'project': 2,
'indirect': {'project': 1}},
{'user': 0, 'role': 2, 'project': 3,
'indirect': {'project': 1}},
# And finally the direct assignment on project 2
{'user': 0, 'role': 3, 'project': 2}]}
]
}
self.execute_assignment_plan(test_plan)
def test_list_effective_assignments_for_tree_with_domain_assignments(self):
"""Test we correctly honor domain inherited assignments on the tree"""
# Enable OS-INHERIT extension
self.config_fixture.config(group='os_inherit', enabled=True)
test_plan = {
# Create a domain with a project hierarchy 3 levels deep:
#
# project 0
# ____________|____________
# | |
# project 1 project 4
# ______|_____ ______|_____
# | | | |
# project 2 project 3 project 5 project 6
#
# Also, create 1 user and 4 roles.
'entities': {
'domains': {
'projects': {'project': [{'project': 2},
{'project': 2}]},
'users': 1},
'roles': 4},
'assignments': [
# An inherited assignment on the domain (which should be
# applied to all the projects)
{'user': 0, 'role': 1, 'domain': 0,
'inherited_to_projects': True},
# A direct assignment to project 2
{'user': 0, 'role': 2, 'project': 2},
# ...and two spoiler assignments, one to the root and one
# to project 4
{'user': 0, 'role': 0, 'project': 0},
{'user': 0, 'role': 3, 'project': 4}],
'tests': [
# List all effective assignments for project 1 and its subtree.
{'params': {'project': 1, 'effective': True,
'include_subtree': True},
'results': [
# The inherited assignment from the domain should appear
# only on the part of the subtree we are interested in
{'user': 0, 'role': 1, 'project': 1,
'indirect': {'domain': 0}},
{'user': 0, 'role': 1, 'project': 2,
'indirect': {'domain': 0}},
{'user': 0, 'role': 1, 'project': 3,
'indirect': {'domain': 0}},
# And finally the direct assignment on project 2
{'user': 0, 'role': 2, 'project': 2}]}
]
}
self.execute_assignment_plan(test_plan)
class FilterTests(filtering.FilterTests):
def test_list_entities_filtered(self):