mirror of
https://github.com/ansible/awx.git
synced 2026-10-31 06:26:15 -02:30
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:
co-authored by
Claude Opus 4.6
parent
882d2df7f3
commit
7765376450
+17
-17
@@ -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
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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'] = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user