diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d52b9ca8c1..75041dc771 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1356,7 +1356,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): last_update_failed = serializers.BooleanField(read_only=True) last_updated = serializers.DateTimeField(read_only=True) show_capabilities = ['start', 'schedule', 'edit', 'delete', 'copy'] - capabilities_prefetch = ['admin', 'update', {'copy': 'organization.project_admin'}] + capabilities_prefetch = [{'edit': 'change'}, {'start': 'update'}, {'copy': 'organization.add_project'}] class Meta: model = Project @@ -1555,7 +1555,7 @@ class LabelsListMixin(object): class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin): show_capabilities = ['edit', 'delete', 'adhoc', 'copy'] - capabilities_prefetch = ['admin', 'adhoc', {'copy': 'organization.inventory_admin'}] + capabilities_prefetch = [{'edit': 'change'}, {'adhoc': 'adhoc'}, {'copy': 'organization.add_inventory'}] class Meta: model = Inventory @@ -1799,7 +1799,7 @@ class InventoryScriptSerializer(InventorySerializer): class HostSerializer(BaseSerializerWithVariables): show_capabilities = ['edit', 'delete'] - capabilities_prefetch = ['inventory.admin'] + capabilities_prefetch = [{'edit': 'inventory.change'}] has_active_failures = serializers.SerializerMethodField() has_inventory_sources = serializers.SerializerMethodField() @@ -1978,7 +1978,7 @@ class AnsibleFactsSerializer(BaseSerializer): class GroupSerializer(BaseSerializerWithVariables): show_capabilities = ['copy', 'edit', 'delete'] - capabilities_prefetch = ['inventory.admin', 'inventory.adhoc'] + capabilities_prefetch = [{'edit': 'inventory.change'}, {'adhoc': 'inventory.adhoc'}] class Meta: model = Group @@ -2376,7 +2376,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt last_update_failed = serializers.BooleanField(read_only=True) last_updated = serializers.DateTimeField(read_only=True) show_capabilities = ['start', 'schedule', 'edit', 'delete'] - capabilities_prefetch = [{'admin': 'inventory.admin'}, {'start': 'inventory.update'}] + capabilities_prefetch = [{'edit': 'inventory.change'}, {'start': 'inventory.update'}] class Meta: model = InventorySource @@ -2995,7 +2995,7 @@ class CredentialTypeSerializer(BaseSerializer): class CredentialSerializer(BaseSerializer): show_capabilities = ['edit', 'delete', 'copy', 'use'] - capabilities_prefetch = ['admin', 'use'] + capabilities_prefetch = [{'edit': 'change'}, {'use': 'use'}] managed = serializers.ReadOnlyField() class Meta: @@ -3336,7 +3336,7 @@ class JobTemplateMixin(object): class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer): show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete'] - capabilities_prefetch = ['admin', 'execute', {'copy': ['project.use', 'inventory.use']}] + capabilities_prefetch = [{'edit': 'change'}, {'start': 'execute'}, {'copy': ['project.use', 'inventory.use']}] status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES, read_only=True, required=False) @@ -3825,7 +3825,7 @@ class SystemJobCancelSerializer(SystemJobSerializer): class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJobTemplateSerializer): show_capabilities = ['start', 'schedule', 'edit', 'copy', 'delete'] - capabilities_prefetch = ['admin', 'execute', {'copy': 'organization.workflow_admin'}] + capabilities_prefetch = [{'edit': 'change'}, {'start': 'execute'}, {'copy': 'organization.add_workflowjobtemplate'}] limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) @@ -4987,17 +4987,17 @@ class BulkJobLaunchSerializer(serializers.Serializer): raise serializers.ValidationError(_("Template types {type_names} not allowed in bulk jobs").format(type_names=type_names)) for model, obj_list in ujts.items(): - role_field = 'execute_role' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update_role' - self.check_list_permission(model, set([obj.id for obj in obj_list]), role_field) + action = 'execute' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update' + self.check_list_permission(model, set([obj.id for obj in obj_list]), action) self.check_organization_permission(attrs, request) if 'inventory' in attrs: requested_use_inventories.add(attrs['inventory'].id) - self.check_list_permission(Inventory, requested_use_inventories, 'use_role') + self.check_list_permission(Inventory, requested_use_inventories, 'use') - self.check_list_permission(Credential, requested_use_credentials, 'use_role') + self.check_list_permission(Credential, requested_use_credentials, 'use') self.check_list_permission(Label, requested_use_labels) self.check_list_permission(InstanceGroup, requested_use_instance_groups) # TODO: change to use_role for conflict self.check_list_permission(ExecutionEnvironment, requested_use_execution_environments) # TODO: change if roles introduced @@ -5011,14 +5011,14 @@ class BulkJobLaunchSerializer(serializers.Serializer): attrs = super().validate(attrs) return attrs - def check_list_permission(self, model, id_list, role_field=None): + def check_list_permission(self, model, id_list, action=None): if not id_list: return user = self.context['request'].user - if role_field is None: # implies "read" level permission is required + if action is None: # implies "read" level permission is required access_qs = user.get_queryset(model) else: - access_qs = model.accessible_objects(user, role_field) + access_qs = model.access_qs(user, action) not_allowed = set(id_list) - set(access_qs.filter(id__in=id_list).values_list('id', flat=True)) if not_allowed: @@ -5115,7 +5115,7 @@ class BulkJobLaunchSerializer(serializers.Serializer): # - If the orgs is not set, set it to the org of the launching user # - If the user is part of multiple orgs, throw a validation error saying user is part of multiple orgs, please provide one if not request.user.is_superuser: - read_org_qs = Organization.accessible_objects(request.user, 'member_role') + read_org_qs = Organization.access_qs(request.user, 'member') if 'organization' not in attrs or attrs['organization'] == None or attrs['organization'] == '': read_org_ct = read_org_qs.count() if read_org_ct == 1: @@ -5149,7 +5149,7 @@ class BulkJobLaunchSerializer(serializers.Serializer): class NotificationTemplateSerializer(BaseSerializer): show_capabilities = ['edit', 'delete', 'copy'] - capabilities_prefetch = [{'copy': 'organization.admin'}] + capabilities_prefetch = [{'copy': 'organization.add_notificationtemplate'}] class Meta: model = NotificationTemplate diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index f68f52decf..b21d007220 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -835,8 +835,8 @@ class TeamProjectsList(SubListAPIView): def get_queryset(self): team = self.get_parent_object() self.check_parent_access(team) - my_qs = self.model.accessible_objects(self.request.user, 'read_role') - team_qs = models.Project.accessible_objects(team, 'read_role') + my_qs = self.model.access_qs(self.request.user, 'view') + team_qs = models.Project.access_qs(team, 'view') return my_qs & team_qs @@ -1009,7 +1009,7 @@ class ProjectTeamsList(ListAPIView): team_member_parent_roles = models.Role.objects.filter(children__in=roles_on_project, role_field='member_role', content_type=team_ct).distinct() team_ids = team_member_parent_roles.values_list('object_id', flat=True) - my_qs = self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=team_ids) + my_qs = self.model.access_qs(self.request.user, 'view').filter(pk__in=team_ids) return my_qs @@ -1239,7 +1239,7 @@ class UserTeamsList(SubListAPIView): u = get_object_or_404(models.User, pk=self.kwargs['pk']) if not self.request.user.can_access(models.User, 'read', u): raise PermissionDenied() - return models.Team.accessible_objects(self.request.user, 'read_role').filter(Q(member_role__members=u) | Q(admin_role__members=u)).distinct() + return models.Team.access_qs(self.request.user, 'view').filter(Q(member_role__members=u) | Q(admin_role__members=u)).distinct() class UserRolesList(SubListAttachDetachAPIView): @@ -1290,8 +1290,8 @@ class UserProjectsList(SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = models.Project.accessible_objects(self.request.user, 'read_role') - user_qs = models.Project.accessible_objects(parent, 'read_role') + my_qs = models.Project.access_qs(self.request.user, 'view') + user_qs = models.Project.access_qs(parent, 'view') return my_qs & user_qs @@ -1304,7 +1304,7 @@ class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = models.Organization.accessible_objects(self.request.user, 'read_role') + my_qs = models.Organization.access_qs(self.request.user, 'view') user_qs = models.Organization.objects.filter(member_role__members=parent) return my_qs & user_qs @@ -1318,7 +1318,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = models.Organization.accessible_objects(self.request.user, 'read_role') + my_qs = models.Organization.access_qs(self.request.user, 'view') user_qs = models.Organization.objects.filter(admin_role__members=parent) return my_qs & user_qs @@ -1495,8 +1495,8 @@ class UserCredentialsList(SubListCreateAPIView): user = self.get_parent_object() self.check_parent_access(user) - visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role') - user_creds = models.Credential.accessible_objects(user, 'read_role') + visible_creds = models.Credential.access_qs(self.request.user, 'view') + user_creds = models.Credential.access_qs(user, 'view') return user_creds & visible_creds @@ -1511,7 +1511,7 @@ class TeamCredentialsList(SubListCreateAPIView): team = self.get_parent_object() self.check_parent_access(team) - visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role') + visible_creds = models.Credential.access_qs(self.request.user, 'view') team_creds = models.Credential.objects.filter(Q(use_role__parents=team.member_role) | Q(admin_role__parents=team.member_role)) return (team_creds & visible_creds).distinct() @@ -1527,7 +1527,7 @@ class OrganizationCredentialList(SubListCreateAPIView): organization = self.get_parent_object() self.check_parent_access(organization) - user_visible = models.Credential.accessible_objects(self.request.user, 'read_role').all() + user_visible = models.Credential.access_qs(self.request.user, 'view').all() org_set = models.Credential.objects.filter(organization=organization) if self.request.user.is_superuser or self.request.user.is_system_auditor: diff --git a/awx/api/views/mixin.py b/awx/api/views/mixin.py index 1f114cb675..909f540b8f 100644 --- a/awx/api/views/mixin.py +++ b/awx/api/views/mixin.py @@ -157,23 +157,21 @@ class OrganizationCountsMixin(object): return full_context db_results = {} - org_qs = self.model.accessible_objects(self.request.user, 'read_role') + org_qs = self.model.access_qs(self.request.user, 'view') org_id_list = org_qs.values('id') if len(org_id_list) == 0: if self.request.method == 'POST': full_context['related_field_counts'] = {} return full_context - inv_qs = Inventory.accessible_objects(self.request.user, 'read_role') - project_qs = Project.accessible_objects(self.request.user, 'read_role') - jt_qs = JobTemplate.accessible_objects(self.request.user, 'read_role') + inv_qs = Inventory.access_qs(self.request.user, 'view') + project_qs = Project.access_qs(self.request.user, 'view') + jt_qs = JobTemplate.access_qs(self.request.user, 'view') # Produce counts of Foreign Key relationships db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization') - db_results['teams'] = ( - Team.accessible_objects(self.request.user, 'read_role').values('organization').annotate(Count('organization')).order_by('organization') - ) + db_results['teams'] = Team.access_qs(self.request.user, 'view').values('organization').annotate(Count('organization')).order_by('organization') db_results['job_templates'] = jt_qs.values('organization').annotate(Count('organization')).order_by('organization') diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 2e4ef01dd7..48d374ca69 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -77,7 +77,6 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI org_id = int(self.kwargs['pk']) org_counts = {} - access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'} member_rd = RoleDefinition.objects.filter(name='Organization Member').first() admin_rd = RoleDefinition.objects.filter(name='Organization Admin').first() @@ -93,10 +92,10 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI else: org_counts.update({'users': 0, 'admins': 0}) - org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(organization__id=org_id).count() - org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(organization__id=org_id).count() - org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(organization__id=org_id).count() - org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(organization__id=org_id).count() + org_counts['inventories'] = Inventory.access_qs(self.request.user, 'view').filter(organization__id=org_id).count() + org_counts['teams'] = Team.access_qs(self.request.user, 'view').filter(organization__id=org_id).count() + org_counts['projects'] = Project.access_qs(self.request.user, 'view').filter(organization__id=org_id).count() + org_counts['job_templates'] = JobTemplate.access_qs(self.request.user, 'view').filter(organization__id=org_id).count() org_counts['hosts'] = Host.objects.org_active_count(org_id) full_context['related_field_counts'] = {} diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 777a5fc5e3..a7d36a5817 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -369,7 +369,7 @@ class ApiV2ConfigView(APIView): ) else: # Only check JobTemplate access if org check failed - if JobTemplate.accessible_objects(request.user, 'admin_role').exists(): + if JobTemplate.access_qs(request.user, 'change').exists(): data['custom_virtualenvs'] = get_custom_venv_choices() return Response(data) diff --git a/awx/main/access.py b/awx/main/access.py index 24208e4f6c..dc22c45529 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -74,7 +74,7 @@ from awx.main.models import ( WorkflowApproval, WorkflowApprovalTemplate, ) -from awx.main.models.mixins import ResourceMixin + __all__ = [ 'get_user_queryset', @@ -692,7 +692,7 @@ class UserAccess(BaseAccess): returns True if `u` is member of any organization that is not also an organization that `self.user` admins """ - return not self.user_organizations(u).exclude(pk__in=Organization.accessible_pk_qs(self.user, 'admin_role')).exists() + return not self.user_organizations(u).exclude(pk__in=Organization.access_ids_qs(self.user, 'change')).exists() def user_is_orphaned(self, u): return not self.user_organizations(u).exists() @@ -905,7 +905,7 @@ class HostAccess(BaseAccess): prefetch_related = ('groups', 'inventory_sources') def filtered_queryset(self): - return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')) + return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view')) def can_add(self, data): if not data: # So the browseable API will work @@ -967,7 +967,7 @@ class GroupAccess(BaseAccess): ) def filtered_queryset(self): - return Group.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')) + return Group.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view')) def can_add(self, data): if not data: # So the browseable API will work @@ -1009,7 +1009,7 @@ class InventorySourceAccess(NotificationAttachMixin, UnifiedCredentialsMixin, Ba prefetch_related = ('credentials__credential_type', 'last_job', 'source_project') def filtered_queryset(self): - return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')) + return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view')) def can_add(self, data): if not data or 'inventory' not in data: @@ -1059,7 +1059,7 @@ class InventoryUpdateAccess(BaseAccess): prefetch_related = ('unified_job_template', 'instance_group', 'credentials__credential_type', 'inventory') def filtered_queryset(self): - return self.model.objects.filter(inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')) + return self.model.objects.filter(inventory_source__inventory__in=Inventory.access_ids_qs(self.user, 'view')) def can_cancel(self, obj): if not obj.can_cancel: @@ -1187,7 +1187,7 @@ class CredentialInputSourceAccess(BaseAccess): select_related = ('target_credential', 'source_credential') def filtered_queryset(self): - return CredentialInputSource.objects.filter(target_credential__in=Credential.accessible_pk_qs(self.user, 'read_role')) + return CredentialInputSource.objects.filter(target_credential__in=Credential.access_ids_qs(self.user, 'view')) @check_superuser def can_add(self, data): @@ -1232,10 +1232,8 @@ class TeamAccess(BaseAccess): Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists() ): return self.model.objects.all() - org_member_teams = ( - self.model.objects.filter(organization__in=Organization.accessible_pk_qs(self.user, 'member_role')).order_by().values_list('pk', flat=True) - ) - direct_read_teams = self.model.objects.filter(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')).order_by().values_list('pk', flat=True) + org_member_teams = self.model.objects.filter(organization__in=Organization.access_ids_qs(self.user, 'member')).order_by().values_list('pk', flat=True) + direct_read_teams = self.model.objects.filter(pk__in=self.model.access_ids_qs(self.user, 'view')).order_by().values_list('pk', flat=True) return self.model.objects.filter(pk__in=org_member_teams.union(direct_read_teams)) @check_superuser @@ -1268,7 +1266,7 @@ class TeamAccess(BaseAccess): if sub_obj.content_object is None: raise PermissionDenied(_("The {} role cannot be assigned to a team").format(sub_obj.name)) - if isinstance(sub_obj.content_object, ResourceMixin): + if permission_registry.is_registered(sub_obj.content_object): role_access = RoleAccess(self.user) return role_access.can_attach(sub_obj, obj, 'member_role.parents', *args, **kwargs) if self.user.is_superuser: @@ -1284,7 +1282,10 @@ class TeamAccess(BaseAccess): def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): # MANAGE_ORGANIZATION_AUTH setting checked in RoleAccess if isinstance(sub_obj, Role): - if isinstance(sub_obj.content_object, ResourceMixin): + if sub_obj.content_object is None: + raise PermissionDenied(_("The {} role cannot be unassigned from a team").format(sub_obj.name)) + + if permission_registry.is_registered(sub_obj.content_object): role_access = RoleAccess(self.user) return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs) @@ -1423,7 +1424,7 @@ class ProjectUpdateAccess(BaseAccess): ) def filtered_queryset(self): - return self.model.objects.filter(project__in=Project.accessible_pk_qs(self.user, 'read_role')) + return self.model.objects.filter(project__in=Project.access_ids_qs(self.user, 'view')) @check_superuser def can_cancel(self, obj): @@ -1817,7 +1818,8 @@ class JobLaunchConfigAccess(UnifiedCredentialsMixin, BaseAccess): if cls is Label: return LabelAccess(self.user).filtered_queryset() else: - return cls._accessible_pk_qs(cls, self.user, 'use_role') + action = f'use_{cls._meta.model_name}' + return cls.access_qs(self.user, action) def has_obj_m2m_access(self, obj): for relationship, cls in self.M2M_CHECKS.items(): @@ -1961,7 +1963,7 @@ class WorkflowJobNodeAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( - Q(workflow_job__unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) + Q(workflow_job__unified_job_template__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view')) | Q(workflow_job__organization__in=Organization.objects.filter(Q(admin_role__members=self.user))) ) @@ -2105,8 +2107,7 @@ class WorkflowJobAccess(BaseAccess): def filtered_queryset(self): return WorkflowJob.objects.filter( - Q(unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) - | Q(organization__in=Organization.accessible_pk_qs(self.user, 'auditor_role')) + Q(unified_job_template__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view')) | Q(organization__in=Organization.access_ids_qs(self.user, 'audit')) ) def can_read(self, obj): @@ -2211,7 +2212,7 @@ class AdHocCommandAccess(BaseAccess): ) def filtered_queryset(self): - return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')) + return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view')) def can_add(self, data, validate_license=True): if not data: # So the browseable API will work @@ -2352,7 +2353,7 @@ class ProjectUpdateEventAccess(BaseAccess): model = ProjectUpdateEvent def filtered_queryset(self): - return self.model.objects.filter(Q(project_update__project__in=Project.accessible_pk_qs(self.user, 'read_role'))) + return self.model.objects.filter(Q(project_update__project__in=Project.access_ids_qs(self.user, 'view'))) def can_add(self, data): return False @@ -2372,7 +2373,7 @@ class InventoryUpdateEventAccess(BaseAccess): model = InventoryUpdateEvent def filtered_queryset(self): - return self.model.objects.filter(Q(inventory_update__inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))) + return self.model.objects.filter(Q(inventory_update__inventory_source__inventory__in=Inventory.access_ids_qs(self.user, 'view'))) def can_add(self, data): return False @@ -2392,7 +2393,8 @@ class ReceptorAddressAccess(BaseAccess): model = ReceptorAddress def filtered_queryset(self): - return self.model.objects.filter(Q(instance__in=Instance.accessible_pk_qs(self.user, 'read_role'))) + instance_qs = InstanceAccess(self.user).filtered_queryset() + return self.model.objects.filter(Q(instance__in=instance_qs)) @check_superuser def can_add(self, data): @@ -2456,7 +2458,7 @@ class UnifiedJobTemplateAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( - Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')) + Q(pk__in=self.model.access_ids_qs(self.user, 'view')) | Q( pk__in=InventorySource.objects.filter( inventory__id__in=Inventory.access_ids_qs(self.user, 'view'), @@ -2509,7 +2511,7 @@ class UnifiedJobAccess(BaseAccess): def filtered_queryset(self): inv_pk_qs = Inventory.access_ids_qs(self.user, 'view') return self.model.objects.filter( - Q(unified_job_template_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) + Q(unified_job_template_id__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view')) | Q( pk__in=InventoryUpdate.objects.filter( inventory_source__inventory__id__in=inv_pk_qs, @@ -2520,7 +2522,7 @@ class UnifiedJobAccess(BaseAccess): inventory__id__in=inv_pk_qs, ).values('pk') ) - | Q(organization__in=Organization.access_ids_qs(self.user, 'audit_organization')) + | Q(organization__in=Organization.access_ids_qs(self.user, 'audit')) ) def get_queryset(self): @@ -2642,7 +2644,7 @@ class LabelAccess(BaseAccess): Q(organization__in=Organization.access_ids_qs(self.user, 'view')) | Q( pk__in=UnifiedJobTemplate.labels.through.objects.filter( - unifiedjobtemplate_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'), + unifiedjobtemplate_id__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view'), ).values('label_id') ) ) @@ -2851,7 +2853,7 @@ class RoleAccess(BaseAccess): if not settings.MANAGE_ORGANIZATION_AUTH and not self.user.is_superuser: return False - if isinstance(obj.content_object, ResourceMixin) and self.user in obj.content_object.admin_role: + if obj.content_object is not None and permission_registry.is_registered(obj.content_object) and self.user in obj.content_object.admin_role: return True return False @@ -2888,7 +2890,7 @@ class WorkflowApprovalAccess(BaseAccess): return True def filtered_queryset(self): - return self.model.objects.filter(unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role')) + return self.model.objects.filter(unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.access_ids_qs(self.user, 'view')) def can_approve_or_deny(self, obj): if (obj.workflow_job_template and self.user in obj.workflow_job_template.approval_role) or self.user.is_superuser: @@ -2934,7 +2936,7 @@ class WorkflowApprovalTemplateAccess(BaseAccess): return self.user in obj.workflow_job_template.execute_role def filtered_queryset(self): - return self.model.objects.filter(workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role')) + return self.model.objects.filter(workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.access_ids_qs(self.user, 'view')) for cls in BaseAccess.__subclasses__(): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 1bb5f87f24..eeacfdac4d 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -75,7 +75,6 @@ from awx.main.models.rbac import ( # noqa from awx.main.models.mixins import ( # noqa CustomVirtualEnvMixin, ExecutionEnvironmentMixin, - ResourceMixin, SurveyJobMixin, SurveyJobTemplateMixin, TaskManagerInventoryUpdateMixin, diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index bb8a1e9d21..8d5f1bbd50 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -40,7 +40,6 @@ from awx.main.fields import ( from awx.main.utils import decrypt_field, classproperty, set_environ from awx.main.validators import validate_ssh_private_key from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, PrimordialModel -from awx.main.models.mixins import ResourceMixin from awx.main.models.rbac import ( ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR, @@ -77,7 +76,7 @@ def build_safe_env(env): return safe_env -class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): +class Credential(PasswordFieldsModel, CommonModelNameNotUnique): """ A credential contains information about how to talk to a remote resource Usually this is a SSH key location, and possibly an unlock password. diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 455d10d2cb..cb57ef47f0 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -34,7 +34,7 @@ from awx.main.models.rbac import ( from awx.main.models.unified_jobs import UnifiedJob from awx.main.utils.common import get_corrected_cpu, get_cpu_effective_capacity, get_corrected_memory, get_mem_effective_capacity from awx.main.utils.redis import get_redis_client -from awx.main.models.mixins import RelatedJobsMixin, ResourceMixin +from awx.main.models.mixins import RelatedJobsMixin from awx.main.models.receptor_address import ReceptorAddress # ansible-runner @@ -409,7 +409,7 @@ class Instance(HasPolicyEditsMixin, BaseModel): self.save_health_data(awx_application_version, get_cpu_count(), get_mem_in_bytes(), update_last_seen=True, errors=errors) -class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin, ResourceMixin): +class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin): """A model representing a Queue/Group of AWX Instances.""" name = models.CharField(max_length=250, unique=True) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index f323688264..53f1757769 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -42,7 +42,6 @@ from awx.main.models.base import BaseModel, CommonModelNameNotUnique, VarsDictPr from awx.main.models.events import InventoryUpdateEvent, UnpartitionedInventoryUpdateEvent from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate from awx.main.models.mixins import ( - ResourceMixin, TaskManagerInventoryUpdateMixin, RelatedJobsMixin, CustomVirtualEnvMixin, @@ -71,7 +70,7 @@ class InventoryConstructedInventoryMembership(models.Model): ) -class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin, OpaQueryPathMixin): +class Inventory(CommonModelNameNotUnique, RelatedJobsMixin, OpaQueryPathMixin): """ an inventory source contains lists and hosts. """ diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0336ab72c8..06cebf2ac6 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -42,7 +42,6 @@ from awx.main.models.notifications import ( from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPseudoField, polymorphic from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField from awx.main.models.mixins import ( - ResourceMixin, SurveyJobTemplateMixin, SurveyJobMixin, TaskManagerJobMixin, @@ -190,9 +189,7 @@ class JobOptions(BaseModel): return needed -class JobTemplate( - UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin -): +class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin): """ A job template is a reusable job definition for applying a project (with playbook) to an inventory source with a given credential. diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 69a0fbba5d..42b355725c 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -9,7 +9,7 @@ import requests # Django from django.apps import apps from django.conf import settings -from django.contrib.contenttypes.models import ContentType + from django.core.exceptions import ValidationError from django.db import models from django.db.models.query import QuerySet @@ -20,19 +20,17 @@ from ansible_base.lib.utils.models import prevent_search # AWX -from awx.main.models.rbac import Role, RoleAncestorEntry, to_permissions from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic from awx.main.utils.execution_environments import get_default_execution_environment from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted from awx.main.utils.polymorphic import build_polymorphic_ctypes_map from awx.main.fields import AskForField -from awx.main.constants import ACTIVE_STATES, org_role_to_permission +from awx.main.constants import ACTIVE_STATES logger = logging.getLogger('awx.main.models.mixins') __all__ = [ - 'ResourceMixin', 'SurveyJobTemplateMixin', 'SurveyJobMixin', 'TaskManagerUnifiedJobMixin', @@ -45,57 +43,6 @@ __all__ = [ ] -class ResourceMixin(models.Model): - class Meta: - abstract = True - - @classmethod - def accessible_objects(cls, accessor, role_field): - """ - Use instead of `MyModel.objects` when you want to only consider - resources that a user has specific permissions for. For example: - MyModel.accessible_objects(user, 'read_role').filter(name__istartswith='bar'); - NOTE: This should only be used for list type things. - """ - return ResourceMixin._accessible_objects(cls, accessor, role_field) - - @classmethod - def accessible_pk_qs(cls, accessor, role_field): - return ResourceMixin._accessible_pk_qs(cls, accessor, role_field) - - @staticmethod - def _accessible_pk_qs(cls, accessor, role_field, content_types=None): - if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED: - if cls._meta.model_name == 'organization' and role_field in org_role_to_permission: - # Organization roles can not use the DAB RBAC shortcuts - # like Organization.access_qs(user, 'change_jobtemplate') is needed - # not just Organization.access_qs(user, 'change') is needed - if accessor.is_superuser: - return cls.objects.values_list('id') - - codename = org_role_to_permission[role_field] - - return cls.access_ids_qs(accessor, codename, content_types=content_types) - return cls.access_ids_qs(accessor, to_permissions[role_field], content_types=content_types) - if accessor._meta.model_name == 'user': - ancestor_roles = accessor.roles.all() - elif type(accessor) == Role: - ancestor_roles = [accessor] - else: - raise RuntimeError(f'Role filters only valid for users and ancestor role, received {accessor}') - - if content_types is None: - ct_kwarg = dict(content_type=ContentType.objects.get_for_model(cls)) - else: - ct_kwarg = dict(content_type_id__in=content_types) - - return RoleAncestorEntry.objects.filter(ancestor__in=ancestor_roles, role_field=role_field, **ct_kwarg).values_list('object_id').distinct() - - @staticmethod - def _accessible_objects(cls, accessor, role_field): - return cls.objects.filter(pk__in=ResourceMixin._accessible_pk_qs(cls, accessor, role_field)) - - class SurveyJobTemplateMixin(models.Model): class Meta: abstract = True diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 49929307f8..45c72f95d4 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -22,12 +22,12 @@ from awx.main.models.rbac import ( ROLE_SINGLETON_SYSTEM_AUDITOR, ) from awx.main.models.unified_jobs import UnifiedJob -from awx.main.models.mixins import ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin +from awx.main.models.mixins import CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin __all__ = ['Organization', 'Team', 'UserSessionMembership'] -class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin): +class Organization(CommonModel, NotificationFieldsModel, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin): """ An organization is the basic unit of multi-tenancy divisions """ @@ -134,7 +134,7 @@ class OrganizationGalaxyCredentialMembership(models.Model): ) -class Team(CommonModelNameNotUnique, ResourceMixin): +class Team(CommonModelNameNotUnique): """ A team is a group of users that work on common projects. """ diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 83d024045d..fa54472eb4 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -30,7 +30,7 @@ from awx.main.models.unified_jobs import ( UnifiedJobTemplate, ) from awx.main.models.jobs import Job -from awx.main.models.mixins import ResourceMixin, TaskManagerProjectUpdateMixin, CustomVirtualEnvMixin, RelatedJobsMixin +from awx.main.models.mixins import TaskManagerProjectUpdateMixin, CustomVirtualEnvMixin, RelatedJobsMixin from awx.main.utils import update_scm_url, polymorphic from awx.main.utils.ansible import skip_directory, could_be_inventory, could_be_playbook from awx.main.utils.execution_environments import get_control_plane_execution_environment @@ -249,7 +249,7 @@ class ProjectOptions(models.Model): return proj_path + '.lock' -class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin): +class Project(UnifiedJobTemplate, ProjectOptions, CustomVirtualEnvMixin, RelatedJobsMixin): """ A project represents a playbook git repo that can access a set of inventories """ diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 1a8abd47f1..5e5c44231c 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -45,7 +45,6 @@ from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, from awx.main.dispatch import get_task_queuename from awx.main.registrar import activity_stream_registrar from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin -from awx.main.models.rbac import to_permissions from awx.main.utils.common import ( camelcase_to_underscore, get_model_for_type, @@ -214,21 +213,18 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn return [c for c in cls.__subclasses__() if permission_registry.is_registered(c)] @classmethod - def accessible_pk_qs(cls, accessor, role_field): + def access_ids_qs(cls, accessor, action): """ - A re-implementation of accessible pk queryset for the "normal" unified JTs. - Does not return inventory sources or system JTs, these should - be handled inside of get_queryset where it is utilized. + Returns a queryset of IDs for UnifiedJobTemplates accessible to the user. + Handles the polymorphic nature by checking permissions across all submodels. """ # do not use this if in a subclass if cls != UnifiedJobTemplate: - return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field) - - action = to_permissions[role_field] + return super(UnifiedJobTemplate, cls).access_ids_qs(accessor, action) # Special condition for super auditor role_subclasses = cls._submodels_with_roles() - all_codenames = {f'{action}_{cls._meta.model_name}' for cls in role_subclasses} + all_codenames = {f'{action}_{subcls._meta.model_name}' for subcls in role_subclasses} if not (all_codenames - accessor.singleton_permissions()): role_cts = ContentType.objects.get_for_models(*role_subclasses).values() qs = cls.objects.filter(polymorphic_ctype__in=role_cts) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 776fbed373..e450e31828 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -33,7 +33,6 @@ from awx.main.models.base import CreatedModifiedModel, VarsDictProperty from awx.main.models.rbac import ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR from awx.main.fields import ImplicitRoleField, JSONBlob, OrderedManyToManyField from awx.main.models.mixins import ( - ResourceMixin, SurveyJobTemplateMixin, SurveyJobMixin, RelatedJobsMixin, @@ -460,7 +459,7 @@ class WorkflowJobOptions(LaunchTimeConfigBase): return new_workflow_job -class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin, RelatedJobsMixin, WebhookTemplateMixin): +class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, RelatedJobsMixin, WebhookTemplateMixin): SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')] FIELDS_TO_PRESERVE_AT_COPY = [ 'labels', diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index 22d9ec9990..d94f9bc2e4 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -242,7 +242,7 @@ def test_user_roles_unattach_functional(organization, alice, bob, get): def test_prefetch_jt_capabilities(job_template, rando): job_template.execute_role.members.add(rando) qs = JobTemplate.objects.all() - mapping = prefetch_page_capabilities(JobTemplate, qs, ['admin', 'execute'], rando) + mapping = prefetch_page_capabilities(JobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], rando) assert mapping[job_template.id] == {'edit': False, 'start': True} @@ -250,10 +250,10 @@ def test_prefetch_jt_capabilities(job_template, rando): def test_prefetch_ujt_job_template_capabilities(alice, bob, job_template): job_template.execute_role.members.add(alice) qs = UnifiedJobTemplate.objects.all() - mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, ['admin', 'execute'], alice) + mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], alice) assert mapping[job_template.id] == {'edit': False, 'start': True} qs = UnifiedJobTemplate.objects.all() - mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, ['admin', 'execute'], bob) + mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], bob) assert mapping[job_template.id] == {'edit': False, 'start': False} @@ -282,7 +282,7 @@ def test_prefetch_ujt_project_capabilities(alice, project, job_template, mocker) def test_prefetch_group_capabilities(group, rando): group.inventory.adhoc_role.members.add(rando) qs = Group.objects.all() - mapping = prefetch_page_capabilities(Group, qs, ['inventory.admin', 'inventory.adhoc'], rando) + mapping = prefetch_page_capabilities(Group, qs, [{'edit': 'inventory.change'}, {'adhoc': 'inventory.adhoc'}], rando) assert mapping[group.id] == {'edit': False, 'adhoc': True} diff --git a/awx/main/tests/functional/dab_rbac/test_external_auditor.py b/awx/main/tests/functional/dab_rbac/test_external_auditor.py index c3602d7366..2495586b37 100644 --- a/awx/main/tests/functional/dab_rbac/test_external_auditor.py +++ b/awx/main/tests/functional/dab_rbac/test_external_auditor.py @@ -46,7 +46,7 @@ def test_access_qs_external_auditor(ext_auditor_rd, rando, job_template): ujt_cls = apps.get_model('main', 'UnifiedJobTemplate') assert job_template in jt_cls.access_qs(rando) assert job_template.id in jt_cls.access_ids_qs(rando) - assert job_template.id in ujt_cls.accessible_pk_qs(rando, 'read_role') + assert job_template.id in ujt_cls.access_ids_qs(rando, 'view') @pytest.mark.django_db diff --git a/awx/main/tests/functional/dab_rbac/test_translation_layer.py b/awx/main/tests/functional/dab_rbac/test_translation_layer.py index 538ed90192..5844d1bda3 100644 --- a/awx/main/tests/functional/dab_rbac/test_translation_layer.py +++ b/awx/main/tests/functional/dab_rbac/test_translation_layer.py @@ -8,6 +8,7 @@ from crum import impersonate from awx.main.fields import ImplicitRoleField from awx.main.models.rbac import get_role_from_object_role, give_creator_permissions, get_role_codenames, get_role_definition from awx.main.models import ActivityStream, User, Organization, WorkflowJobTemplate, WorkflowJobTemplateNode, Team +from awx.main.constants import org_role_to_permission from awx.api.versioning import reverse from ansible_base.rbac.models import RoleUserAssignment, RoleDefinition @@ -147,11 +148,11 @@ def test_organization_level_permissions(organization, inventory, setup_managed_r assert u1 not in organization.workflow_admin_role assert not (set(u1.has_roles.all()) & set(u2.has_roles.all())) # user have no roles in common - # Old style - assert set(Organization.accessible_objects(u1, 'inventory_admin_role')) == set([organization]) - assert set(Organization.accessible_objects(u2, 'inventory_admin_role')) == set() - assert set(Organization.accessible_objects(u1, 'workflow_admin_role')) == set() - assert set(Organization.accessible_objects(u2, 'workflow_admin_role')) == set([organization]) + # Old style (converted to new style) + assert set(Organization.access_qs(u1, org_role_to_permission['inventory_admin_role'])) == set([organization]) + assert set(Organization.access_qs(u2, org_role_to_permission['inventory_admin_role'])) == set() + assert set(Organization.access_qs(u1, org_role_to_permission['workflow_admin_role'])) == set() + assert set(Organization.access_qs(u2, org_role_to_permission['workflow_admin_role'])) == set([organization]) # New style assert set(Organization.access_qs(u1, 'add_inventory')) == set([organization]) @@ -165,7 +166,7 @@ def test_organization_level_permissions(organization, inventory, setup_managed_r def test_organization_execute_role(organization, rando, setup_managed_roles): organization.execute_role.members.add(rando) assert rando in organization.execute_role - assert set(Organization.accessible_objects(rando, 'execute_role')) == set([organization]) + assert set(Organization.access_qs(rando, org_role_to_permission['execute_role'])) == set([organization]) @pytest.mark.django_db diff --git a/awx/main/tests/functional/rbac/test_rbac_team.py b/awx/main/tests/functional/rbac/test_rbac_team.py index 6c3e68c6c1..12109cc5e7 100644 --- a/awx/main/tests/functional/rbac/test_rbac_team.py +++ b/awx/main/tests/functional/rbac/test_rbac_team.py @@ -92,7 +92,7 @@ def test_team_accessible_by(team, user, project): u = user('team_member', False) team.member_role.children.add(project.use_role) - assert list(Project.accessible_objects(team, 'read_role')) == [project] + assert list(Project.access_qs(team, 'view')) == [project] assert u not in project.read_role team.member_role.members.add(u) @@ -104,11 +104,11 @@ def test_team_accessible_objects(team, user, project): u = user('team_member', False) team.member_role.children.add(project.use_role) - assert len(Project.accessible_objects(team, 'read_role')) == 1 - assert not Project.accessible_objects(u, 'read_role') + assert len(Project.access_qs(team, 'view')) == 1 + assert not Project.access_qs(u, 'view') team.member_role.members.add(u) - assert len(Project.accessible_objects(u, 'read_role')) == 1 + assert len(Project.access_qs(u, 'view')) == 1 @pytest.mark.django_db @@ -117,7 +117,7 @@ def test_team_admin_member_access(team, user, project): team.member_role.children.add(project.use_role) team.admin_role.members.add(u) - assert len(Project.accessible_objects(u, 'use_role')) == 1 + assert len(Project.access_qs(u, 'use')) == 1 @pytest.mark.django_db @@ -154,7 +154,7 @@ def test_org_admin_team_access(organization, team, user, project): team.member_role.children.add(project.use_role) - assert len(Project.accessible_objects(u, 'use_role')) == 1 + assert len(Project.access_qs(u, 'use')) == 1 @pytest.mark.django_db diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 831a7ab61c..ea31b519ee 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -592,18 +592,18 @@ def prefetch_page_capabilities(model, page, prefetch_list, user): 4: {'edit': True, 'start': True}, 6: {'edit': False, 'start': False} } - Each capability is produced for all items in the page in a single query + Each capability is produced for all items in the page in a single query. - Examples of prefetch language: - prefetch_list = ['admin', 'execute'] - --> prefetch the admin (edit) and execute (start) permissions for + Examples of prefetch language (permission-based): + prefetch_list = [{'edit': 'change'}, {'start': 'execute'}] + --> prefetch change (edit) and execute (start) permissions for items in list for current user - prefetch_list = ['inventory.admin'] - --> prefetch the related inventory FK permissions for current user, + prefetch_list = [{'edit': 'inventory.change'}] + --> prefetch related inventory FK change permissions for current user, and put it into the object's cache - prefetch_list = [{'copy': ['inventory.admin', 'project.admin']}] - --> prefetch logical combination of admin permission to inventory AND - project, put into cache dictionary as "copy" + prefetch_list = [{'copy': ['inventory.use', 'project.use']}] + --> prefetch logical combination of use permission to inventory AND + project, put into cache dictionary as 'copy' """ page_ids = [obj.id for obj in page] mapping = {} @@ -621,29 +621,28 @@ def prefetch_page_capabilities(model, page, prefetch_list, user): if type(paths) is not list: paths = [paths] - # Build the query for accessible_objects according the user & role(s) + # Build the query for access_qs according the user & permission(s) filter_args = [] - for role_path in paths: - if '.' in role_path: - res_path = '__'.join(role_path.split('.')[:-1]) - role_type = role_path.split('.')[-1] + for perm_path in paths: + if '.' in perm_path: + res_path = '__'.join(perm_path.split('.')[:-1]) + action = perm_path.split('.')[-1] parent_model = model - for subpath in role_path.split('.')[:-1]: + for subpath in perm_path.split('.')[:-1]: parent_model = parent_model._meta.get_field(subpath).related_model - filter_args.append( - Q(Q(**{'%s__pk__in' % res_path: parent_model.accessible_pk_qs(user, '%s_role' % role_type)}) | Q(**{'%s__isnull' % res_path: True})) - ) + access_qs = parent_model.access_qs(user, action) + filter_args.append(Q(Q(**{'%s__in' % res_path: access_qs}) | Q(**{'%s__isnull' % res_path: True}))) else: - role_type = role_path - filter_args.append(Q(**{'pk__in': model.accessible_pk_qs(user, '%s_role' % role_type)})) + action = perm_path + if hasattr(model, 'access_qs'): + filter_args.append(Q(**{'pk__in': model.access_qs(user, action)})) + else: + # UnifiedJobTemplate is not registered with the permission registry, + # so it only has access_ids_qs (not access_qs) + filter_args.append(Q(**{'pk__in': model.access_ids_qs(user, action)})) if display_method is None: - # Role name translation to UI names for methods - display_method = role_type - if role_type == 'admin': - display_method = 'edit' - elif role_type in ['execute', 'update']: - display_method = 'start' + display_method = action # Union that query with the list of items on page filter_args.append(Q(pk__in=page_ids)) diff --git a/docs/docsite/rst/contributor/DJANGO_REQUIREMENTS.rst b/docs/docsite/rst/contributor/DJANGO_REQUIREMENTS.rst index fe5aebb1aa..d7907dd51a 100644 --- a/docs/docsite/rst/contributor/DJANGO_REQUIREMENTS.rst +++ b/docs/docsite/rst/contributor/DJANGO_REQUIREMENTS.rst @@ -219,11 +219,6 @@ This example allows the Django debug toolbar to work. .. code-block:: python # models/mixins.py - class ResourceMixin(models.Model): - """Common resource management functionality""" - class Meta: - abstract = True - class ExecutionEnvironmentMixin(models.Model): """Execution environment configuration""" class Meta: diff --git a/docs/resource_copy.md b/docs/resource_copy.md index d0d87908d5..18e38645c0 100644 --- a/docs/resource_copy.md +++ b/docs/resource_copy.md @@ -64,7 +64,7 @@ be the same as `serializer_class` of corresponding resource detail view; for exa Third, for the underlying model of the resource, add two macros, `FIELDS_TO_PRESERVE_AT_COPY` and `FIELDS_TO_DISCARD_AT_COPY`, as needed. Here is an example: ```python -class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin): +class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin): ''' A job template is a reusable job definition for applying a project (with playbook) to an inventory source with a given credential.