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:
parent
7327e937fd
commit
bfbe1ee96c
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue