AAP-51957 Move to new DAB RBAC queryset patterns (#16118)

Replace ResourceMixin/accessible_objects/accessible_pk_qs with DAB RBAC access_qs/access_ids_qs

Remove the ResourceMixin class and migrate all callers to the new
DAB RBAC queryset API: access_qs replaces accessible_objects and
access_ids_qs replaces accessible_pk_qs, using permission strings
(view, change, use, etc.) instead of role field names.

Also updates prefetch_page_capabilities to use permission-based
capability definitions instead of role field references.

Restore UNION pattern in TeamAccess.filtered_queryset

The access_qs migration accidentally collapsed the UNION into an OR,
which can be 48x slower at scale (AAP-83319).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alan Rominger
2026-08-25 11:24:18 -04:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 882d2df7f3
commit 7765376450
23 changed files with 132 additions and 202 deletions
+17 -17
View File
@@ -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
+12 -12
View File
@@ -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:
+5 -7
View File
@@ -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')
+4 -5
View File
@@ -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'] = {}
+1 -1
View File
@@ -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)