diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 36db2fd538..a3e4b79371 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -319,7 +319,7 @@ class BaseSerializer(serializers.ModelSerializer): # RBAC summary fields request = self.context.get('request', None) if request and isinstance(obj, ResourceMixin) and request.user.is_authenticated(): - summary_fields['permissions'] = obj.get_permissions(request.user) + summary_fields['active_roles'] = obj.get_permissions(request.user) roles = {} for field in obj._meta.get_fields(): if type(field) is ImplicitRoleField: @@ -1479,7 +1479,7 @@ class ResourceAccessListElementSerializer(UserSerializer): if 'summary_fields' not in ret: ret['summary_fields'] = {} - ret['summary_fields']['permissions'] = get_roles_on_resource(obj, user) + ret['summary_fields']['active_roles'] = get_roles_on_resource(obj, user) def format_role_perm(role): role_dict = { 'id': role.id, 'name': role.name, 'description': role.description} @@ -1489,7 +1489,7 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['related'] = reverse_gfk(role.content_object) except: pass - return { 'role': role_dict, 'permissions': get_roles_on_resource(obj, role)} + return { 'role': role_dict, 'active_roles': get_roles_on_resource(obj, role)} def format_team_role_perm(team_role, permissive_role_ids): role = team_role.children.filter(id__in=permissive_role_ids)[0] @@ -1507,7 +1507,7 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['related'] = reverse_gfk(role.content_object) except: pass - return { 'role': role_dict, 'permissions': get_roles_on_resource(obj, team_role)} + return { 'role': role_dict, 'active_roles': get_roles_on_resource(obj, team_role)} team_content_type = ContentType.objects.get_for_model(Team) content_type = ContentType.objects.get_for_model(obj) diff --git a/awx/main/fields.py b/awx/main/fields.py index cda5266981..e116299bcb 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -130,6 +130,10 @@ class ImplicitRoleField(models.ForeignKey): field_names = [field_names] for field_name in field_names: + # Handle the OR syntax for role parents + if type(field_name) == tuple: + continue + if field_name.startswith('singleton:'): continue @@ -227,8 +231,16 @@ class ImplicitRoleField(models.ForeignKey): paths = self.parent_role if type(self.parent_role) is list else [self.parent_role] parent_roles = set() + for path in paths: - if path.startswith("singleton:"): + if type(path) == tuple: + for or_path in path: + if or_path.startswith("singleton:"): + raise Exception("Unable to use Singleton role in an OR context.") + parents = resolve_role_field(instance, or_path) + if len(parents) is not 0: + break + elif path.startswith("singleton:"): singleton_name = path[10:] Role_ = get_current_apps().get_model('main', 'Role') qs = Role_.objects.filter(singleton_name=singleton_name) @@ -244,6 +256,7 @@ class ImplicitRoleField(models.ForeignKey): parents = [role.id] else: parents = resolve_role_field(instance, path) + for parent in parents: parent_roles.add(parent) return parent_roles diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index 0cd6abca3c..3bfaa311eb 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -175,15 +175,15 @@ class Migration(migrations.Migration): name='use_role', field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True'), ), - migrations.AddField( + migrations.AlterField( model_name='jobtemplate', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=b'project.admin_role', to='main.Role', role_name=b'Job Template Administrator', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=[(b'project.admin_role', b'inventory.admin_role')], to='main.Role', role_name=b'Job Template Administrator', null=b'True'), ), - migrations.AddField( + migrations.AlterField( model_name='jobtemplate', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=b'project.auditor_role', to='main.Role', role_name=b'Job Template Auditor', null=b'True'), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=[(b'project.auditor_role', b'inventory.auditor_role')], to='main.Role', role_name=b'Job Template Auditor', null=b'True'), ), migrations.AddField( model_name='jobtemplate', diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 585b4ed43a..c48007e24a 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -228,12 +228,12 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): admin_role = ImplicitRoleField( role_name='Job Template Administrator', role_description='Full access to all settings', - parent_role='project.admin_role', + parent_role=[('project.admin_role', 'inventory.admin_role')] ) auditor_role = ImplicitRoleField( role_name='Job Template Auditor', role_description='Read-only access to all settings', - parent_role='project.auditor_role', + parent_role=[('project.auditor_role', 'inventory.auditor_role')] ) execute_role = ImplicitRoleField( role_name='Job Template Runner', diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index da9033848c..f757dc580e 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -333,15 +333,13 @@ class RoleAncestorEntry(models.Model): descendent = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') ancestor = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') role_field = models.TextField(null=False) - #content_type_id = models.PositiveIntegerField(null=False) - #object_id = models.PositiveIntegerField(null=False) content_type_id = models.PositiveIntegerField(null=False) object_id = models.PositiveIntegerField(null=False) def get_roles_on_resource(resource, accessor): ''' - Returns a dict (or None) of the roles a accessor has for a given resource. + Returns a string list of the roles a accessor has for a given resource. An accessor can be either a User, Role, or an arbitrary resource that contains one or more Roles associated with it. ''' @@ -355,11 +353,11 @@ def get_roles_on_resource(resource, accessor): roles = Role.objects.filter(content_type__pk=accessor_type.id, object_id=accessor.id) - return { - role_field: True for role_field in + return [ + role_field for role_field in RoleAncestorEntry.objects.filter( ancestor__in=roles, content_type_id=ContentType.objects.get_for_model(resource).id, object_id=resource.id ).values_list('role_field', flat=True) - } + ] diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index cb75cd33ef..3e080c8453 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -420,8 +420,8 @@ def test_ensure_permissions_is_present(organization, get, user): org = response.data assert 'summary_fields' in org - assert 'permissions' in org['summary_fields'] - assert org['summary_fields']['permissions']['read_role'] > 0 + assert 'active_roles' in org['summary_fields'] + assert 'read_role' in org['summary_fields']['active_roles'] @pytest.mark.django_db def test_ensure_role_summary_is_present(organization, get, user): diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 8a153a550a..537052afd2 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -4,6 +4,8 @@ from awx.main.models import ( Role, Organization, Project, + JobTemplate, + Inventory, ) @@ -220,3 +222,29 @@ def test_auto_parenting(): assert org2.admin_role.is_ancestor_of(prj1.admin_role) assert org2.admin_role.is_ancestor_of(prj2.admin_role) +@pytest.mark.django_db +def test_OR_parents(alice, bob): + org1 = Organization.objects.create(name="org1") + + inv = Inventory.objects.create(name='inv1', organization=org1) + prj = Project.objects.create(name='prj1', organization=org1) + + jt1 = JobTemplate.objects.create(name='jt1', inventory=inv) + jt2 = JobTemplate.objects.create(name='jt2', project=prj) + jt3 = JobTemplate.objects.create(name='jt3', inventory=inv, project=prj) + + assert bob not in jt1.admin_role + assert alice not in jt2.admin_role + assert bob not in jt3.admin_role + assert alice not in jt3.admin_role + + inv.admin_role.members.add(bob) + assert bob in jt1.admin_role + assert alice not in jt1.admin_role + + prj.admin_role.members.add(alice) + assert alice in jt2.admin_role + assert bob not in jt2.admin_role + + assert alice in jt3.admin_role + assert bob not in jt3.admin_role