Fix project federation tokens for inherited roles.

Currently project-scoped federation-generated tokens fail to include
group roles that are inherited to the project from the owning domain.
This error is also exposed via the /auth/projects and
/OS-FEDERATION/projects API calls. This patch patch fixes this.

Change-Id: I1ce5007984938365208630ad901c7c508c57fcd4
Closes-bug: 1389752
Closes-bug: 1385694
This commit is contained in:
Henry Nash 2014-11-07 17:27:46 +00:00
parent 7327e937fd
commit bfbe1ee96c
4 changed files with 295 additions and 57 deletions

View File

@ -234,19 +234,19 @@ class Assignment(keystone_assignment.Driver):
project_refs = query.filter_by(domain_id=domain_id)
return [project_ref.to_dict() for project_ref in project_refs]
def _project_ids_to_dicts(self, session, ids):
if not ids:
return []
else:
query = session.query(Project)
query = query.filter(Project.id.in_(ids))
project_refs = query.all()
return [project_ref.to_dict() for project_ref in project_refs]
def list_projects_for_user(self, user_id, group_ids, hints):
# TODO(henry-nash): Now that we have a single assignment table, we
# should be able to honor the hints list that is provided.
def _project_ids_to_dicts(session, ids):
if not ids:
return []
else:
query = session.query(Project)
query = query.filter(Project.id.in_(ids))
project_refs = query.all()
return [project_ref.to_dict() for project_ref in project_refs]
with sql.transaction() as session:
# First get a list of the projects and domains for which the user
# has any kind of role assigned
@ -266,7 +266,7 @@ class Assignment(keystone_assignment.Driver):
project_ids.add(assignment.target_id)
if not CONF.os_inherit.enabled:
return _project_ids_to_dicts(session, project_ids)
return self._project_ids_to_dicts(session, project_ids)
# Inherited roles are enabled, so check to see if this user has any
# such roles (direct or group) on any domain, in which case we must
@ -288,7 +288,7 @@ class Assignment(keystone_assignment.Driver):
for project_ref in query.all():
project_ids.add(project_ref.id)
return _project_ids_to_dicts(session, project_ids)
return self._project_ids_to_dicts(session, project_ids)
def list_domains_for_user(self, user_id, group_ids, hints):
with sql.transaction() as session:
@ -314,29 +314,46 @@ class Assignment(keystone_assignment.Driver):
def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
def _get_roles_for_groups_on_domain(group_ids, domain_id):
sql_constraints = sqlalchemy.and_(
RoleAssignment.type == AssignmentType.GROUP_DOMAIN,
RoleAssignment.target_id == domain_id,
RoleAssignment.inherited == false(),
Role.id == RoleAssignment.role_id,
RoleAssignment.actor_id.in_(group_ids))
session = sql.get_session()
with session.begin():
query = session.query(Role).filter(
sql_constraints).distinct()
return [role.to_dict() for role in query.all()]
def _get_roles_for_groups_on_project(group_ids, project_id):
def _role_ids_to_dicts(session, ids):
if not ids:
return []
else:
query = session.query(Role)
query = query.filter(Role.id.in_(ids))
role_refs = query.all()
return [role_ref.to_dict() for role_ref in role_refs]
with sql.transaction() as session:
project = self._get_project(session, project_id)
role_ids = self._get_group_project_roles(
session, group_ids, project_id, project.domain_id)
return _role_ids_to_dicts(session, role_ids)
if project_id is not None:
assignment_type = AssignmentType.GROUP_PROJECT
target_id = project_id
return _get_roles_for_groups_on_project(group_ids, project_id)
elif domain_id is not None:
assignment_type = AssignmentType.GROUP_DOMAIN
target_id = domain_id
return _get_roles_for_groups_on_domain(group_ids, domain_id)
else:
raise AttributeError(_("Must specify either domain or project"))
sql_constraints = sqlalchemy.and_(
RoleAssignment.type == assignment_type,
RoleAssignment.target_id == target_id,
RoleAssignment.inherited == false(),
Role.id == RoleAssignment.role_id,
RoleAssignment.actor_id.in_(group_ids))
session = sql.get_session()
with session.begin():
query = session.query(Role).filter(
sql_constraints).distinct()
return [role.to_dict() for role in query.all()]
def get_group_project_roles(self, groups, project_id, project_domain_id):
def _get_group_project_roles(self, session, groups, project_id,
project_domain_id):
sql_constraints = sqlalchemy.and_(
RoleAssignment.type == AssignmentType.GROUP_PROJECT,
RoleAssignment.target_id == project_id)
@ -352,36 +369,67 @@ class Assignment(keystone_assignment.Driver):
# NOTE(morganfainberg): Only select the columns we actually care about
# here, in this case role_id.
with sql.transaction() as session:
query = session.query(RoleAssignment.role_id).filter(
sql_constraints).distinct()
query = session.query(RoleAssignment.role_id).filter(
sql_constraints).distinct()
return [result.role_id for result in query.all()]
def _list_entities_for_groups(self, group_ids, entity):
if entity == Domain:
assignment_type = AssignmentType.GROUP_DOMAIN
else:
assignment_type = AssignmentType.GROUP_PROJECT
def get_group_project_roles(self, groups, project_id, project_domain_id):
with sql.transaction() as session:
return self._get_group_project_roles(session, groups, project_id,
project_domain_id)
def list_projects_for_groups(self, group_ids):
with sql.transaction() as session:
# First get a list of the projects and domains for which the groups
# have any kind of role assigned
query = session.query(RoleAssignment)
query = query.filter(RoleAssignment.actor_id.in_(group_ids))
assignments = query.all()
project_ids = set()
for assignment in assignments:
if (assignment.type == AssignmentType.GROUP_PROJECT):
project_ids.add(assignment.target_id)
if not CONF.os_inherit.enabled:
return self._project_ids_to_dicts(session, project_ids)
# Inherited roles are enabled, so check to see if the groups have
# roles on any domain, in which case we must add in all the
# projects in that domain.
domain_ids = set()
for assignment in assignments:
if ((assignment.type == AssignmentType.GROUP_DOMAIN) and
assignment.inherited):
domain_ids.add(assignment.target_id)
# Get the projects that are owned by all of these domains and
# add them in to the project id list
if domain_ids:
query = session.query(Project.id)
query = query.filter(Project.domain_id.in_(domain_ids))
for project_ref in query.all():
project_ids.add(project_ref.id)
return self._project_ids_to_dicts(session, project_ids)
def list_domains_for_groups(self, group_ids):
group_sql_conditions = sqlalchemy.and_(
RoleAssignment.type == assignment_type,
RoleAssignment.type == AssignmentType.GROUP_DOMAIN,
RoleAssignment.inherited == false(),
entity.id == RoleAssignment.target_id,
Domain.id == RoleAssignment.target_id,
RoleAssignment.actor_id.in_(group_ids))
session = sql.get_session()
with session.begin():
query = session.query(entity).filter(
query = session.query(Domain).filter(
group_sql_conditions)
return [x.to_dict() for x in query.all()]
def list_projects_for_groups(self, group_ids):
return self._list_entities_for_groups(group_ids, Project)
def list_domains_for_groups(self, group_ids):
return self._list_entities_for_groups(group_ids, Domain)
def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
with sql.transaction() as session:
self._get_project(session, tenant_id)

View File

@ -3089,6 +3089,90 @@ class IdentityTests(object):
self.assertIn(role_list[0], role_refs)
self.assertIn(role_list[1], role_refs)
def test_get_roles_for_groups_on_project(self):
"""Test retrieving group project roles.
Test Plan:
- Create two domains, two projects, six groups and six roles
- Project1 is in Domain1, Project2 is in Domain2
- Domain2/Project2 are spoilers
- Assign a different direct group role to each project as well
as both an inherited and non-inherited role to each domain
- Get the group roles for Project 1 - depending on whether we have
enabled inheritance, we should either get back just the direct role
or both the direct one plus the inherited domain role from Domain 1
"""
domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(domain1['id'], domain1)
domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(domain2['id'], domain2)
project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain1['id']}
self.assignment_api.create_project(project1['id'], project1)
project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain2['id']}
self.assignment_api.create_project(project2['id'], project2)
group_list = []
group_id_list = []
role_list = []
for _ in range(6):
group = {'name': uuid.uuid4().hex, 'domain_id': domain1['id']}
group = self.identity_api.create_group(group)
group_list.append(group)
group_id_list.append(group['id'])
role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_role(role['id'], role)
role_list.append(role)
# Assign the roles - one inherited and one non-inherited on Domain1,
# plus one on Project1
self.assignment_api.create_grant(group_id=group_list[0]['id'],
domain_id=domain1['id'],
role_id=role_list[0]['id'])
self.assignment_api.create_grant(group_id=group_list[1]['id'],
domain_id=domain1['id'],
role_id=role_list[1]['id'],
inherited_to_projects=True)
self.assignment_api.create_grant(group_id=group_list[2]['id'],
project_id=project1['id'],
role_id=role_list[2]['id'])
# ...and a duplicate set of spoiler assignments to Domain2/Project2
self.assignment_api.create_grant(group_id=group_list[3]['id'],
domain_id=domain2['id'],
role_id=role_list[3]['id'])
self.assignment_api.create_grant(group_id=group_list[4]['id'],
domain_id=domain2['id'],
role_id=role_list[4]['id'],
inherited_to_projects=True)
self.assignment_api.create_grant(group_id=group_list[5]['id'],
project_id=project2['id'],
role_id=role_list[5]['id'])
# Now get the effective roles for all groups on the Project1. With
# inheritance off, we should only get back the direct role.
self.config_fixture.config(group='os_inherit', enabled=False)
role_refs = self.assignment_api.get_roles_for_groups(
group_id_list, project_id=project1['id'])
self.assertThat(role_refs, matchers.HasLength(1))
self.assertIn(role_list[2], role_refs)
# With inheritance on, we should also get back the inherited role from
# its owning domain.
self.config_fixture.config(group='os_inherit', enabled=True)
role_refs = self.assignment_api.get_roles_for_groups(
group_id_list, project_id=project1['id'])
self.assertThat(role_refs, matchers.HasLength(2))
self.assertIn(role_list[1], role_refs)
self.assertIn(role_list[2], role_refs)
def test_list_domains_for_groups(self):
"""Test retrieving domains for a list of groups.
@ -3138,6 +3222,104 @@ class IdentityTests(object):
self.assertIn(domain_list[0], domain_refs)
self.assertIn(domain_list[1], domain_refs)
def test_list_projects_for_groups(self):
"""Test retrieving projects for a list of groups.
Test Plan:
- Create two domains, four projects, seven groups and seven roles
- Project1-3 are in Domain1, Project4 is in Domain2
- Domain2/Project4 are spoilers
- Project1 and 2 have direct group roles, Project3 has no direct
roles but should inherit a group role from Domain1
- Get the projects for the group roles that are assigned to Project1
Project2 and the inherited one on Domain1. Depending on whether we
have enabled inheritance, we should either get back just the projects
with direct roles (Project 1 and 2) or also Project3 due to its
inherited role from Domain1.
"""
domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(domain1['id'], domain1)
domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(domain2['id'], domain2)
project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain1['id']}
project1 = self.assignment_api.create_project(project1['id'], project1)
project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain1['id']}
project2 = self.assignment_api.create_project(project2['id'], project2)
project3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain1['id']}
project3 = self.assignment_api.create_project(project3['id'], project3)
project4 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'domain_id': domain2['id']}
project4 = self.assignment_api.create_project(project4['id'], project4)
group_list = []
role_list = []
for _ in range(7):
group = {'name': uuid.uuid4().hex, 'domain_id': domain1['id']}
group = self.identity_api.create_group(group)
group_list.append(group)
role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_role(role['id'], role)
role_list.append(role)
# Assign the roles - one inherited and one non-inherited on Domain1,
# plus one on Project1 and Project2
self.assignment_api.create_grant(group_id=group_list[0]['id'],
domain_id=domain1['id'],
role_id=role_list[0]['id'])
self.assignment_api.create_grant(group_id=group_list[1]['id'],
domain_id=domain1['id'],
role_id=role_list[1]['id'],
inherited_to_projects=True)
self.assignment_api.create_grant(group_id=group_list[2]['id'],
project_id=project1['id'],
role_id=role_list[2]['id'])
self.assignment_api.create_grant(group_id=group_list[3]['id'],
project_id=project2['id'],
role_id=role_list[3]['id'])
# ...and a few of spoiler assignments to Domain2/Project4
self.assignment_api.create_grant(group_id=group_list[4]['id'],
domain_id=domain2['id'],
role_id=role_list[4]['id'])
self.assignment_api.create_grant(group_id=group_list[5]['id'],
domain_id=domain2['id'],
role_id=role_list[5]['id'],
inherited_to_projects=True)
self.assignment_api.create_grant(group_id=group_list[6]['id'],
project_id=project4['id'],
role_id=role_list[6]['id'])
# Now get the projects for the groups that have roles on Project1,
# Project2 and the inherited role on Domain!. With inheritance off,
# we should only get back the projects with direct role.
self.config_fixture.config(group='os_inherit', enabled=False)
group_id_list = [group_list[1]['id'], group_list[2]['id'],
group_list[3]['id']]
project_refs = (
self.assignment_api.list_projects_for_groups(group_id_list))
self.assertThat(project_refs, matchers.HasLength(2))
self.assertIn(project1, project_refs)
self.assertIn(project2, project_refs)
# With inheritance on, we should also get back the Project3 due to the
# inherited role from its owning domain.
self.config_fixture.config(group='os_inherit', enabled=True)
project_refs = (
self.assignment_api.list_projects_for_groups(group_id_list))
self.assertThat(project_refs, matchers.HasLength(3))
self.assertIn(project1, project_refs)
self.assertIn(project2, project_refs)
self.assertIn(project3, project_refs)
class TokenTests(object):
def _create_token_id(self):

View File

@ -412,9 +412,15 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
def test_get_roles_for_groups_on_domain(self):
self.skipTest('Blocked by bug: 1390125')
def test_get_roles_for_groups_on_project(self):
self.skipTest('Blocked by bug: 1390125')
def test_list_domains_for_groups(self):
self.skipTest('N/A: LDAP does not support multiple domains')
def test_list_projects_for_groups(self):
self.skipTest('Blocked by bug: 1390125')
def test_list_role_assignments_unfiltered(self):
new_domain = self._get_domain_fixture()
new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex,

View File

@ -1023,13 +1023,16 @@ class FederatedTokenTests(FederationTests):
def test_scope_to_project_with_only_inherited_roles(self):
"""Try to scope token whose only roles are inherited."""
# TODO(henry-nash): This *should* work, but currently fails due
# to bug 1389752.
self.config_fixture.config(group='os_inherit', enabled=True)
self.v3_authenticate_token(
self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER,
expected_status=401)
r = self.v3_authenticate_token(
self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER)
token_resp = r.result['token']
project_id = token_resp['project']['id']
self.assertEqual(project_id, self.project_inherited['id'])
self._check_scoped_token_attributes(token_resp)
roles_ref = [self.role_customer]
projects_ref = self.project_inherited
self._check_projects_and_roles(token_resp, roles_ref, projects_ref)
def test_scope_token_from_nonexistent_unscoped_token(self):
"""Try to scope token from non-existent unscoped token."""
@ -1100,16 +1103,15 @@ class FederatedTokenTests(FederationTests):
self.tokens['EMPLOYEE_ASSERTION'],
self.tokens['ADMIN_ASSERTION'])
# TODO(henry-nash): The customer and admin assertions should both also
# get back project_inherited. This fails due to bug 1389752.
self.config_fixture.config(group='os_inherit', enabled=True)
projects_refs = (set([self.proj_customers['id']]),
projects_refs = (set([self.proj_customers['id'],
self.project_inherited['id']]),
set([self.proj_employees['id'],
self.project_all['id']]),
set([self.proj_employees['id'],
self.project_all['id'],
self.proj_customers['id']]))
self.proj_customers['id'],
self.project_inherited['id']]))
for token, projects_ref in zip(token, projects_refs):
for url in urls: