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_update_failed = serializers.BooleanField(read_only=True)
last_updated = serializers.DateTimeField(read_only=True) last_updated = serializers.DateTimeField(read_only=True)
show_capabilities = ['start', 'schedule', 'edit', 'delete', 'copy'] 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: class Meta:
model = Project model = Project
@@ -1555,7 +1555,7 @@ class LabelsListMixin(object):
class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin): class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin):
show_capabilities = ['edit', 'delete', 'adhoc', 'copy'] 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: class Meta:
model = Inventory model = Inventory
@@ -1799,7 +1799,7 @@ class InventoryScriptSerializer(InventorySerializer):
class HostSerializer(BaseSerializerWithVariables): class HostSerializer(BaseSerializerWithVariables):
show_capabilities = ['edit', 'delete'] show_capabilities = ['edit', 'delete']
capabilities_prefetch = ['inventory.admin'] capabilities_prefetch = [{'edit': 'inventory.change'}]
has_active_failures = serializers.SerializerMethodField() has_active_failures = serializers.SerializerMethodField()
has_inventory_sources = serializers.SerializerMethodField() has_inventory_sources = serializers.SerializerMethodField()
@@ -1978,7 +1978,7 @@ class AnsibleFactsSerializer(BaseSerializer):
class GroupSerializer(BaseSerializerWithVariables): class GroupSerializer(BaseSerializerWithVariables):
show_capabilities = ['copy', 'edit', 'delete'] show_capabilities = ['copy', 'edit', 'delete']
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc'] capabilities_prefetch = [{'edit': 'inventory.change'}, {'adhoc': 'inventory.adhoc'}]
class Meta: class Meta:
model = Group model = Group
@@ -2376,7 +2376,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
last_update_failed = serializers.BooleanField(read_only=True) last_update_failed = serializers.BooleanField(read_only=True)
last_updated = serializers.DateTimeField(read_only=True) last_updated = serializers.DateTimeField(read_only=True)
show_capabilities = ['start', 'schedule', 'edit', 'delete'] show_capabilities = ['start', 'schedule', 'edit', 'delete']
capabilities_prefetch = [{'admin': 'inventory.admin'}, {'start': 'inventory.update'}] capabilities_prefetch = [{'edit': 'inventory.change'}, {'start': 'inventory.update'}]
class Meta: class Meta:
model = InventorySource model = InventorySource
@@ -2995,7 +2995,7 @@ class CredentialTypeSerializer(BaseSerializer):
class CredentialSerializer(BaseSerializer): class CredentialSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete', 'copy', 'use'] show_capabilities = ['edit', 'delete', 'copy', 'use']
capabilities_prefetch = ['admin', 'use'] capabilities_prefetch = [{'edit': 'change'}, {'use': 'use'}]
managed = serializers.ReadOnlyField() managed = serializers.ReadOnlyField()
class Meta: class Meta:
@@ -3336,7 +3336,7 @@ class JobTemplateMixin(object):
class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer): class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer):
show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete'] 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) 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): class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJobTemplateSerializer):
show_capabilities = ['start', 'schedule', 'edit', 'copy', 'delete'] 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) 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) 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)) raise serializers.ValidationError(_("Template types {type_names} not allowed in bulk jobs").format(type_names=type_names))
for model, obj_list in ujts.items(): for model, obj_list in ujts.items():
role_field = 'execute_role' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update_role' action = 'execute' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update'
self.check_list_permission(model, set([obj.id for obj in obj_list]), role_field) self.check_list_permission(model, set([obj.id for obj in obj_list]), action)
self.check_organization_permission(attrs, request) self.check_organization_permission(attrs, request)
if 'inventory' in attrs: if 'inventory' in attrs:
requested_use_inventories.add(attrs['inventory'].id) 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(Label, requested_use_labels)
self.check_list_permission(InstanceGroup, requested_use_instance_groups) # TODO: change to use_role for conflict 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 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) attrs = super().validate(attrs)
return 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: if not id_list:
return return
user = self.context['request'].user 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) access_qs = user.get_queryset(model)
else: 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)) not_allowed = set(id_list) - set(access_qs.filter(id__in=id_list).values_list('id', flat=True))
if not_allowed: 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 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 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: 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'] == '': if 'organization' not in attrs or attrs['organization'] == None or attrs['organization'] == '':
read_org_ct = read_org_qs.count() read_org_ct = read_org_qs.count()
if read_org_ct == 1: if read_org_ct == 1:
@@ -5149,7 +5149,7 @@ class BulkJobLaunchSerializer(serializers.Serializer):
class NotificationTemplateSerializer(BaseSerializer): class NotificationTemplateSerializer(BaseSerializer):
show_capabilities = ['edit', 'delete', 'copy'] show_capabilities = ['edit', 'delete', 'copy']
capabilities_prefetch = [{'copy': 'organization.admin'}] capabilities_prefetch = [{'copy': 'organization.add_notificationtemplate'}]
class Meta: class Meta:
model = NotificationTemplate model = NotificationTemplate
+12 -12
View File
@@ -835,8 +835,8 @@ class TeamProjectsList(SubListAPIView):
def get_queryset(self): def get_queryset(self):
team = self.get_parent_object() team = self.get_parent_object()
self.check_parent_access(team) self.check_parent_access(team)
my_qs = self.model.accessible_objects(self.request.user, 'read_role') my_qs = self.model.access_qs(self.request.user, 'view')
team_qs = models.Project.accessible_objects(team, 'read_role') team_qs = models.Project.access_qs(team, 'view')
return my_qs & team_qs 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_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) 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 return my_qs
@@ -1239,7 +1239,7 @@ class UserTeamsList(SubListAPIView):
u = get_object_or_404(models.User, pk=self.kwargs['pk']) u = get_object_or_404(models.User, pk=self.kwargs['pk'])
if not self.request.user.can_access(models.User, 'read', u): if not self.request.user.can_access(models.User, 'read', u):
raise PermissionDenied() 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): class UserRolesList(SubListAttachDetachAPIView):
@@ -1290,8 +1290,8 @@ class UserProjectsList(SubListAPIView):
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
self.check_parent_access(parent) self.check_parent_access(parent)
my_qs = models.Project.accessible_objects(self.request.user, 'read_role') my_qs = models.Project.access_qs(self.request.user, 'view')
user_qs = models.Project.accessible_objects(parent, 'read_role') user_qs = models.Project.access_qs(parent, 'view')
return my_qs & user_qs return my_qs & user_qs
@@ -1304,7 +1304,7 @@ class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView):
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
self.check_parent_access(parent) 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) user_qs = models.Organization.objects.filter(member_role__members=parent)
return my_qs & user_qs return my_qs & user_qs
@@ -1318,7 +1318,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView):
def get_queryset(self): def get_queryset(self):
parent = self.get_parent_object() parent = self.get_parent_object()
self.check_parent_access(parent) 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) user_qs = models.Organization.objects.filter(admin_role__members=parent)
return my_qs & user_qs return my_qs & user_qs
@@ -1495,8 +1495,8 @@ class UserCredentialsList(SubListCreateAPIView):
user = self.get_parent_object() user = self.get_parent_object()
self.check_parent_access(user) self.check_parent_access(user)
visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role') visible_creds = models.Credential.access_qs(self.request.user, 'view')
user_creds = models.Credential.accessible_objects(user, 'read_role') user_creds = models.Credential.access_qs(user, 'view')
return user_creds & visible_creds return user_creds & visible_creds
@@ -1511,7 +1511,7 @@ class TeamCredentialsList(SubListCreateAPIView):
team = self.get_parent_object() team = self.get_parent_object()
self.check_parent_access(team) 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)) 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() return (team_creds & visible_creds).distinct()
@@ -1527,7 +1527,7 @@ class OrganizationCredentialList(SubListCreateAPIView):
organization = self.get_parent_object() organization = self.get_parent_object()
self.check_parent_access(organization) 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) org_set = models.Credential.objects.filter(organization=organization)
if self.request.user.is_superuser or self.request.user.is_system_auditor: 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 return full_context
db_results = {} 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') org_id_list = org_qs.values('id')
if len(org_id_list) == 0: if len(org_id_list) == 0:
if self.request.method == 'POST': if self.request.method == 'POST':
full_context['related_field_counts'] = {} full_context['related_field_counts'] = {}
return full_context return full_context
inv_qs = Inventory.accessible_objects(self.request.user, 'read_role') inv_qs = Inventory.access_qs(self.request.user, 'view')
project_qs = Project.accessible_objects(self.request.user, 'read_role') project_qs = Project.access_qs(self.request.user, 'view')
jt_qs = JobTemplate.accessible_objects(self.request.user, 'read_role') jt_qs = JobTemplate.access_qs(self.request.user, 'view')
# Produce counts of Foreign Key relationships # Produce counts of Foreign Key relationships
db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization') db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization')
db_results['teams'] = ( db_results['teams'] = Team.access_qs(self.request.user, 'view').values('organization').annotate(Count('organization')).order_by('organization')
Team.accessible_objects(self.request.user, 'read_role').values('organization').annotate(Count('organization')).order_by('organization')
)
db_results['job_templates'] = jt_qs.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_id = int(self.kwargs['pk'])
org_counts = {} org_counts = {}
access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'}
member_rd = RoleDefinition.objects.filter(name='Organization Member').first() member_rd = RoleDefinition.objects.filter(name='Organization Member').first()
admin_rd = RoleDefinition.objects.filter(name='Organization Admin').first() admin_rd = RoleDefinition.objects.filter(name='Organization Admin').first()
@@ -93,10 +92,10 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
else: else:
org_counts.update({'users': 0, 'admins': 0}) org_counts.update({'users': 0, 'admins': 0})
org_counts['inventories'] = Inventory.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.accessible_objects(**access_kwargs).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.accessible_objects(**access_kwargs).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.accessible_objects(**access_kwargs).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) org_counts['hosts'] = Host.objects.org_active_count(org_id)
full_context['related_field_counts'] = {} full_context['related_field_counts'] = {}
+1 -1
View File
@@ -369,7 +369,7 @@ class ApiV2ConfigView(APIView):
) )
else: else:
# Only check JobTemplate access if org check failed # 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() data['custom_virtualenvs'] = get_custom_venv_choices()
return Response(data) return Response(data)
+31 -29
View File
@@ -74,7 +74,7 @@ from awx.main.models import (
WorkflowApproval, WorkflowApproval,
WorkflowApprovalTemplate, WorkflowApprovalTemplate,
) )
from awx.main.models.mixins import ResourceMixin
__all__ = [ __all__ = [
'get_user_queryset', 'get_user_queryset',
@@ -692,7 +692,7 @@ class UserAccess(BaseAccess):
returns True if `u` is member of any organization that is returns True if `u` is member of any organization that is
not also an organization that `self.user` admins 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): def user_is_orphaned(self, u):
return not self.user_organizations(u).exists() return not self.user_organizations(u).exists()
@@ -905,7 +905,7 @@ class HostAccess(BaseAccess):
prefetch_related = ('groups', 'inventory_sources') prefetch_related = ('groups', 'inventory_sources')
def filtered_queryset(self): 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): def can_add(self, data):
if not data: # So the browseable API will work if not data: # So the browseable API will work
@@ -967,7 +967,7 @@ class GroupAccess(BaseAccess):
) )
def filtered_queryset(self): 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): def can_add(self, data):
if not data: # So the browseable API will work 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') prefetch_related = ('credentials__credential_type', 'last_job', 'source_project')
def filtered_queryset(self): 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): def can_add(self, data):
if not data or 'inventory' not in 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') prefetch_related = ('unified_job_template', 'instance_group', 'credentials__credential_type', 'inventory')
def filtered_queryset(self): 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): def can_cancel(self, obj):
if not obj.can_cancel: if not obj.can_cancel:
@@ -1187,7 +1187,7 @@ class CredentialInputSourceAccess(BaseAccess):
select_related = ('target_credential', 'source_credential') select_related = ('target_credential', 'source_credential')
def filtered_queryset(self): 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 @check_superuser
def can_add(self, data): 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() Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
): ):
return self.model.objects.all() return self.model.objects.all()
org_member_teams = ( org_member_teams = self.model.objects.filter(organization__in=Organization.access_ids_qs(self.user, 'member')).order_by().values_list('pk', flat=True)
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.access_ids_qs(self.user, 'view')).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)
return self.model.objects.filter(pk__in=org_member_teams.union(direct_read_teams)) return self.model.objects.filter(pk__in=org_member_teams.union(direct_read_teams))
@check_superuser @check_superuser
@@ -1268,7 +1266,7 @@ class TeamAccess(BaseAccess):
if sub_obj.content_object is None: if sub_obj.content_object is None:
raise PermissionDenied(_("The {} role cannot be assigned to a team").format(sub_obj.name)) 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) role_access = RoleAccess(self.user)
return role_access.can_attach(sub_obj, obj, 'member_role.parents', *args, **kwargs) return role_access.can_attach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
if self.user.is_superuser: if self.user.is_superuser:
@@ -1284,7 +1282,10 @@ class TeamAccess(BaseAccess):
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
# MANAGE_ORGANIZATION_AUTH setting checked in RoleAccess # MANAGE_ORGANIZATION_AUTH setting checked in RoleAccess
if isinstance(sub_obj, Role): 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) role_access = RoleAccess(self.user)
return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs) return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
@@ -1423,7 +1424,7 @@ class ProjectUpdateAccess(BaseAccess):
) )
def filtered_queryset(self): 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 @check_superuser
def can_cancel(self, obj): def can_cancel(self, obj):
@@ -1817,7 +1818,8 @@ class JobLaunchConfigAccess(UnifiedCredentialsMixin, BaseAccess):
if cls is Label: if cls is Label:
return LabelAccess(self.user).filtered_queryset() return LabelAccess(self.user).filtered_queryset()
else: 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): def has_obj_m2m_access(self, obj):
for relationship, cls in self.M2M_CHECKS.items(): for relationship, cls in self.M2M_CHECKS.items():
@@ -1961,7 +1963,7 @@ class WorkflowJobNodeAccess(BaseAccess):
def filtered_queryset(self): def filtered_queryset(self):
return self.model.objects.filter( 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))) | 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): def filtered_queryset(self):
return WorkflowJob.objects.filter( return WorkflowJob.objects.filter(
Q(unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) Q(unified_job_template__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view')) | Q(organization__in=Organization.access_ids_qs(self.user, 'audit'))
| Q(organization__in=Organization.accessible_pk_qs(self.user, 'auditor_role'))
) )
def can_read(self, obj): def can_read(self, obj):
@@ -2211,7 +2212,7 @@ class AdHocCommandAccess(BaseAccess):
) )
def filtered_queryset(self): 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): def can_add(self, data, validate_license=True):
if not data: # So the browseable API will work if not data: # So the browseable API will work
@@ -2352,7 +2353,7 @@ class ProjectUpdateEventAccess(BaseAccess):
model = ProjectUpdateEvent model = ProjectUpdateEvent
def filtered_queryset(self): 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): def can_add(self, data):
return False return False
@@ -2372,7 +2373,7 @@ class InventoryUpdateEventAccess(BaseAccess):
model = InventoryUpdateEvent model = InventoryUpdateEvent
def filtered_queryset(self): 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): def can_add(self, data):
return False return False
@@ -2392,7 +2393,8 @@ class ReceptorAddressAccess(BaseAccess):
model = ReceptorAddress model = ReceptorAddress
def filtered_queryset(self): 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 @check_superuser
def can_add(self, data): def can_add(self, data):
@@ -2456,7 +2458,7 @@ class UnifiedJobTemplateAccess(BaseAccess):
def filtered_queryset(self): def filtered_queryset(self):
return self.model.objects.filter( 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( | Q(
pk__in=InventorySource.objects.filter( pk__in=InventorySource.objects.filter(
inventory__id__in=Inventory.access_ids_qs(self.user, 'view'), inventory__id__in=Inventory.access_ids_qs(self.user, 'view'),
@@ -2509,7 +2511,7 @@ class UnifiedJobAccess(BaseAccess):
def filtered_queryset(self): def filtered_queryset(self):
inv_pk_qs = Inventory.access_ids_qs(self.user, 'view') inv_pk_qs = Inventory.access_ids_qs(self.user, 'view')
return self.model.objects.filter( 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( | Q(
pk__in=InventoryUpdate.objects.filter( pk__in=InventoryUpdate.objects.filter(
inventory_source__inventory__id__in=inv_pk_qs, inventory_source__inventory__id__in=inv_pk_qs,
@@ -2520,7 +2522,7 @@ class UnifiedJobAccess(BaseAccess):
inventory__id__in=inv_pk_qs, inventory__id__in=inv_pk_qs,
).values('pk') ).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): def get_queryset(self):
@@ -2642,7 +2644,7 @@ class LabelAccess(BaseAccess):
Q(organization__in=Organization.access_ids_qs(self.user, 'view')) Q(organization__in=Organization.access_ids_qs(self.user, 'view'))
| Q( | Q(
pk__in=UnifiedJobTemplate.labels.through.objects.filter( 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') ).values('label_id')
) )
) )
@@ -2851,7 +2853,7 @@ class RoleAccess(BaseAccess):
if not settings.MANAGE_ORGANIZATION_AUTH and not self.user.is_superuser: if not settings.MANAGE_ORGANIZATION_AUTH and not self.user.is_superuser:
return False 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 True
return False return False
@@ -2888,7 +2890,7 @@ class WorkflowApprovalAccess(BaseAccess):
return True return True
def filtered_queryset(self): 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): 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: 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 return self.user in obj.workflow_job_template.execute_role
def filtered_queryset(self): 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__(): for cls in BaseAccess.__subclasses__():
-1
View File
@@ -75,7 +75,6 @@ from awx.main.models.rbac import ( # noqa
from awx.main.models.mixins import ( # noqa from awx.main.models.mixins import ( # noqa
CustomVirtualEnvMixin, CustomVirtualEnvMixin,
ExecutionEnvironmentMixin, ExecutionEnvironmentMixin,
ResourceMixin,
SurveyJobMixin, SurveyJobMixin,
SurveyJobTemplateMixin, SurveyJobTemplateMixin,
TaskManagerInventoryUpdateMixin, TaskManagerInventoryUpdateMixin,
+1 -2
View File
@@ -40,7 +40,6 @@ from awx.main.fields import (
from awx.main.utils import decrypt_field, classproperty, set_environ from awx.main.utils import decrypt_field, classproperty, set_environ
from awx.main.validators import validate_ssh_private_key from awx.main.validators import validate_ssh_private_key
from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, PrimordialModel from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, PrimordialModel
from awx.main.models.mixins import ResourceMixin
from awx.main.models.rbac import ( from awx.main.models.rbac import (
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
ROLE_SINGLETON_SYSTEM_AUDITOR, ROLE_SINGLETON_SYSTEM_AUDITOR,
@@ -77,7 +76,7 @@ def build_safe_env(env):
return safe_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 A credential contains information about how to talk to a remote resource
Usually this is a SSH key location, and possibly an unlock password. Usually this is a SSH key location, and possibly an unlock password.
+2 -2
View File
@@ -34,7 +34,7 @@ from awx.main.models.rbac import (
from awx.main.models.unified_jobs import UnifiedJob 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.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.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 from awx.main.models.receptor_address import ReceptorAddress
# ansible-runner # 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) 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.""" """A model representing a Queue/Group of AWX Instances."""
name = models.CharField(max_length=250, unique=True) name = models.CharField(max_length=250, unique=True)
+1 -2
View File
@@ -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.events import InventoryUpdateEvent, UnpartitionedInventoryUpdateEvent
from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate
from awx.main.models.mixins import ( from awx.main.models.mixins import (
ResourceMixin,
TaskManagerInventoryUpdateMixin, TaskManagerInventoryUpdateMixin,
RelatedJobsMixin, RelatedJobsMixin,
CustomVirtualEnvMixin, 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. an inventory source contains lists and hosts.
""" """
+1 -4
View File
@@ -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.utils import parse_yaml_or_json, getattr_dne, NullablePromptPseudoField, polymorphic
from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField
from awx.main.models.mixins import ( from awx.main.models.mixins import (
ResourceMixin,
SurveyJobTemplateMixin, SurveyJobTemplateMixin,
SurveyJobMixin, SurveyJobMixin,
TaskManagerJobMixin, TaskManagerJobMixin,
@@ -190,9 +189,7 @@ class JobOptions(BaseModel):
return needed return needed
class JobTemplate( class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin):
UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin
):
""" """
A job template is a reusable job definition for applying a project (with A job template is a reusable job definition for applying a project (with
playbook) to an inventory source with a given credential. playbook) to an inventory source with a given credential.
+2 -55
View File
@@ -9,7 +9,7 @@ import requests
# Django # Django
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
@@ -20,19 +20,17 @@ from ansible_base.lib.utils.models import prevent_search
# AWX # 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 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.execution_environments import get_default_execution_environment
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted 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.utils.polymorphic import build_polymorphic_ctypes_map
from awx.main.fields import AskForField 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') logger = logging.getLogger('awx.main.models.mixins')
__all__ = [ __all__ = [
'ResourceMixin',
'SurveyJobTemplateMixin', 'SurveyJobTemplateMixin',
'SurveyJobMixin', 'SurveyJobMixin',
'TaskManagerUnifiedJobMixin', '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 SurveyJobTemplateMixin(models.Model):
class Meta: class Meta:
abstract = True abstract = True
+3 -3
View File
@@ -22,12 +22,12 @@ from awx.main.models.rbac import (
ROLE_SINGLETON_SYSTEM_AUDITOR, ROLE_SINGLETON_SYSTEM_AUDITOR,
) )
from awx.main.models.unified_jobs import UnifiedJob 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'] __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 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. A team is a group of users that work on common projects.
""" """
+2 -2
View File
@@ -30,7 +30,7 @@ from awx.main.models.unified_jobs import (
UnifiedJobTemplate, UnifiedJobTemplate,
) )
from awx.main.models.jobs import Job 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 import update_scm_url, polymorphic
from awx.main.utils.ansible import skip_directory, could_be_inventory, could_be_playbook 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 from awx.main.utils.execution_environments import get_control_plane_execution_environment
@@ -249,7 +249,7 @@ class ProjectOptions(models.Model):
return proj_path + '.lock' 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 A project represents a playbook git repo that can access a set of inventories
""" """
+5 -9
View File
@@ -45,7 +45,6 @@ from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel,
from awx.main.dispatch import get_task_queuename from awx.main.dispatch import get_task_queuename
from awx.main.registrar import activity_stream_registrar from awx.main.registrar import activity_stream_registrar
from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
from awx.main.models.rbac import to_permissions
from awx.main.utils.common import ( from awx.main.utils.common import (
camelcase_to_underscore, camelcase_to_underscore,
get_model_for_type, 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)] return [c for c in cls.__subclasses__() if permission_registry.is_registered(c)]
@classmethod @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. Returns a queryset of IDs for UnifiedJobTemplates accessible to the user.
Does not return inventory sources or system JTs, these should Handles the polymorphic nature by checking permissions across all submodels.
be handled inside of get_queryset where it is utilized.
""" """
# do not use this if in a subclass # do not use this if in a subclass
if cls != UnifiedJobTemplate: if cls != UnifiedJobTemplate:
return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field) return super(UnifiedJobTemplate, cls).access_ids_qs(accessor, action)
action = to_permissions[role_field]
# Special condition for super auditor # Special condition for super auditor
role_subclasses = cls._submodels_with_roles() 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()): if not (all_codenames - accessor.singleton_permissions()):
role_cts = ContentType.objects.get_for_models(*role_subclasses).values() role_cts = ContentType.objects.get_for_models(*role_subclasses).values()
qs = cls.objects.filter(polymorphic_ctype__in=role_cts) qs = cls.objects.filter(polymorphic_ctype__in=role_cts)
+1 -2
View File
@@ -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.models.rbac import ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
from awx.main.fields import ImplicitRoleField, JSONBlob, OrderedManyToManyField from awx.main.fields import ImplicitRoleField, JSONBlob, OrderedManyToManyField
from awx.main.models.mixins import ( from awx.main.models.mixins import (
ResourceMixin,
SurveyJobTemplateMixin, SurveyJobTemplateMixin,
SurveyJobMixin, SurveyJobMixin,
RelatedJobsMixin, RelatedJobsMixin,
@@ -460,7 +459,7 @@ class WorkflowJobOptions(LaunchTimeConfigBase):
return new_workflow_job 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')] SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
FIELDS_TO_PRESERVE_AT_COPY = [ FIELDS_TO_PRESERVE_AT_COPY = [
'labels', 'labels',
@@ -242,7 +242,7 @@ def test_user_roles_unattach_functional(organization, alice, bob, get):
def test_prefetch_jt_capabilities(job_template, rando): def test_prefetch_jt_capabilities(job_template, rando):
job_template.execute_role.members.add(rando) job_template.execute_role.members.add(rando)
qs = JobTemplate.objects.all() 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} 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): def test_prefetch_ujt_job_template_capabilities(alice, bob, job_template):
job_template.execute_role.members.add(alice) job_template.execute_role.members.add(alice)
qs = UnifiedJobTemplate.objects.all() 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} assert mapping[job_template.id] == {'edit': False, 'start': True}
qs = UnifiedJobTemplate.objects.all() 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} 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): def test_prefetch_group_capabilities(group, rando):
group.inventory.adhoc_role.members.add(rando) group.inventory.adhoc_role.members.add(rando)
qs = Group.objects.all() 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} assert mapping[group.id] == {'edit': False, 'adhoc': True}
@@ -46,7 +46,7 @@ def test_access_qs_external_auditor(ext_auditor_rd, rando, job_template):
ujt_cls = apps.get_model('main', 'UnifiedJobTemplate') ujt_cls = apps.get_model('main', 'UnifiedJobTemplate')
assert job_template in jt_cls.access_qs(rando) 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 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 @pytest.mark.django_db
@@ -8,6 +8,7 @@ from crum import impersonate
from awx.main.fields import ImplicitRoleField 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.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.models import ActivityStream, User, Organization, WorkflowJobTemplate, WorkflowJobTemplateNode, Team
from awx.main.constants import org_role_to_permission
from awx.api.versioning import reverse from awx.api.versioning import reverse
from ansible_base.rbac.models import RoleUserAssignment, RoleDefinition 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 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 assert not (set(u1.has_roles.all()) & set(u2.has_roles.all())) # user have no roles in common
# Old style # Old style (converted to new style)
assert set(Organization.accessible_objects(u1, 'inventory_admin_role')) == set([organization]) assert set(Organization.access_qs(u1, org_role_to_permission['inventory_admin_role'])) == set([organization])
assert set(Organization.accessible_objects(u2, 'inventory_admin_role')) == set() assert set(Organization.access_qs(u2, org_role_to_permission['inventory_admin_role'])) == set()
assert set(Organization.accessible_objects(u1, 'workflow_admin_role')) == set() assert set(Organization.access_qs(u1, org_role_to_permission['workflow_admin_role'])) == set()
assert set(Organization.accessible_objects(u2, 'workflow_admin_role')) == set([organization]) assert set(Organization.access_qs(u2, org_role_to_permission['workflow_admin_role'])) == set([organization])
# New style # New style
assert set(Organization.access_qs(u1, 'add_inventory')) == set([organization]) 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): def test_organization_execute_role(organization, rando, setup_managed_roles):
organization.execute_role.members.add(rando) organization.execute_role.members.add(rando)
assert rando in organization.execute_role 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 @pytest.mark.django_db
@@ -92,7 +92,7 @@ def test_team_accessible_by(team, user, project):
u = user('team_member', False) u = user('team_member', False)
team.member_role.children.add(project.use_role) 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 assert u not in project.read_role
team.member_role.members.add(u) team.member_role.members.add(u)
@@ -104,11 +104,11 @@ def test_team_accessible_objects(team, user, project):
u = user('team_member', False) u = user('team_member', False)
team.member_role.children.add(project.use_role) team.member_role.children.add(project.use_role)
assert len(Project.accessible_objects(team, 'read_role')) == 1 assert len(Project.access_qs(team, 'view')) == 1
assert not Project.accessible_objects(u, 'read_role') assert not Project.access_qs(u, 'view')
team.member_role.members.add(u) 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 @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.member_role.children.add(project.use_role)
team.admin_role.members.add(u) 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 @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) 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 @pytest.mark.django_db
+25 -26
View File
@@ -592,18 +592,18 @@ def prefetch_page_capabilities(model, page, prefetch_list, user):
4: {'edit': True, 'start': True}, 4: {'edit': True, 'start': True},
6: {'edit': False, 'start': False} 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: Examples of prefetch language (permission-based):
prefetch_list = ['admin', 'execute'] prefetch_list = [{'edit': 'change'}, {'start': 'execute'}]
--> prefetch the admin (edit) and execute (start) permissions for --> prefetch change (edit) and execute (start) permissions for
items in list for current user items in list for current user
prefetch_list = ['inventory.admin'] prefetch_list = [{'edit': 'inventory.change'}]
--> prefetch the related inventory FK permissions for current user, --> prefetch related inventory FK change permissions for current user,
and put it into the object's cache and put it into the object's cache
prefetch_list = [{'copy': ['inventory.admin', 'project.admin']}] prefetch_list = [{'copy': ['inventory.use', 'project.use']}]
--> prefetch logical combination of admin permission to inventory AND --> prefetch logical combination of use permission to inventory AND
project, put into cache dictionary as "copy" project, put into cache dictionary as 'copy'
""" """
page_ids = [obj.id for obj in page] page_ids = [obj.id for obj in page]
mapping = {} mapping = {}
@@ -621,29 +621,28 @@ def prefetch_page_capabilities(model, page, prefetch_list, user):
if type(paths) is not list: if type(paths) is not list:
paths = [paths] 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 = [] filter_args = []
for role_path in paths: for perm_path in paths:
if '.' in role_path: if '.' in perm_path:
res_path = '__'.join(role_path.split('.')[:-1]) res_path = '__'.join(perm_path.split('.')[:-1])
role_type = role_path.split('.')[-1] action = perm_path.split('.')[-1]
parent_model = model 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 parent_model = parent_model._meta.get_field(subpath).related_model
filter_args.append( access_qs = parent_model.access_qs(user, action)
Q(Q(**{'%s__pk__in' % res_path: parent_model.accessible_pk_qs(user, '%s_role' % role_type)}) | Q(**{'%s__isnull' % res_path: True})) filter_args.append(Q(Q(**{'%s__in' % res_path: access_qs}) | Q(**{'%s__isnull' % res_path: True})))
)
else: else:
role_type = role_path action = perm_path
filter_args.append(Q(**{'pk__in': model.accessible_pk_qs(user, '%s_role' % role_type)})) 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: if display_method is None:
# Role name translation to UI names for methods display_method = action
display_method = role_type
if role_type == 'admin':
display_method = 'edit'
elif role_type in ['execute', 'update']:
display_method = 'start'
# Union that query with the list of items on page # Union that query with the list of items on page
filter_args.append(Q(pk__in=page_ids)) filter_args.append(Q(pk__in=page_ids))
@@ -219,11 +219,6 @@ This example allows the Django debug toolbar to work.
.. code-block:: python .. code-block:: python
# models/mixins.py # models/mixins.py
class ResourceMixin(models.Model):
"""Common resource management functionality"""
class Meta:
abstract = True
class ExecutionEnvironmentMixin(models.Model): class ExecutionEnvironmentMixin(models.Model):
"""Execution environment configuration""" """Execution environment configuration"""
class Meta: class Meta:
+1 -1
View File
@@ -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 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: `FIELDS_TO_DISCARD_AT_COPY`, as needed. Here is an example:
```python ```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 A job template is a reusable job definition for applying a project (with
playbook) to an inventory source with a given credential. playbook) to an inventory source with a given credential.