mirror of
https://github.com/ansible/awx.git
synced 2026-10-29 22:03:20 -02:30
AAP-51957 Move to new DAB RBAC queryset patterns (#16118)
Replace ResourceMixin/accessible_objects/accessible_pk_qs with DAB RBAC access_qs/access_ids_qs Remove the ResourceMixin class and migrate all callers to the new DAB RBAC queryset API: access_qs replaces accessible_objects and access_ids_qs replaces accessible_pk_qs, using permission strings (view, change, use, etc.) instead of role field names. Also updates prefetch_page_capabilities to use permission-based capability definitions instead of role field references. Restore UNION pattern in TeamAccess.filtered_queryset The access_qs migration accidentally collapsed the UNION into an OR, which can be 48x slower at scale (AAP-83319). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
882d2df7f3
commit
7765376450
+17
-17
@@ -1356,7 +1356,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
||||
last_update_failed = serializers.BooleanField(read_only=True)
|
||||
last_updated = serializers.DateTimeField(read_only=True)
|
||||
show_capabilities = ['start', 'schedule', 'edit', 'delete', 'copy']
|
||||
capabilities_prefetch = ['admin', 'update', {'copy': 'organization.project_admin'}]
|
||||
capabilities_prefetch = [{'edit': 'change'}, {'start': 'update'}, {'copy': 'organization.add_project'}]
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -1555,7 +1555,7 @@ class LabelsListMixin(object):
|
||||
|
||||
class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin):
|
||||
show_capabilities = ['edit', 'delete', 'adhoc', 'copy']
|
||||
capabilities_prefetch = ['admin', 'adhoc', {'copy': 'organization.inventory_admin'}]
|
||||
capabilities_prefetch = [{'edit': 'change'}, {'adhoc': 'adhoc'}, {'copy': 'organization.add_inventory'}]
|
||||
|
||||
class Meta:
|
||||
model = Inventory
|
||||
@@ -1799,7 +1799,7 @@ class InventoryScriptSerializer(InventorySerializer):
|
||||
|
||||
class HostSerializer(BaseSerializerWithVariables):
|
||||
show_capabilities = ['edit', 'delete']
|
||||
capabilities_prefetch = ['inventory.admin']
|
||||
capabilities_prefetch = [{'edit': 'inventory.change'}]
|
||||
|
||||
has_active_failures = serializers.SerializerMethodField()
|
||||
has_inventory_sources = serializers.SerializerMethodField()
|
||||
@@ -1978,7 +1978,7 @@ class AnsibleFactsSerializer(BaseSerializer):
|
||||
|
||||
class GroupSerializer(BaseSerializerWithVariables):
|
||||
show_capabilities = ['copy', 'edit', 'delete']
|
||||
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc']
|
||||
capabilities_prefetch = [{'edit': 'inventory.change'}, {'adhoc': 'inventory.adhoc'}]
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
@@ -2376,7 +2376,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
||||
last_update_failed = serializers.BooleanField(read_only=True)
|
||||
last_updated = serializers.DateTimeField(read_only=True)
|
||||
show_capabilities = ['start', 'schedule', 'edit', 'delete']
|
||||
capabilities_prefetch = [{'admin': 'inventory.admin'}, {'start': 'inventory.update'}]
|
||||
capabilities_prefetch = [{'edit': 'inventory.change'}, {'start': 'inventory.update'}]
|
||||
|
||||
class Meta:
|
||||
model = InventorySource
|
||||
@@ -2995,7 +2995,7 @@ class CredentialTypeSerializer(BaseSerializer):
|
||||
|
||||
class CredentialSerializer(BaseSerializer):
|
||||
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
||||
capabilities_prefetch = ['admin', 'use']
|
||||
capabilities_prefetch = [{'edit': 'change'}, {'use': 'use'}]
|
||||
managed = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
@@ -3336,7 +3336,7 @@ class JobTemplateMixin(object):
|
||||
|
||||
class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||
show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete']
|
||||
capabilities_prefetch = ['admin', 'execute', {'copy': ['project.use', 'inventory.use']}]
|
||||
capabilities_prefetch = [{'edit': 'change'}, {'start': 'execute'}, {'copy': ['project.use', 'inventory.use']}]
|
||||
|
||||
status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES, read_only=True, required=False)
|
||||
|
||||
@@ -3825,7 +3825,7 @@ class SystemJobCancelSerializer(SystemJobSerializer):
|
||||
|
||||
class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJobTemplateSerializer):
|
||||
show_capabilities = ['start', 'schedule', 'edit', 'copy', 'delete']
|
||||
capabilities_prefetch = ['admin', 'execute', {'copy': 'organization.workflow_admin'}]
|
||||
capabilities_prefetch = [{'edit': 'change'}, {'start': 'execute'}, {'copy': 'organization.add_workflowjobtemplate'}]
|
||||
limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
|
||||
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
|
||||
|
||||
@@ -4987,17 +4987,17 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
||||
raise serializers.ValidationError(_("Template types {type_names} not allowed in bulk jobs").format(type_names=type_names))
|
||||
|
||||
for model, obj_list in ujts.items():
|
||||
role_field = 'execute_role' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update_role'
|
||||
self.check_list_permission(model, set([obj.id for obj in obj_list]), role_field)
|
||||
action = 'execute' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update'
|
||||
self.check_list_permission(model, set([obj.id for obj in obj_list]), action)
|
||||
|
||||
self.check_organization_permission(attrs, request)
|
||||
|
||||
if 'inventory' in attrs:
|
||||
requested_use_inventories.add(attrs['inventory'].id)
|
||||
|
||||
self.check_list_permission(Inventory, requested_use_inventories, 'use_role')
|
||||
self.check_list_permission(Inventory, requested_use_inventories, 'use')
|
||||
|
||||
self.check_list_permission(Credential, requested_use_credentials, 'use_role')
|
||||
self.check_list_permission(Credential, requested_use_credentials, 'use')
|
||||
self.check_list_permission(Label, requested_use_labels)
|
||||
self.check_list_permission(InstanceGroup, requested_use_instance_groups) # TODO: change to use_role for conflict
|
||||
self.check_list_permission(ExecutionEnvironment, requested_use_execution_environments) # TODO: change if roles introduced
|
||||
@@ -5011,14 +5011,14 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
||||
attrs = super().validate(attrs)
|
||||
return attrs
|
||||
|
||||
def check_list_permission(self, model, id_list, role_field=None):
|
||||
def check_list_permission(self, model, id_list, action=None):
|
||||
if not id_list:
|
||||
return
|
||||
user = self.context['request'].user
|
||||
if role_field is None: # implies "read" level permission is required
|
||||
if action is None: # implies "read" level permission is required
|
||||
access_qs = user.get_queryset(model)
|
||||
else:
|
||||
access_qs = model.accessible_objects(user, role_field)
|
||||
access_qs = model.access_qs(user, action)
|
||||
|
||||
not_allowed = set(id_list) - set(access_qs.filter(id__in=id_list).values_list('id', flat=True))
|
||||
if not_allowed:
|
||||
@@ -5115,7 +5115,7 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
||||
# - If the orgs is not set, set it to the org of the launching user
|
||||
# - If the user is part of multiple orgs, throw a validation error saying user is part of multiple orgs, please provide one
|
||||
if not request.user.is_superuser:
|
||||
read_org_qs = Organization.accessible_objects(request.user, 'member_role')
|
||||
read_org_qs = Organization.access_qs(request.user, 'member')
|
||||
if 'organization' not in attrs or attrs['organization'] == None or attrs['organization'] == '':
|
||||
read_org_ct = read_org_qs.count()
|
||||
if read_org_ct == 1:
|
||||
@@ -5149,7 +5149,7 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
||||
|
||||
class NotificationTemplateSerializer(BaseSerializer):
|
||||
show_capabilities = ['edit', 'delete', 'copy']
|
||||
capabilities_prefetch = [{'copy': 'organization.admin'}]
|
||||
capabilities_prefetch = [{'copy': 'organization.add_notificationtemplate'}]
|
||||
|
||||
class Meta:
|
||||
model = NotificationTemplate
|
||||
|
||||
+12
-12
@@ -835,8 +835,8 @@ class TeamProjectsList(SubListAPIView):
|
||||
def get_queryset(self):
|
||||
team = self.get_parent_object()
|
||||
self.check_parent_access(team)
|
||||
my_qs = self.model.accessible_objects(self.request.user, 'read_role')
|
||||
team_qs = models.Project.accessible_objects(team, 'read_role')
|
||||
my_qs = self.model.access_qs(self.request.user, 'view')
|
||||
team_qs = models.Project.access_qs(team, 'view')
|
||||
return my_qs & team_qs
|
||||
|
||||
|
||||
@@ -1009,7 +1009,7 @@ class ProjectTeamsList(ListAPIView):
|
||||
team_member_parent_roles = models.Role.objects.filter(children__in=roles_on_project, role_field='member_role', content_type=team_ct).distinct()
|
||||
|
||||
team_ids = team_member_parent_roles.values_list('object_id', flat=True)
|
||||
my_qs = self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=team_ids)
|
||||
my_qs = self.model.access_qs(self.request.user, 'view').filter(pk__in=team_ids)
|
||||
return my_qs
|
||||
|
||||
|
||||
@@ -1239,7 +1239,7 @@ class UserTeamsList(SubListAPIView):
|
||||
u = get_object_or_404(models.User, pk=self.kwargs['pk'])
|
||||
if not self.request.user.can_access(models.User, 'read', u):
|
||||
raise PermissionDenied()
|
||||
return models.Team.accessible_objects(self.request.user, 'read_role').filter(Q(member_role__members=u) | Q(admin_role__members=u)).distinct()
|
||||
return models.Team.access_qs(self.request.user, 'view').filter(Q(member_role__members=u) | Q(admin_role__members=u)).distinct()
|
||||
|
||||
|
||||
class UserRolesList(SubListAttachDetachAPIView):
|
||||
@@ -1290,8 +1290,8 @@ class UserProjectsList(SubListAPIView):
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
my_qs = models.Project.accessible_objects(self.request.user, 'read_role')
|
||||
user_qs = models.Project.accessible_objects(parent, 'read_role')
|
||||
my_qs = models.Project.access_qs(self.request.user, 'view')
|
||||
user_qs = models.Project.access_qs(parent, 'view')
|
||||
return my_qs & user_qs
|
||||
|
||||
|
||||
@@ -1304,7 +1304,7 @@ class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView):
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
my_qs = models.Organization.accessible_objects(self.request.user, 'read_role')
|
||||
my_qs = models.Organization.access_qs(self.request.user, 'view')
|
||||
user_qs = models.Organization.objects.filter(member_role__members=parent)
|
||||
return my_qs & user_qs
|
||||
|
||||
@@ -1318,7 +1318,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView):
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
my_qs = models.Organization.accessible_objects(self.request.user, 'read_role')
|
||||
my_qs = models.Organization.access_qs(self.request.user, 'view')
|
||||
user_qs = models.Organization.objects.filter(admin_role__members=parent)
|
||||
return my_qs & user_qs
|
||||
|
||||
@@ -1495,8 +1495,8 @@ class UserCredentialsList(SubListCreateAPIView):
|
||||
user = self.get_parent_object()
|
||||
self.check_parent_access(user)
|
||||
|
||||
visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role')
|
||||
user_creds = models.Credential.accessible_objects(user, 'read_role')
|
||||
visible_creds = models.Credential.access_qs(self.request.user, 'view')
|
||||
user_creds = models.Credential.access_qs(user, 'view')
|
||||
return user_creds & visible_creds
|
||||
|
||||
|
||||
@@ -1511,7 +1511,7 @@ class TeamCredentialsList(SubListCreateAPIView):
|
||||
team = self.get_parent_object()
|
||||
self.check_parent_access(team)
|
||||
|
||||
visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role')
|
||||
visible_creds = models.Credential.access_qs(self.request.user, 'view')
|
||||
team_creds = models.Credential.objects.filter(Q(use_role__parents=team.member_role) | Q(admin_role__parents=team.member_role))
|
||||
return (team_creds & visible_creds).distinct()
|
||||
|
||||
@@ -1527,7 +1527,7 @@ class OrganizationCredentialList(SubListCreateAPIView):
|
||||
organization = self.get_parent_object()
|
||||
self.check_parent_access(organization)
|
||||
|
||||
user_visible = models.Credential.accessible_objects(self.request.user, 'read_role').all()
|
||||
user_visible = models.Credential.access_qs(self.request.user, 'view').all()
|
||||
org_set = models.Credential.objects.filter(organization=organization)
|
||||
|
||||
if self.request.user.is_superuser or self.request.user.is_system_auditor:
|
||||
|
||||
@@ -157,23 +157,21 @@ class OrganizationCountsMixin(object):
|
||||
return full_context
|
||||
|
||||
db_results = {}
|
||||
org_qs = self.model.accessible_objects(self.request.user, 'read_role')
|
||||
org_qs = self.model.access_qs(self.request.user, 'view')
|
||||
org_id_list = org_qs.values('id')
|
||||
if len(org_id_list) == 0:
|
||||
if self.request.method == 'POST':
|
||||
full_context['related_field_counts'] = {}
|
||||
return full_context
|
||||
|
||||
inv_qs = Inventory.accessible_objects(self.request.user, 'read_role')
|
||||
project_qs = Project.accessible_objects(self.request.user, 'read_role')
|
||||
jt_qs = JobTemplate.accessible_objects(self.request.user, 'read_role')
|
||||
inv_qs = Inventory.access_qs(self.request.user, 'view')
|
||||
project_qs = Project.access_qs(self.request.user, 'view')
|
||||
jt_qs = JobTemplate.access_qs(self.request.user, 'view')
|
||||
|
||||
# Produce counts of Foreign Key relationships
|
||||
db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
||||
|
||||
db_results['teams'] = (
|
||||
Team.accessible_objects(self.request.user, 'read_role').values('organization').annotate(Count('organization')).order_by('organization')
|
||||
)
|
||||
db_results['teams'] = Team.access_qs(self.request.user, 'view').values('organization').annotate(Count('organization')).order_by('organization')
|
||||
|
||||
db_results['job_templates'] = jt_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
|
||||
org_id = int(self.kwargs['pk'])
|
||||
|
||||
org_counts = {}
|
||||
access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'}
|
||||
member_rd = RoleDefinition.objects.filter(name='Organization Member').first()
|
||||
admin_rd = RoleDefinition.objects.filter(name='Organization Admin').first()
|
||||
|
||||
@@ -93,10 +92,10 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
|
||||
else:
|
||||
org_counts.update({'users': 0, 'admins': 0})
|
||||
|
||||
org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||
org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
||||
org_counts['inventories'] = Inventory.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||
org_counts['teams'] = Team.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||
org_counts['projects'] = Project.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||
org_counts['job_templates'] = JobTemplate.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||
org_counts['hosts'] = Host.objects.org_active_count(org_id)
|
||||
|
||||
full_context['related_field_counts'] = {}
|
||||
|
||||
@@ -369,7 +369,7 @@ class ApiV2ConfigView(APIView):
|
||||
)
|
||||
else:
|
||||
# Only check JobTemplate access if org check failed
|
||||
if JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
||||
if JobTemplate.access_qs(request.user, 'change').exists():
|
||||
data['custom_virtualenvs'] = get_custom_venv_choices()
|
||||
|
||||
return Response(data)
|
||||
|
||||
+31
-29
@@ -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__():
|
||||
|
||||
@@ -75,7 +75,6 @@ from awx.main.models.rbac import ( # noqa
|
||||
from awx.main.models.mixins import ( # noqa
|
||||
CustomVirtualEnvMixin,
|
||||
ExecutionEnvironmentMixin,
|
||||
ResourceMixin,
|
||||
SurveyJobMixin,
|
||||
SurveyJobTemplateMixin,
|
||||
TaskManagerInventoryUpdateMixin,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user