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)
+31 -29
View File
@@ -74,7 +74,7 @@ from awx.main.models import (
WorkflowApproval,
WorkflowApprovalTemplate,
)
from awx.main.models.mixins import ResourceMixin
__all__ = [
'get_user_queryset',
@@ -692,7 +692,7 @@ class UserAccess(BaseAccess):
returns True if `u` is member of any organization that is
not also an organization that `self.user` admins
"""
return not self.user_organizations(u).exclude(pk__in=Organization.accessible_pk_qs(self.user, 'admin_role')).exists()
return not self.user_organizations(u).exclude(pk__in=Organization.access_ids_qs(self.user, 'change')).exists()
def user_is_orphaned(self, u):
return not self.user_organizations(u).exists()
@@ -905,7 +905,7 @@ class HostAccess(BaseAccess):
prefetch_related = ('groups', 'inventory_sources')
def filtered_queryset(self):
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
def can_add(self, data):
if not data: # So the browseable API will work
@@ -967,7 +967,7 @@ class GroupAccess(BaseAccess):
)
def filtered_queryset(self):
return Group.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
return Group.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
def can_add(self, data):
if not data: # So the browseable API will work
@@ -1009,7 +1009,7 @@ class InventorySourceAccess(NotificationAttachMixin, UnifiedCredentialsMixin, Ba
prefetch_related = ('credentials__credential_type', 'last_job', 'source_project')
def filtered_queryset(self):
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
def can_add(self, data):
if not data or 'inventory' not in data:
@@ -1059,7 +1059,7 @@ class InventoryUpdateAccess(BaseAccess):
prefetch_related = ('unified_job_template', 'instance_group', 'credentials__credential_type', 'inventory')
def filtered_queryset(self):
return self.model.objects.filter(inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
return self.model.objects.filter(inventory_source__inventory__in=Inventory.access_ids_qs(self.user, 'view'))
def can_cancel(self, obj):
if not obj.can_cancel:
@@ -1187,7 +1187,7 @@ class CredentialInputSourceAccess(BaseAccess):
select_related = ('target_credential', 'source_credential')
def filtered_queryset(self):
return CredentialInputSource.objects.filter(target_credential__in=Credential.accessible_pk_qs(self.user, 'read_role'))
return CredentialInputSource.objects.filter(target_credential__in=Credential.access_ids_qs(self.user, 'view'))
@check_superuser
def can_add(self, data):
@@ -1232,10 +1232,8 @@ class TeamAccess(BaseAccess):
Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
):
return self.model.objects.all()
org_member_teams = (
self.model.objects.filter(organization__in=Organization.accessible_pk_qs(self.user, 'member_role')).order_by().values_list('pk', flat=True)
)
direct_read_teams = self.model.objects.filter(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')).order_by().values_list('pk', flat=True)
org_member_teams = self.model.objects.filter(organization__in=Organization.access_ids_qs(self.user, 'member')).order_by().values_list('pk', flat=True)
direct_read_teams = self.model.objects.filter(pk__in=self.model.access_ids_qs(self.user, 'view')).order_by().values_list('pk', flat=True)
return self.model.objects.filter(pk__in=org_member_teams.union(direct_read_teams))
@check_superuser
@@ -1268,7 +1266,7 @@ class TeamAccess(BaseAccess):
if sub_obj.content_object is None:
raise PermissionDenied(_("The {} role cannot be assigned to a team").format(sub_obj.name))
if isinstance(sub_obj.content_object, ResourceMixin):
if permission_registry.is_registered(sub_obj.content_object):
role_access = RoleAccess(self.user)
return role_access.can_attach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
if self.user.is_superuser:
@@ -1284,7 +1282,10 @@ class TeamAccess(BaseAccess):
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
# MANAGE_ORGANIZATION_AUTH setting checked in RoleAccess
if isinstance(sub_obj, Role):
if isinstance(sub_obj.content_object, ResourceMixin):
if sub_obj.content_object is None:
raise PermissionDenied(_("The {} role cannot be unassigned from a team").format(sub_obj.name))
if permission_registry.is_registered(sub_obj.content_object):
role_access = RoleAccess(self.user)
return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
@@ -1423,7 +1424,7 @@ class ProjectUpdateAccess(BaseAccess):
)
def filtered_queryset(self):
return self.model.objects.filter(project__in=Project.accessible_pk_qs(self.user, 'read_role'))
return self.model.objects.filter(project__in=Project.access_ids_qs(self.user, 'view'))
@check_superuser
def can_cancel(self, obj):
@@ -1817,7 +1818,8 @@ class JobLaunchConfigAccess(UnifiedCredentialsMixin, BaseAccess):
if cls is Label:
return LabelAccess(self.user).filtered_queryset()
else:
return cls._accessible_pk_qs(cls, self.user, 'use_role')
action = f'use_{cls._meta.model_name}'
return cls.access_qs(self.user, action)
def has_obj_m2m_access(self, obj):
for relationship, cls in self.M2M_CHECKS.items():
@@ -1961,7 +1963,7 @@ class WorkflowJobNodeAccess(BaseAccess):
def filtered_queryset(self):
return self.model.objects.filter(
Q(workflow_job__unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
Q(workflow_job__unified_job_template__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view'))
| Q(workflow_job__organization__in=Organization.objects.filter(Q(admin_role__members=self.user)))
)
@@ -2105,8 +2107,7 @@ class WorkflowJobAccess(BaseAccess):
def filtered_queryset(self):
return WorkflowJob.objects.filter(
Q(unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
| Q(organization__in=Organization.accessible_pk_qs(self.user, 'auditor_role'))
Q(unified_job_template__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view')) | Q(organization__in=Organization.access_ids_qs(self.user, 'audit'))
)
def can_read(self, obj):
@@ -2211,7 +2212,7 @@ class AdHocCommandAccess(BaseAccess):
)
def filtered_queryset(self):
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
def can_add(self, data, validate_license=True):
if not data: # So the browseable API will work
@@ -2352,7 +2353,7 @@ class ProjectUpdateEventAccess(BaseAccess):
model = ProjectUpdateEvent
def filtered_queryset(self):
return self.model.objects.filter(Q(project_update__project__in=Project.accessible_pk_qs(self.user, 'read_role')))
return self.model.objects.filter(Q(project_update__project__in=Project.access_ids_qs(self.user, 'view')))
def can_add(self, data):
return False
@@ -2372,7 +2373,7 @@ class InventoryUpdateEventAccess(BaseAccess):
model = InventoryUpdateEvent
def filtered_queryset(self):
return self.model.objects.filter(Q(inventory_update__inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')))
return self.model.objects.filter(Q(inventory_update__inventory_source__inventory__in=Inventory.access_ids_qs(self.user, 'view')))
def can_add(self, data):
return False
@@ -2392,7 +2393,8 @@ class ReceptorAddressAccess(BaseAccess):
model = ReceptorAddress
def filtered_queryset(self):
return self.model.objects.filter(Q(instance__in=Instance.accessible_pk_qs(self.user, 'read_role')))
instance_qs = InstanceAccess(self.user).filtered_queryset()
return self.model.objects.filter(Q(instance__in=instance_qs))
@check_superuser
def can_add(self, data):
@@ -2456,7 +2458,7 @@ class UnifiedJobTemplateAccess(BaseAccess):
def filtered_queryset(self):
return self.model.objects.filter(
Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role'))
Q(pk__in=self.model.access_ids_qs(self.user, 'view'))
| Q(
pk__in=InventorySource.objects.filter(
inventory__id__in=Inventory.access_ids_qs(self.user, 'view'),
@@ -2509,7 +2511,7 @@ class UnifiedJobAccess(BaseAccess):
def filtered_queryset(self):
inv_pk_qs = Inventory.access_ids_qs(self.user, 'view')
return self.model.objects.filter(
Q(unified_job_template_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
Q(unified_job_template_id__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view'))
| Q(
pk__in=InventoryUpdate.objects.filter(
inventory_source__inventory__id__in=inv_pk_qs,
@@ -2520,7 +2522,7 @@ class UnifiedJobAccess(BaseAccess):
inventory__id__in=inv_pk_qs,
).values('pk')
)
| Q(organization__in=Organization.access_ids_qs(self.user, 'audit_organization'))
| Q(organization__in=Organization.access_ids_qs(self.user, 'audit'))
)
def get_queryset(self):
@@ -2642,7 +2644,7 @@ class LabelAccess(BaseAccess):
Q(organization__in=Organization.access_ids_qs(self.user, 'view'))
| Q(
pk__in=UnifiedJobTemplate.labels.through.objects.filter(
unifiedjobtemplate_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'),
unifiedjobtemplate_id__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view'),
).values('label_id')
)
)
@@ -2851,7 +2853,7 @@ class RoleAccess(BaseAccess):
if not settings.MANAGE_ORGANIZATION_AUTH and not self.user.is_superuser:
return False
if isinstance(obj.content_object, ResourceMixin) and self.user in obj.content_object.admin_role:
if obj.content_object is not None and permission_registry.is_registered(obj.content_object) and self.user in obj.content_object.admin_role:
return True
return False
@@ -2888,7 +2890,7 @@ class WorkflowApprovalAccess(BaseAccess):
return True
def filtered_queryset(self):
return self.model.objects.filter(unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role'))
return self.model.objects.filter(unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.access_ids_qs(self.user, 'view'))
def can_approve_or_deny(self, obj):
if (obj.workflow_job_template and self.user in obj.workflow_job_template.approval_role) or self.user.is_superuser:
@@ -2934,7 +2936,7 @@ class WorkflowApprovalTemplateAccess(BaseAccess):
return self.user in obj.workflow_job_template.execute_role
def filtered_queryset(self):
return self.model.objects.filter(workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role'))
return self.model.objects.filter(workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.access_ids_qs(self.user, 'view'))
for cls in BaseAccess.__subclasses__():
-1
View File
@@ -75,7 +75,6 @@ from awx.main.models.rbac import ( # noqa
from awx.main.models.mixins import ( # noqa
CustomVirtualEnvMixin,
ExecutionEnvironmentMixin,
ResourceMixin,
SurveyJobMixin,
SurveyJobTemplateMixin,
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.validators import validate_ssh_private_key
from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, PrimordialModel
from awx.main.models.mixins import ResourceMixin
from awx.main.models.rbac import (
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
ROLE_SINGLETON_SYSTEM_AUDITOR,
@@ -77,7 +76,7 @@ def build_safe_env(env):
return safe_env
class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
"""
A credential contains information about how to talk to a remote resource
Usually this is a SSH key location, and possibly an unlock password.
+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.utils.common import get_corrected_cpu, get_cpu_effective_capacity, get_corrected_memory, get_mem_effective_capacity
from awx.main.utils.redis import get_redis_client
from awx.main.models.mixins import RelatedJobsMixin, ResourceMixin
from awx.main.models.mixins import RelatedJobsMixin
from awx.main.models.receptor_address import ReceptorAddress
# ansible-runner
@@ -409,7 +409,7 @@ class Instance(HasPolicyEditsMixin, BaseModel):
self.save_health_data(awx_application_version, get_cpu_count(), get_mem_in_bytes(), update_last_seen=True, errors=errors)
class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin, ResourceMixin):
class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
"""A model representing a Queue/Group of AWX Instances."""
name = models.CharField(max_length=250, unique=True)
+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.unified_jobs import UnifiedJob, UnifiedJobTemplate
from awx.main.models.mixins import (
ResourceMixin,
TaskManagerInventoryUpdateMixin,
RelatedJobsMixin,
CustomVirtualEnvMixin,
@@ -71,7 +70,7 @@ class InventoryConstructedInventoryMembership(models.Model):
)
class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin, OpaQueryPathMixin):
class Inventory(CommonModelNameNotUnique, RelatedJobsMixin, OpaQueryPathMixin):
"""
an inventory source contains lists and hosts.
"""
+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.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField
from awx.main.models.mixins import (
ResourceMixin,
SurveyJobTemplateMixin,
SurveyJobMixin,
TaskManagerJobMixin,
@@ -190,9 +189,7 @@ class JobOptions(BaseModel):
return needed
class JobTemplate(
UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin
):
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin):
"""
A job template is a reusable job definition for applying a project (with
playbook) to an inventory source with a given credential.
+2 -55
View File
@@ -9,7 +9,7 @@ import requests
# Django
from django.apps import apps
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.query import QuerySet
@@ -20,19 +20,17 @@ from ansible_base.lib.utils.models import prevent_search
# AWX
from awx.main.models.rbac import Role, RoleAncestorEntry, to_permissions
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic
from awx.main.utils.execution_environments import get_default_execution_environment
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
from awx.main.fields import AskForField
from awx.main.constants import ACTIVE_STATES, org_role_to_permission
from awx.main.constants import ACTIVE_STATES
logger = logging.getLogger('awx.main.models.mixins')
__all__ = [
'ResourceMixin',
'SurveyJobTemplateMixin',
'SurveyJobMixin',
'TaskManagerUnifiedJobMixin',
@@ -45,57 +43,6 @@ __all__ = [
]
class ResourceMixin(models.Model):
class Meta:
abstract = True
@classmethod
def accessible_objects(cls, accessor, role_field):
"""
Use instead of `MyModel.objects` when you want to only consider
resources that a user has specific permissions for. For example:
MyModel.accessible_objects(user, 'read_role').filter(name__istartswith='bar');
NOTE: This should only be used for list type things.
"""
return ResourceMixin._accessible_objects(cls, accessor, role_field)
@classmethod
def accessible_pk_qs(cls, accessor, role_field):
return ResourceMixin._accessible_pk_qs(cls, accessor, role_field)
@staticmethod
def _accessible_pk_qs(cls, accessor, role_field, content_types=None):
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
if cls._meta.model_name == 'organization' and role_field in org_role_to_permission:
# Organization roles can not use the DAB RBAC shortcuts
# like Organization.access_qs(user, 'change_jobtemplate') is needed
# not just Organization.access_qs(user, 'change') is needed
if accessor.is_superuser:
return cls.objects.values_list('id')
codename = org_role_to_permission[role_field]
return cls.access_ids_qs(accessor, codename, content_types=content_types)
return cls.access_ids_qs(accessor, to_permissions[role_field], content_types=content_types)
if accessor._meta.model_name == 'user':
ancestor_roles = accessor.roles.all()
elif type(accessor) == Role:
ancestor_roles = [accessor]
else:
raise RuntimeError(f'Role filters only valid for users and ancestor role, received {accessor}')
if content_types is None:
ct_kwarg = dict(content_type=ContentType.objects.get_for_model(cls))
else:
ct_kwarg = dict(content_type_id__in=content_types)
return RoleAncestorEntry.objects.filter(ancestor__in=ancestor_roles, role_field=role_field, **ct_kwarg).values_list('object_id').distinct()
@staticmethod
def _accessible_objects(cls, accessor, role_field):
return cls.objects.filter(pk__in=ResourceMixin._accessible_pk_qs(cls, accessor, role_field))
class SurveyJobTemplateMixin(models.Model):
class Meta:
abstract = True
+3 -3
View File
@@ -22,12 +22,12 @@ from awx.main.models.rbac import (
ROLE_SINGLETON_SYSTEM_AUDITOR,
)
from awx.main.models.unified_jobs import UnifiedJob
from awx.main.models.mixins import ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin
from awx.main.models.mixins import CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin
__all__ = ['Organization', 'Team', 'UserSessionMembership']
class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin):
class Organization(CommonModel, NotificationFieldsModel, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin):
"""
An organization is the basic unit of multi-tenancy divisions
"""
@@ -134,7 +134,7 @@ class OrganizationGalaxyCredentialMembership(models.Model):
)
class Team(CommonModelNameNotUnique, ResourceMixin):
class Team(CommonModelNameNotUnique):
"""
A team is a group of users that work on common projects.
"""
+2 -2
View File
@@ -30,7 +30,7 @@ from awx.main.models.unified_jobs import (
UnifiedJobTemplate,
)
from awx.main.models.jobs import Job
from awx.main.models.mixins import ResourceMixin, TaskManagerProjectUpdateMixin, CustomVirtualEnvMixin, RelatedJobsMixin
from awx.main.models.mixins import TaskManagerProjectUpdateMixin, CustomVirtualEnvMixin, RelatedJobsMixin
from awx.main.utils import update_scm_url, polymorphic
from awx.main.utils.ansible import skip_directory, could_be_inventory, could_be_playbook
from awx.main.utils.execution_environments import get_control_plane_execution_environment
@@ -249,7 +249,7 @@ class ProjectOptions(models.Model):
return proj_path + '.lock'
class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin):
class Project(UnifiedJobTemplate, ProjectOptions, CustomVirtualEnvMixin, RelatedJobsMixin):
"""
A project represents a playbook git repo that can access a set of inventories
"""
+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.registrar import activity_stream_registrar
from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
from awx.main.models.rbac import to_permissions
from awx.main.utils.common import (
camelcase_to_underscore,
get_model_for_type,
@@ -214,21 +213,18 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn
return [c for c in cls.__subclasses__() if permission_registry.is_registered(c)]
@classmethod
def accessible_pk_qs(cls, accessor, role_field):
def access_ids_qs(cls, accessor, action):
"""
A re-implementation of accessible pk queryset for the "normal" unified JTs.
Does not return inventory sources or system JTs, these should
be handled inside of get_queryset where it is utilized.
Returns a queryset of IDs for UnifiedJobTemplates accessible to the user.
Handles the polymorphic nature by checking permissions across all submodels.
"""
# do not use this if in a subclass
if cls != UnifiedJobTemplate:
return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field)
action = to_permissions[role_field]
return super(UnifiedJobTemplate, cls).access_ids_qs(accessor, action)
# Special condition for super auditor
role_subclasses = cls._submodels_with_roles()
all_codenames = {f'{action}_{cls._meta.model_name}' for cls in role_subclasses}
all_codenames = {f'{action}_{subcls._meta.model_name}' for subcls in role_subclasses}
if not (all_codenames - accessor.singleton_permissions()):
role_cts = ContentType.objects.get_for_models(*role_subclasses).values()
qs = cls.objects.filter(polymorphic_ctype__in=role_cts)
+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.fields import ImplicitRoleField, JSONBlob, OrderedManyToManyField
from awx.main.models.mixins import (
ResourceMixin,
SurveyJobTemplateMixin,
SurveyJobMixin,
RelatedJobsMixin,
@@ -460,7 +459,7 @@ class WorkflowJobOptions(LaunchTimeConfigBase):
return new_workflow_job
class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin, RelatedJobsMixin, WebhookTemplateMixin):
class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, RelatedJobsMixin, WebhookTemplateMixin):
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
FIELDS_TO_PRESERVE_AT_COPY = [
'labels',
@@ -242,7 +242,7 @@ def test_user_roles_unattach_functional(organization, alice, bob, get):
def test_prefetch_jt_capabilities(job_template, rando):
job_template.execute_role.members.add(rando)
qs = JobTemplate.objects.all()
mapping = prefetch_page_capabilities(JobTemplate, qs, ['admin', 'execute'], rando)
mapping = prefetch_page_capabilities(JobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], rando)
assert mapping[job_template.id] == {'edit': False, 'start': True}
@@ -250,10 +250,10 @@ def test_prefetch_jt_capabilities(job_template, rando):
def test_prefetch_ujt_job_template_capabilities(alice, bob, job_template):
job_template.execute_role.members.add(alice)
qs = UnifiedJobTemplate.objects.all()
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, ['admin', 'execute'], alice)
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], alice)
assert mapping[job_template.id] == {'edit': False, 'start': True}
qs = UnifiedJobTemplate.objects.all()
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, ['admin', 'execute'], bob)
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], bob)
assert mapping[job_template.id] == {'edit': False, 'start': False}
@@ -282,7 +282,7 @@ def test_prefetch_ujt_project_capabilities(alice, project, job_template, mocker)
def test_prefetch_group_capabilities(group, rando):
group.inventory.adhoc_role.members.add(rando)
qs = Group.objects.all()
mapping = prefetch_page_capabilities(Group, qs, ['inventory.admin', 'inventory.adhoc'], rando)
mapping = prefetch_page_capabilities(Group, qs, [{'edit': 'inventory.change'}, {'adhoc': 'inventory.adhoc'}], rando)
assert mapping[group.id] == {'edit': False, 'adhoc': True}
@@ -46,7 +46,7 @@ def test_access_qs_external_auditor(ext_auditor_rd, rando, job_template):
ujt_cls = apps.get_model('main', 'UnifiedJobTemplate')
assert job_template in jt_cls.access_qs(rando)
assert job_template.id in jt_cls.access_ids_qs(rando)
assert job_template.id in ujt_cls.accessible_pk_qs(rando, 'read_role')
assert job_template.id in ujt_cls.access_ids_qs(rando, 'view')
@pytest.mark.django_db
@@ -8,6 +8,7 @@ from crum import impersonate
from awx.main.fields import ImplicitRoleField
from awx.main.models.rbac import get_role_from_object_role, give_creator_permissions, get_role_codenames, get_role_definition
from awx.main.models import ActivityStream, User, Organization, WorkflowJobTemplate, WorkflowJobTemplateNode, Team
from awx.main.constants import org_role_to_permission
from awx.api.versioning import reverse
from ansible_base.rbac.models import RoleUserAssignment, RoleDefinition
@@ -147,11 +148,11 @@ def test_organization_level_permissions(organization, inventory, setup_managed_r
assert u1 not in organization.workflow_admin_role
assert not (set(u1.has_roles.all()) & set(u2.has_roles.all())) # user have no roles in common
# Old style
assert set(Organization.accessible_objects(u1, 'inventory_admin_role')) == set([organization])
assert set(Organization.accessible_objects(u2, 'inventory_admin_role')) == set()
assert set(Organization.accessible_objects(u1, 'workflow_admin_role')) == set()
assert set(Organization.accessible_objects(u2, 'workflow_admin_role')) == set([organization])
# Old style (converted to new style)
assert set(Organization.access_qs(u1, org_role_to_permission['inventory_admin_role'])) == set([organization])
assert set(Organization.access_qs(u2, org_role_to_permission['inventory_admin_role'])) == set()
assert set(Organization.access_qs(u1, org_role_to_permission['workflow_admin_role'])) == set()
assert set(Organization.access_qs(u2, org_role_to_permission['workflow_admin_role'])) == set([organization])
# New style
assert set(Organization.access_qs(u1, 'add_inventory')) == set([organization])
@@ -165,7 +166,7 @@ def test_organization_level_permissions(organization, inventory, setup_managed_r
def test_organization_execute_role(organization, rando, setup_managed_roles):
organization.execute_role.members.add(rando)
assert rando in organization.execute_role
assert set(Organization.accessible_objects(rando, 'execute_role')) == set([organization])
assert set(Organization.access_qs(rando, org_role_to_permission['execute_role'])) == set([organization])
@pytest.mark.django_db
@@ -92,7 +92,7 @@ def test_team_accessible_by(team, user, project):
u = user('team_member', False)
team.member_role.children.add(project.use_role)
assert list(Project.accessible_objects(team, 'read_role')) == [project]
assert list(Project.access_qs(team, 'view')) == [project]
assert u not in project.read_role
team.member_role.members.add(u)
@@ -104,11 +104,11 @@ def test_team_accessible_objects(team, user, project):
u = user('team_member', False)
team.member_role.children.add(project.use_role)
assert len(Project.accessible_objects(team, 'read_role')) == 1
assert not Project.accessible_objects(u, 'read_role')
assert len(Project.access_qs(team, 'view')) == 1
assert not Project.access_qs(u, 'view')
team.member_role.members.add(u)
assert len(Project.accessible_objects(u, 'read_role')) == 1
assert len(Project.access_qs(u, 'view')) == 1
@pytest.mark.django_db
@@ -117,7 +117,7 @@ def test_team_admin_member_access(team, user, project):
team.member_role.children.add(project.use_role)
team.admin_role.members.add(u)
assert len(Project.accessible_objects(u, 'use_role')) == 1
assert len(Project.access_qs(u, 'use')) == 1
@pytest.mark.django_db
@@ -154,7 +154,7 @@ def test_org_admin_team_access(organization, team, user, project):
team.member_role.children.add(project.use_role)
assert len(Project.accessible_objects(u, 'use_role')) == 1
assert len(Project.access_qs(u, 'use')) == 1
@pytest.mark.django_db
+25 -26
View File
@@ -592,18 +592,18 @@ def prefetch_page_capabilities(model, page, prefetch_list, user):
4: {'edit': True, 'start': True},
6: {'edit': False, 'start': False}
}
Each capability is produced for all items in the page in a single query
Each capability is produced for all items in the page in a single query.
Examples of prefetch language:
prefetch_list = ['admin', 'execute']
--> prefetch the admin (edit) and execute (start) permissions for
Examples of prefetch language (permission-based):
prefetch_list = [{'edit': 'change'}, {'start': 'execute'}]
--> prefetch change (edit) and execute (start) permissions for
items in list for current user
prefetch_list = ['inventory.admin']
--> prefetch the related inventory FK permissions for current user,
prefetch_list = [{'edit': 'inventory.change'}]
--> prefetch related inventory FK change permissions for current user,
and put it into the object's cache
prefetch_list = [{'copy': ['inventory.admin', 'project.admin']}]
--> prefetch logical combination of admin permission to inventory AND
project, put into cache dictionary as "copy"
prefetch_list = [{'copy': ['inventory.use', 'project.use']}]
--> prefetch logical combination of use permission to inventory AND
project, put into cache dictionary as 'copy'
"""
page_ids = [obj.id for obj in page]
mapping = {}
@@ -621,29 +621,28 @@ def prefetch_page_capabilities(model, page, prefetch_list, user):
if type(paths) is not list:
paths = [paths]
# Build the query for accessible_objects according the user & role(s)
# Build the query for access_qs according the user & permission(s)
filter_args = []
for role_path in paths:
if '.' in role_path:
res_path = '__'.join(role_path.split('.')[:-1])
role_type = role_path.split('.')[-1]
for perm_path in paths:
if '.' in perm_path:
res_path = '__'.join(perm_path.split('.')[:-1])
action = perm_path.split('.')[-1]
parent_model = model
for subpath in role_path.split('.')[:-1]:
for subpath in perm_path.split('.')[:-1]:
parent_model = parent_model._meta.get_field(subpath).related_model
filter_args.append(
Q(Q(**{'%s__pk__in' % res_path: parent_model.accessible_pk_qs(user, '%s_role' % role_type)}) | Q(**{'%s__isnull' % res_path: True}))
)
access_qs = parent_model.access_qs(user, action)
filter_args.append(Q(Q(**{'%s__in' % res_path: access_qs}) | Q(**{'%s__isnull' % res_path: True})))
else:
role_type = role_path
filter_args.append(Q(**{'pk__in': model.accessible_pk_qs(user, '%s_role' % role_type)}))
action = perm_path
if hasattr(model, 'access_qs'):
filter_args.append(Q(**{'pk__in': model.access_qs(user, action)}))
else:
# UnifiedJobTemplate is not registered with the permission registry,
# so it only has access_ids_qs (not access_qs)
filter_args.append(Q(**{'pk__in': model.access_ids_qs(user, action)}))
if display_method is None:
# Role name translation to UI names for methods
display_method = role_type
if role_type == 'admin':
display_method = 'edit'
elif role_type in ['execute', 'update']:
display_method = 'start'
display_method = action
# Union that query with the list of items on page
filter_args.append(Q(pk__in=page_ids))
@@ -219,11 +219,6 @@ This example allows the Django debug toolbar to work.
.. code-block:: python
# models/mixins.py
class ResourceMixin(models.Model):
"""Common resource management functionality"""
class Meta:
abstract = True
class ExecutionEnvironmentMixin(models.Model):
"""Execution environment configuration"""
class Meta:
+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
`FIELDS_TO_DISCARD_AT_COPY`, as needed. Here is an example:
```python
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin):
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin):
'''
A job template is a reusable job definition for applying a project (with
playbook) to an inventory source with a given credential.