mirror of
https://github.com/ansible/awx.git
synced 2026-10-31 14:36:17 -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_update_failed = serializers.BooleanField(read_only=True)
|
||||||
last_updated = serializers.DateTimeField(read_only=True)
|
last_updated = serializers.DateTimeField(read_only=True)
|
||||||
show_capabilities = ['start', 'schedule', 'edit', 'delete', 'copy']
|
show_capabilities = ['start', 'schedule', 'edit', 'delete', 'copy']
|
||||||
capabilities_prefetch = ['admin', 'update', {'copy': 'organization.project_admin'}]
|
capabilities_prefetch = [{'edit': 'change'}, {'start': 'update'}, {'copy': 'organization.add_project'}]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
@@ -1555,7 +1555,7 @@ class LabelsListMixin(object):
|
|||||||
|
|
||||||
class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin):
|
class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin):
|
||||||
show_capabilities = ['edit', 'delete', 'adhoc', 'copy']
|
show_capabilities = ['edit', 'delete', 'adhoc', 'copy']
|
||||||
capabilities_prefetch = ['admin', 'adhoc', {'copy': 'organization.inventory_admin'}]
|
capabilities_prefetch = [{'edit': 'change'}, {'adhoc': 'adhoc'}, {'copy': 'organization.add_inventory'}]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Inventory
|
model = Inventory
|
||||||
@@ -1799,7 +1799,7 @@ class InventoryScriptSerializer(InventorySerializer):
|
|||||||
|
|
||||||
class HostSerializer(BaseSerializerWithVariables):
|
class HostSerializer(BaseSerializerWithVariables):
|
||||||
show_capabilities = ['edit', 'delete']
|
show_capabilities = ['edit', 'delete']
|
||||||
capabilities_prefetch = ['inventory.admin']
|
capabilities_prefetch = [{'edit': 'inventory.change'}]
|
||||||
|
|
||||||
has_active_failures = serializers.SerializerMethodField()
|
has_active_failures = serializers.SerializerMethodField()
|
||||||
has_inventory_sources = serializers.SerializerMethodField()
|
has_inventory_sources = serializers.SerializerMethodField()
|
||||||
@@ -1978,7 +1978,7 @@ class AnsibleFactsSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class GroupSerializer(BaseSerializerWithVariables):
|
class GroupSerializer(BaseSerializerWithVariables):
|
||||||
show_capabilities = ['copy', 'edit', 'delete']
|
show_capabilities = ['copy', 'edit', 'delete']
|
||||||
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc']
|
capabilities_prefetch = [{'edit': 'inventory.change'}, {'adhoc': 'inventory.adhoc'}]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Group
|
model = Group
|
||||||
@@ -2376,7 +2376,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
last_update_failed = serializers.BooleanField(read_only=True)
|
last_update_failed = serializers.BooleanField(read_only=True)
|
||||||
last_updated = serializers.DateTimeField(read_only=True)
|
last_updated = serializers.DateTimeField(read_only=True)
|
||||||
show_capabilities = ['start', 'schedule', 'edit', 'delete']
|
show_capabilities = ['start', 'schedule', 'edit', 'delete']
|
||||||
capabilities_prefetch = [{'admin': 'inventory.admin'}, {'start': 'inventory.update'}]
|
capabilities_prefetch = [{'edit': 'inventory.change'}, {'start': 'inventory.update'}]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventorySource
|
model = InventorySource
|
||||||
@@ -2995,7 +2995,7 @@ class CredentialTypeSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class CredentialSerializer(BaseSerializer):
|
class CredentialSerializer(BaseSerializer):
|
||||||
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
||||||
capabilities_prefetch = ['admin', 'use']
|
capabilities_prefetch = [{'edit': 'change'}, {'use': 'use'}]
|
||||||
managed = serializers.ReadOnlyField()
|
managed = serializers.ReadOnlyField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -3336,7 +3336,7 @@ class JobTemplateMixin(object):
|
|||||||
|
|
||||||
class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
||||||
show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete']
|
show_capabilities = ['start', 'schedule', 'copy', 'edit', 'delete']
|
||||||
capabilities_prefetch = ['admin', 'execute', {'copy': ['project.use', 'inventory.use']}]
|
capabilities_prefetch = [{'edit': 'change'}, {'start': 'execute'}, {'copy': ['project.use', 'inventory.use']}]
|
||||||
|
|
||||||
status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES, read_only=True, required=False)
|
status = serializers.ChoiceField(choices=JobTemplate.JOB_TEMPLATE_STATUS_CHOICES, read_only=True, required=False)
|
||||||
|
|
||||||
@@ -3825,7 +3825,7 @@ class SystemJobCancelSerializer(SystemJobSerializer):
|
|||||||
|
|
||||||
class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJobTemplateSerializer):
|
class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJobTemplateSerializer):
|
||||||
show_capabilities = ['start', 'schedule', 'edit', 'copy', 'delete']
|
show_capabilities = ['start', 'schedule', 'edit', 'copy', 'delete']
|
||||||
capabilities_prefetch = ['admin', 'execute', {'copy': 'organization.workflow_admin'}]
|
capabilities_prefetch = [{'edit': 'change'}, {'start': 'execute'}, {'copy': 'organization.add_workflowjobtemplate'}]
|
||||||
limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
|
limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
|
||||||
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
|
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
|
||||||
|
|
||||||
@@ -4987,17 +4987,17 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
|||||||
raise serializers.ValidationError(_("Template types {type_names} not allowed in bulk jobs").format(type_names=type_names))
|
raise serializers.ValidationError(_("Template types {type_names} not allowed in bulk jobs").format(type_names=type_names))
|
||||||
|
|
||||||
for model, obj_list in ujts.items():
|
for model, obj_list in ujts.items():
|
||||||
role_field = 'execute_role' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update_role'
|
action = 'execute' if issubclass(model, (JobTemplate, WorkflowJobTemplate)) else 'update'
|
||||||
self.check_list_permission(model, set([obj.id for obj in obj_list]), role_field)
|
self.check_list_permission(model, set([obj.id for obj in obj_list]), action)
|
||||||
|
|
||||||
self.check_organization_permission(attrs, request)
|
self.check_organization_permission(attrs, request)
|
||||||
|
|
||||||
if 'inventory' in attrs:
|
if 'inventory' in attrs:
|
||||||
requested_use_inventories.add(attrs['inventory'].id)
|
requested_use_inventories.add(attrs['inventory'].id)
|
||||||
|
|
||||||
self.check_list_permission(Inventory, requested_use_inventories, 'use_role')
|
self.check_list_permission(Inventory, requested_use_inventories, 'use')
|
||||||
|
|
||||||
self.check_list_permission(Credential, requested_use_credentials, 'use_role')
|
self.check_list_permission(Credential, requested_use_credentials, 'use')
|
||||||
self.check_list_permission(Label, requested_use_labels)
|
self.check_list_permission(Label, requested_use_labels)
|
||||||
self.check_list_permission(InstanceGroup, requested_use_instance_groups) # TODO: change to use_role for conflict
|
self.check_list_permission(InstanceGroup, requested_use_instance_groups) # TODO: change to use_role for conflict
|
||||||
self.check_list_permission(ExecutionEnvironment, requested_use_execution_environments) # TODO: change if roles introduced
|
self.check_list_permission(ExecutionEnvironment, requested_use_execution_environments) # TODO: change if roles introduced
|
||||||
@@ -5011,14 +5011,14 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
|||||||
attrs = super().validate(attrs)
|
attrs = super().validate(attrs)
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def check_list_permission(self, model, id_list, role_field=None):
|
def check_list_permission(self, model, id_list, action=None):
|
||||||
if not id_list:
|
if not id_list:
|
||||||
return
|
return
|
||||||
user = self.context['request'].user
|
user = self.context['request'].user
|
||||||
if role_field is None: # implies "read" level permission is required
|
if action is None: # implies "read" level permission is required
|
||||||
access_qs = user.get_queryset(model)
|
access_qs = user.get_queryset(model)
|
||||||
else:
|
else:
|
||||||
access_qs = model.accessible_objects(user, role_field)
|
access_qs = model.access_qs(user, action)
|
||||||
|
|
||||||
not_allowed = set(id_list) - set(access_qs.filter(id__in=id_list).values_list('id', flat=True))
|
not_allowed = set(id_list) - set(access_qs.filter(id__in=id_list).values_list('id', flat=True))
|
||||||
if not_allowed:
|
if not_allowed:
|
||||||
@@ -5115,7 +5115,7 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
|||||||
# - If the orgs is not set, set it to the org of the launching user
|
# - If the orgs is not set, set it to the org of the launching user
|
||||||
# - If the user is part of multiple orgs, throw a validation error saying user is part of multiple orgs, please provide one
|
# - If the user is part of multiple orgs, throw a validation error saying user is part of multiple orgs, please provide one
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
read_org_qs = Organization.accessible_objects(request.user, 'member_role')
|
read_org_qs = Organization.access_qs(request.user, 'member')
|
||||||
if 'organization' not in attrs or attrs['organization'] == None or attrs['organization'] == '':
|
if 'organization' not in attrs or attrs['organization'] == None or attrs['organization'] == '':
|
||||||
read_org_ct = read_org_qs.count()
|
read_org_ct = read_org_qs.count()
|
||||||
if read_org_ct == 1:
|
if read_org_ct == 1:
|
||||||
@@ -5149,7 +5149,7 @@ class BulkJobLaunchSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
class NotificationTemplateSerializer(BaseSerializer):
|
class NotificationTemplateSerializer(BaseSerializer):
|
||||||
show_capabilities = ['edit', 'delete', 'copy']
|
show_capabilities = ['edit', 'delete', 'copy']
|
||||||
capabilities_prefetch = [{'copy': 'organization.admin'}]
|
capabilities_prefetch = [{'copy': 'organization.add_notificationtemplate'}]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = NotificationTemplate
|
model = NotificationTemplate
|
||||||
|
|||||||
+12
-12
@@ -835,8 +835,8 @@ class TeamProjectsList(SubListAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
team = self.get_parent_object()
|
team = self.get_parent_object()
|
||||||
self.check_parent_access(team)
|
self.check_parent_access(team)
|
||||||
my_qs = self.model.accessible_objects(self.request.user, 'read_role')
|
my_qs = self.model.access_qs(self.request.user, 'view')
|
||||||
team_qs = models.Project.accessible_objects(team, 'read_role')
|
team_qs = models.Project.access_qs(team, 'view')
|
||||||
return my_qs & team_qs
|
return my_qs & team_qs
|
||||||
|
|
||||||
|
|
||||||
@@ -1009,7 +1009,7 @@ class ProjectTeamsList(ListAPIView):
|
|||||||
team_member_parent_roles = models.Role.objects.filter(children__in=roles_on_project, role_field='member_role', content_type=team_ct).distinct()
|
team_member_parent_roles = models.Role.objects.filter(children__in=roles_on_project, role_field='member_role', content_type=team_ct).distinct()
|
||||||
|
|
||||||
team_ids = team_member_parent_roles.values_list('object_id', flat=True)
|
team_ids = team_member_parent_roles.values_list('object_id', flat=True)
|
||||||
my_qs = self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=team_ids)
|
my_qs = self.model.access_qs(self.request.user, 'view').filter(pk__in=team_ids)
|
||||||
return my_qs
|
return my_qs
|
||||||
|
|
||||||
|
|
||||||
@@ -1239,7 +1239,7 @@ class UserTeamsList(SubListAPIView):
|
|||||||
u = get_object_or_404(models.User, pk=self.kwargs['pk'])
|
u = get_object_or_404(models.User, pk=self.kwargs['pk'])
|
||||||
if not self.request.user.can_access(models.User, 'read', u):
|
if not self.request.user.can_access(models.User, 'read', u):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
return models.Team.accessible_objects(self.request.user, 'read_role').filter(Q(member_role__members=u) | Q(admin_role__members=u)).distinct()
|
return models.Team.access_qs(self.request.user, 'view').filter(Q(member_role__members=u) | Q(admin_role__members=u)).distinct()
|
||||||
|
|
||||||
|
|
||||||
class UserRolesList(SubListAttachDetachAPIView):
|
class UserRolesList(SubListAttachDetachAPIView):
|
||||||
@@ -1290,8 +1290,8 @@ class UserProjectsList(SubListAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
parent = self.get_parent_object()
|
parent = self.get_parent_object()
|
||||||
self.check_parent_access(parent)
|
self.check_parent_access(parent)
|
||||||
my_qs = models.Project.accessible_objects(self.request.user, 'read_role')
|
my_qs = models.Project.access_qs(self.request.user, 'view')
|
||||||
user_qs = models.Project.accessible_objects(parent, 'read_role')
|
user_qs = models.Project.access_qs(parent, 'view')
|
||||||
return my_qs & user_qs
|
return my_qs & user_qs
|
||||||
|
|
||||||
|
|
||||||
@@ -1304,7 +1304,7 @@ class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
parent = self.get_parent_object()
|
parent = self.get_parent_object()
|
||||||
self.check_parent_access(parent)
|
self.check_parent_access(parent)
|
||||||
my_qs = models.Organization.accessible_objects(self.request.user, 'read_role')
|
my_qs = models.Organization.access_qs(self.request.user, 'view')
|
||||||
user_qs = models.Organization.objects.filter(member_role__members=parent)
|
user_qs = models.Organization.objects.filter(member_role__members=parent)
|
||||||
return my_qs & user_qs
|
return my_qs & user_qs
|
||||||
|
|
||||||
@@ -1318,7 +1318,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
parent = self.get_parent_object()
|
parent = self.get_parent_object()
|
||||||
self.check_parent_access(parent)
|
self.check_parent_access(parent)
|
||||||
my_qs = models.Organization.accessible_objects(self.request.user, 'read_role')
|
my_qs = models.Organization.access_qs(self.request.user, 'view')
|
||||||
user_qs = models.Organization.objects.filter(admin_role__members=parent)
|
user_qs = models.Organization.objects.filter(admin_role__members=parent)
|
||||||
return my_qs & user_qs
|
return my_qs & user_qs
|
||||||
|
|
||||||
@@ -1495,8 +1495,8 @@ class UserCredentialsList(SubListCreateAPIView):
|
|||||||
user = self.get_parent_object()
|
user = self.get_parent_object()
|
||||||
self.check_parent_access(user)
|
self.check_parent_access(user)
|
||||||
|
|
||||||
visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role')
|
visible_creds = models.Credential.access_qs(self.request.user, 'view')
|
||||||
user_creds = models.Credential.accessible_objects(user, 'read_role')
|
user_creds = models.Credential.access_qs(user, 'view')
|
||||||
return user_creds & visible_creds
|
return user_creds & visible_creds
|
||||||
|
|
||||||
|
|
||||||
@@ -1511,7 +1511,7 @@ class TeamCredentialsList(SubListCreateAPIView):
|
|||||||
team = self.get_parent_object()
|
team = self.get_parent_object()
|
||||||
self.check_parent_access(team)
|
self.check_parent_access(team)
|
||||||
|
|
||||||
visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role')
|
visible_creds = models.Credential.access_qs(self.request.user, 'view')
|
||||||
team_creds = models.Credential.objects.filter(Q(use_role__parents=team.member_role) | Q(admin_role__parents=team.member_role))
|
team_creds = models.Credential.objects.filter(Q(use_role__parents=team.member_role) | Q(admin_role__parents=team.member_role))
|
||||||
return (team_creds & visible_creds).distinct()
|
return (team_creds & visible_creds).distinct()
|
||||||
|
|
||||||
@@ -1527,7 +1527,7 @@ class OrganizationCredentialList(SubListCreateAPIView):
|
|||||||
organization = self.get_parent_object()
|
organization = self.get_parent_object()
|
||||||
self.check_parent_access(organization)
|
self.check_parent_access(organization)
|
||||||
|
|
||||||
user_visible = models.Credential.accessible_objects(self.request.user, 'read_role').all()
|
user_visible = models.Credential.access_qs(self.request.user, 'view').all()
|
||||||
org_set = models.Credential.objects.filter(organization=organization)
|
org_set = models.Credential.objects.filter(organization=organization)
|
||||||
|
|
||||||
if self.request.user.is_superuser or self.request.user.is_system_auditor:
|
if self.request.user.is_superuser or self.request.user.is_system_auditor:
|
||||||
|
|||||||
@@ -157,23 +157,21 @@ class OrganizationCountsMixin(object):
|
|||||||
return full_context
|
return full_context
|
||||||
|
|
||||||
db_results = {}
|
db_results = {}
|
||||||
org_qs = self.model.accessible_objects(self.request.user, 'read_role')
|
org_qs = self.model.access_qs(self.request.user, 'view')
|
||||||
org_id_list = org_qs.values('id')
|
org_id_list = org_qs.values('id')
|
||||||
if len(org_id_list) == 0:
|
if len(org_id_list) == 0:
|
||||||
if self.request.method == 'POST':
|
if self.request.method == 'POST':
|
||||||
full_context['related_field_counts'] = {}
|
full_context['related_field_counts'] = {}
|
||||||
return full_context
|
return full_context
|
||||||
|
|
||||||
inv_qs = Inventory.accessible_objects(self.request.user, 'read_role')
|
inv_qs = Inventory.access_qs(self.request.user, 'view')
|
||||||
project_qs = Project.accessible_objects(self.request.user, 'read_role')
|
project_qs = Project.access_qs(self.request.user, 'view')
|
||||||
jt_qs = JobTemplate.accessible_objects(self.request.user, 'read_role')
|
jt_qs = JobTemplate.access_qs(self.request.user, 'view')
|
||||||
|
|
||||||
# Produce counts of Foreign Key relationships
|
# Produce counts of Foreign Key relationships
|
||||||
db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
db_results['inventories'] = inv_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
||||||
|
|
||||||
db_results['teams'] = (
|
db_results['teams'] = Team.access_qs(self.request.user, 'view').values('organization').annotate(Count('organization')).order_by('organization')
|
||||||
Team.accessible_objects(self.request.user, 'read_role').values('organization').annotate(Count('organization')).order_by('organization')
|
|
||||||
)
|
|
||||||
|
|
||||||
db_results['job_templates'] = jt_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
db_results['job_templates'] = jt_qs.values('organization').annotate(Count('organization')).order_by('organization')
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
|
|||||||
org_id = int(self.kwargs['pk'])
|
org_id = int(self.kwargs['pk'])
|
||||||
|
|
||||||
org_counts = {}
|
org_counts = {}
|
||||||
access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'}
|
|
||||||
member_rd = RoleDefinition.objects.filter(name='Organization Member').first()
|
member_rd = RoleDefinition.objects.filter(name='Organization Member').first()
|
||||||
admin_rd = RoleDefinition.objects.filter(name='Organization Admin').first()
|
admin_rd = RoleDefinition.objects.filter(name='Organization Admin').first()
|
||||||
|
|
||||||
@@ -93,10 +92,10 @@ class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPI
|
|||||||
else:
|
else:
|
||||||
org_counts.update({'users': 0, 'admins': 0})
|
org_counts.update({'users': 0, 'admins': 0})
|
||||||
|
|
||||||
org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
org_counts['inventories'] = Inventory.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||||
org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
org_counts['teams'] = Team.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||||
org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
org_counts['projects'] = Project.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||||
org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).filter(organization__id=org_id).count()
|
org_counts['job_templates'] = JobTemplate.access_qs(self.request.user, 'view').filter(organization__id=org_id).count()
|
||||||
org_counts['hosts'] = Host.objects.org_active_count(org_id)
|
org_counts['hosts'] = Host.objects.org_active_count(org_id)
|
||||||
|
|
||||||
full_context['related_field_counts'] = {}
|
full_context['related_field_counts'] = {}
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ class ApiV2ConfigView(APIView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Only check JobTemplate access if org check failed
|
# Only check JobTemplate access if org check failed
|
||||||
if JobTemplate.accessible_objects(request.user, 'admin_role').exists():
|
if JobTemplate.access_qs(request.user, 'change').exists():
|
||||||
data['custom_virtualenvs'] = get_custom_venv_choices()
|
data['custom_virtualenvs'] = get_custom_venv_choices()
|
||||||
|
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|||||||
+31
-29
@@ -74,7 +74,7 @@ from awx.main.models import (
|
|||||||
WorkflowApproval,
|
WorkflowApproval,
|
||||||
WorkflowApprovalTemplate,
|
WorkflowApprovalTemplate,
|
||||||
)
|
)
|
||||||
from awx.main.models.mixins import ResourceMixin
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'get_user_queryset',
|
'get_user_queryset',
|
||||||
@@ -692,7 +692,7 @@ class UserAccess(BaseAccess):
|
|||||||
returns True if `u` is member of any organization that is
|
returns True if `u` is member of any organization that is
|
||||||
not also an organization that `self.user` admins
|
not also an organization that `self.user` admins
|
||||||
"""
|
"""
|
||||||
return not self.user_organizations(u).exclude(pk__in=Organization.accessible_pk_qs(self.user, 'admin_role')).exists()
|
return not self.user_organizations(u).exclude(pk__in=Organization.access_ids_qs(self.user, 'change')).exists()
|
||||||
|
|
||||||
def user_is_orphaned(self, u):
|
def user_is_orphaned(self, u):
|
||||||
return not self.user_organizations(u).exists()
|
return not self.user_organizations(u).exists()
|
||||||
@@ -905,7 +905,7 @@ class HostAccess(BaseAccess):
|
|||||||
prefetch_related = ('groups', 'inventory_sources')
|
prefetch_related = ('groups', 'inventory_sources')
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
if not data: # So the browseable API will work
|
if not data: # So the browseable API will work
|
||||||
@@ -967,7 +967,7 @@ class GroupAccess(BaseAccess):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return Group.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return Group.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
if not data: # So the browseable API will work
|
if not data: # So the browseable API will work
|
||||||
@@ -1009,7 +1009,7 @@ class InventorySourceAccess(NotificationAttachMixin, UnifiedCredentialsMixin, Ba
|
|||||||
prefetch_related = ('credentials__credential_type', 'last_job', 'source_project')
|
prefetch_related = ('credentials__credential_type', 'last_job', 'source_project')
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
if not data or 'inventory' not in data:
|
if not data or 'inventory' not in data:
|
||||||
@@ -1059,7 +1059,7 @@ class InventoryUpdateAccess(BaseAccess):
|
|||||||
prefetch_related = ('unified_job_template', 'instance_group', 'credentials__credential_type', 'inventory')
|
prefetch_related = ('unified_job_template', 'instance_group', 'credentials__credential_type', 'inventory')
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(inventory_source__inventory__in=Inventory.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
def can_cancel(self, obj):
|
def can_cancel(self, obj):
|
||||||
if not obj.can_cancel:
|
if not obj.can_cancel:
|
||||||
@@ -1187,7 +1187,7 @@ class CredentialInputSourceAccess(BaseAccess):
|
|||||||
select_related = ('target_credential', 'source_credential')
|
select_related = ('target_credential', 'source_credential')
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return CredentialInputSource.objects.filter(target_credential__in=Credential.accessible_pk_qs(self.user, 'read_role'))
|
return CredentialInputSource.objects.filter(target_credential__in=Credential.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
@@ -1232,10 +1232,8 @@ class TeamAccess(BaseAccess):
|
|||||||
Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
|
Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
|
||||||
):
|
):
|
||||||
return self.model.objects.all()
|
return self.model.objects.all()
|
||||||
org_member_teams = (
|
org_member_teams = self.model.objects.filter(organization__in=Organization.access_ids_qs(self.user, 'member')).order_by().values_list('pk', flat=True)
|
||||||
self.model.objects.filter(organization__in=Organization.accessible_pk_qs(self.user, 'member_role')).order_by().values_list('pk', flat=True)
|
direct_read_teams = self.model.objects.filter(pk__in=self.model.access_ids_qs(self.user, 'view')).order_by().values_list('pk', flat=True)
|
||||||
)
|
|
||||||
direct_read_teams = self.model.objects.filter(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')).order_by().values_list('pk', flat=True)
|
|
||||||
return self.model.objects.filter(pk__in=org_member_teams.union(direct_read_teams))
|
return self.model.objects.filter(pk__in=org_member_teams.union(direct_read_teams))
|
||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
@@ -1268,7 +1266,7 @@ class TeamAccess(BaseAccess):
|
|||||||
if sub_obj.content_object is None:
|
if sub_obj.content_object is None:
|
||||||
raise PermissionDenied(_("The {} role cannot be assigned to a team").format(sub_obj.name))
|
raise PermissionDenied(_("The {} role cannot be assigned to a team").format(sub_obj.name))
|
||||||
|
|
||||||
if isinstance(sub_obj.content_object, ResourceMixin):
|
if permission_registry.is_registered(sub_obj.content_object):
|
||||||
role_access = RoleAccess(self.user)
|
role_access = RoleAccess(self.user)
|
||||||
return role_access.can_attach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
|
return role_access.can_attach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
|
||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
@@ -1284,7 +1282,10 @@ class TeamAccess(BaseAccess):
|
|||||||
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
|
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
|
||||||
# MANAGE_ORGANIZATION_AUTH setting checked in RoleAccess
|
# MANAGE_ORGANIZATION_AUTH setting checked in RoleAccess
|
||||||
if isinstance(sub_obj, Role):
|
if isinstance(sub_obj, Role):
|
||||||
if isinstance(sub_obj.content_object, ResourceMixin):
|
if sub_obj.content_object is None:
|
||||||
|
raise PermissionDenied(_("The {} role cannot be unassigned from a team").format(sub_obj.name))
|
||||||
|
|
||||||
|
if permission_registry.is_registered(sub_obj.content_object):
|
||||||
role_access = RoleAccess(self.user)
|
role_access = RoleAccess(self.user)
|
||||||
return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
|
return role_access.can_unattach(sub_obj, obj, 'member_role.parents', *args, **kwargs)
|
||||||
|
|
||||||
@@ -1423,7 +1424,7 @@ class ProjectUpdateAccess(BaseAccess):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(project__in=Project.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(project__in=Project.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
def can_cancel(self, obj):
|
def can_cancel(self, obj):
|
||||||
@@ -1817,7 +1818,8 @@ class JobLaunchConfigAccess(UnifiedCredentialsMixin, BaseAccess):
|
|||||||
if cls is Label:
|
if cls is Label:
|
||||||
return LabelAccess(self.user).filtered_queryset()
|
return LabelAccess(self.user).filtered_queryset()
|
||||||
else:
|
else:
|
||||||
return cls._accessible_pk_qs(cls, self.user, 'use_role')
|
action = f'use_{cls._meta.model_name}'
|
||||||
|
return cls.access_qs(self.user, action)
|
||||||
|
|
||||||
def has_obj_m2m_access(self, obj):
|
def has_obj_m2m_access(self, obj):
|
||||||
for relationship, cls in self.M2M_CHECKS.items():
|
for relationship, cls in self.M2M_CHECKS.items():
|
||||||
@@ -1961,7 +1963,7 @@ class WorkflowJobNodeAccess(BaseAccess):
|
|||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(
|
return self.model.objects.filter(
|
||||||
Q(workflow_job__unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
|
Q(workflow_job__unified_job_template__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view'))
|
||||||
| Q(workflow_job__organization__in=Organization.objects.filter(Q(admin_role__members=self.user)))
|
| Q(workflow_job__organization__in=Organization.objects.filter(Q(admin_role__members=self.user)))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2105,8 +2107,7 @@ class WorkflowJobAccess(BaseAccess):
|
|||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return WorkflowJob.objects.filter(
|
return WorkflowJob.objects.filter(
|
||||||
Q(unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
|
Q(unified_job_template__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view')) | Q(organization__in=Organization.access_ids_qs(self.user, 'audit'))
|
||||||
| Q(organization__in=Organization.accessible_pk_qs(self.user, 'auditor_role'))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def can_read(self, obj):
|
def can_read(self, obj):
|
||||||
@@ -2211,7 +2212,7 @@ class AdHocCommandAccess(BaseAccess):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
def can_add(self, data, validate_license=True):
|
def can_add(self, data, validate_license=True):
|
||||||
if not data: # So the browseable API will work
|
if not data: # So the browseable API will work
|
||||||
@@ -2352,7 +2353,7 @@ class ProjectUpdateEventAccess(BaseAccess):
|
|||||||
model = ProjectUpdateEvent
|
model = ProjectUpdateEvent
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(Q(project_update__project__in=Project.accessible_pk_qs(self.user, 'read_role')))
|
return self.model.objects.filter(Q(project_update__project__in=Project.access_ids_qs(self.user, 'view')))
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
return False
|
return False
|
||||||
@@ -2372,7 +2373,7 @@ class InventoryUpdateEventAccess(BaseAccess):
|
|||||||
model = InventoryUpdateEvent
|
model = InventoryUpdateEvent
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(Q(inventory_update__inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')))
|
return self.model.objects.filter(Q(inventory_update__inventory_source__inventory__in=Inventory.access_ids_qs(self.user, 'view')))
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
return False
|
return False
|
||||||
@@ -2392,7 +2393,8 @@ class ReceptorAddressAccess(BaseAccess):
|
|||||||
model = ReceptorAddress
|
model = ReceptorAddress
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(Q(instance__in=Instance.accessible_pk_qs(self.user, 'read_role')))
|
instance_qs = InstanceAccess(self.user).filtered_queryset()
|
||||||
|
return self.model.objects.filter(Q(instance__in=instance_qs))
|
||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
@@ -2456,7 +2458,7 @@ class UnifiedJobTemplateAccess(BaseAccess):
|
|||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(
|
return self.model.objects.filter(
|
||||||
Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role'))
|
Q(pk__in=self.model.access_ids_qs(self.user, 'view'))
|
||||||
| Q(
|
| Q(
|
||||||
pk__in=InventorySource.objects.filter(
|
pk__in=InventorySource.objects.filter(
|
||||||
inventory__id__in=Inventory.access_ids_qs(self.user, 'view'),
|
inventory__id__in=Inventory.access_ids_qs(self.user, 'view'),
|
||||||
@@ -2509,7 +2511,7 @@ class UnifiedJobAccess(BaseAccess):
|
|||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
inv_pk_qs = Inventory.access_ids_qs(self.user, 'view')
|
inv_pk_qs = Inventory.access_ids_qs(self.user, 'view')
|
||||||
return self.model.objects.filter(
|
return self.model.objects.filter(
|
||||||
Q(unified_job_template_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
|
Q(unified_job_template_id__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view'))
|
||||||
| Q(
|
| Q(
|
||||||
pk__in=InventoryUpdate.objects.filter(
|
pk__in=InventoryUpdate.objects.filter(
|
||||||
inventory_source__inventory__id__in=inv_pk_qs,
|
inventory_source__inventory__id__in=inv_pk_qs,
|
||||||
@@ -2520,7 +2522,7 @@ class UnifiedJobAccess(BaseAccess):
|
|||||||
inventory__id__in=inv_pk_qs,
|
inventory__id__in=inv_pk_qs,
|
||||||
).values('pk')
|
).values('pk')
|
||||||
)
|
)
|
||||||
| Q(organization__in=Organization.access_ids_qs(self.user, 'audit_organization'))
|
| Q(organization__in=Organization.access_ids_qs(self.user, 'audit'))
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -2642,7 +2644,7 @@ class LabelAccess(BaseAccess):
|
|||||||
Q(organization__in=Organization.access_ids_qs(self.user, 'view'))
|
Q(organization__in=Organization.access_ids_qs(self.user, 'view'))
|
||||||
| Q(
|
| Q(
|
||||||
pk__in=UnifiedJobTemplate.labels.through.objects.filter(
|
pk__in=UnifiedJobTemplate.labels.through.objects.filter(
|
||||||
unifiedjobtemplate_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'),
|
unifiedjobtemplate_id__in=UnifiedJobTemplate.access_ids_qs(self.user, 'view'),
|
||||||
).values('label_id')
|
).values('label_id')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -2851,7 +2853,7 @@ class RoleAccess(BaseAccess):
|
|||||||
if not settings.MANAGE_ORGANIZATION_AUTH and not self.user.is_superuser:
|
if not settings.MANAGE_ORGANIZATION_AUTH and not self.user.is_superuser:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if isinstance(obj.content_object, ResourceMixin) and self.user in obj.content_object.admin_role:
|
if obj.content_object is not None and permission_registry.is_registered(obj.content_object) and self.user in obj.content_object.admin_role:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -2888,7 +2890,7 @@ class WorkflowApprovalAccess(BaseAccess):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
def can_approve_or_deny(self, obj):
|
def can_approve_or_deny(self, obj):
|
||||||
if (obj.workflow_job_template and self.user in obj.workflow_job_template.approval_role) or self.user.is_superuser:
|
if (obj.workflow_job_template and self.user in obj.workflow_job_template.approval_role) or self.user.is_superuser:
|
||||||
@@ -2934,7 +2936,7 @@ class WorkflowApprovalTemplateAccess(BaseAccess):
|
|||||||
return self.user in obj.workflow_job_template.execute_role
|
return self.user in obj.workflow_job_template.execute_role
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.objects.filter(workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.accessible_pk_qs(self.user, 'read_role'))
|
return self.model.objects.filter(workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.access_ids_qs(self.user, 'view'))
|
||||||
|
|
||||||
|
|
||||||
for cls in BaseAccess.__subclasses__():
|
for cls in BaseAccess.__subclasses__():
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ from awx.main.models.rbac import ( # noqa
|
|||||||
from awx.main.models.mixins import ( # noqa
|
from awx.main.models.mixins import ( # noqa
|
||||||
CustomVirtualEnvMixin,
|
CustomVirtualEnvMixin,
|
||||||
ExecutionEnvironmentMixin,
|
ExecutionEnvironmentMixin,
|
||||||
ResourceMixin,
|
|
||||||
SurveyJobMixin,
|
SurveyJobMixin,
|
||||||
SurveyJobTemplateMixin,
|
SurveyJobTemplateMixin,
|
||||||
TaskManagerInventoryUpdateMixin,
|
TaskManagerInventoryUpdateMixin,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ from awx.main.fields import (
|
|||||||
from awx.main.utils import decrypt_field, classproperty, set_environ
|
from awx.main.utils import decrypt_field, classproperty, set_environ
|
||||||
from awx.main.validators import validate_ssh_private_key
|
from awx.main.validators import validate_ssh_private_key
|
||||||
from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, PrimordialModel
|
from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel, PrimordialModel
|
||||||
from awx.main.models.mixins import ResourceMixin
|
|
||||||
from awx.main.models.rbac import (
|
from awx.main.models.rbac import (
|
||||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||||
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||||
@@ -77,7 +76,7 @@ def build_safe_env(env):
|
|||||||
return safe_env
|
return safe_env
|
||||||
|
|
||||||
|
|
||||||
class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
class Credential(PasswordFieldsModel, CommonModelNameNotUnique):
|
||||||
"""
|
"""
|
||||||
A credential contains information about how to talk to a remote resource
|
A credential contains information about how to talk to a remote resource
|
||||||
Usually this is a SSH key location, and possibly an unlock password.
|
Usually this is a SSH key location, and possibly an unlock password.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from awx.main.models.rbac import (
|
|||||||
from awx.main.models.unified_jobs import UnifiedJob
|
from awx.main.models.unified_jobs import UnifiedJob
|
||||||
from awx.main.utils.common import get_corrected_cpu, get_cpu_effective_capacity, get_corrected_memory, get_mem_effective_capacity
|
from awx.main.utils.common import get_corrected_cpu, get_cpu_effective_capacity, get_corrected_memory, get_mem_effective_capacity
|
||||||
from awx.main.utils.redis import get_redis_client
|
from awx.main.utils.redis import get_redis_client
|
||||||
from awx.main.models.mixins import RelatedJobsMixin, ResourceMixin
|
from awx.main.models.mixins import RelatedJobsMixin
|
||||||
from awx.main.models.receptor_address import ReceptorAddress
|
from awx.main.models.receptor_address import ReceptorAddress
|
||||||
|
|
||||||
# ansible-runner
|
# ansible-runner
|
||||||
@@ -409,7 +409,7 @@ class Instance(HasPolicyEditsMixin, BaseModel):
|
|||||||
self.save_health_data(awx_application_version, get_cpu_count(), get_mem_in_bytes(), update_last_seen=True, errors=errors)
|
self.save_health_data(awx_application_version, get_cpu_count(), get_mem_in_bytes(), update_last_seen=True, errors=errors)
|
||||||
|
|
||||||
|
|
||||||
class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin, ResourceMixin):
|
class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
|
||||||
"""A model representing a Queue/Group of AWX Instances."""
|
"""A model representing a Queue/Group of AWX Instances."""
|
||||||
|
|
||||||
name = models.CharField(max_length=250, unique=True)
|
name = models.CharField(max_length=250, unique=True)
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ from awx.main.models.base import BaseModel, CommonModelNameNotUnique, VarsDictPr
|
|||||||
from awx.main.models.events import InventoryUpdateEvent, UnpartitionedInventoryUpdateEvent
|
from awx.main.models.events import InventoryUpdateEvent, UnpartitionedInventoryUpdateEvent
|
||||||
from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate
|
from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate
|
||||||
from awx.main.models.mixins import (
|
from awx.main.models.mixins import (
|
||||||
ResourceMixin,
|
|
||||||
TaskManagerInventoryUpdateMixin,
|
TaskManagerInventoryUpdateMixin,
|
||||||
RelatedJobsMixin,
|
RelatedJobsMixin,
|
||||||
CustomVirtualEnvMixin,
|
CustomVirtualEnvMixin,
|
||||||
@@ -71,7 +70,7 @@ class InventoryConstructedInventoryMembership(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin, OpaQueryPathMixin):
|
class Inventory(CommonModelNameNotUnique, RelatedJobsMixin, OpaQueryPathMixin):
|
||||||
"""
|
"""
|
||||||
an inventory source contains lists and hosts.
|
an inventory source contains lists and hosts.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ from awx.main.models.notifications import (
|
|||||||
from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPseudoField, polymorphic
|
from awx.main.utils import parse_yaml_or_json, getattr_dne, NullablePromptPseudoField, polymorphic
|
||||||
from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField
|
from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob, OrderedManyToManyField
|
||||||
from awx.main.models.mixins import (
|
from awx.main.models.mixins import (
|
||||||
ResourceMixin,
|
|
||||||
SurveyJobTemplateMixin,
|
SurveyJobTemplateMixin,
|
||||||
SurveyJobMixin,
|
SurveyJobMixin,
|
||||||
TaskManagerJobMixin,
|
TaskManagerJobMixin,
|
||||||
@@ -190,9 +189,7 @@ class JobOptions(BaseModel):
|
|||||||
return needed
|
return needed
|
||||||
|
|
||||||
|
|
||||||
class JobTemplate(
|
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin):
|
||||||
UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, WebhookTemplateMixin, OpaQueryPathMixin
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
A job template is a reusable job definition for applying a project (with
|
A job template is a reusable job definition for applying a project (with
|
||||||
playbook) to an inventory source with a given credential.
|
playbook) to an inventory source with a given credential.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import requests
|
|||||||
# Django
|
# Django
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
@@ -20,19 +20,17 @@ from ansible_base.lib.utils.models import prevent_search
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
|
|
||||||
from awx.main.models.rbac import Role, RoleAncestorEntry, to_permissions
|
|
||||||
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic
|
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic
|
||||||
from awx.main.utils.execution_environments import get_default_execution_environment
|
from awx.main.utils.execution_environments import get_default_execution_environment
|
||||||
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
|
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
|
||||||
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
|
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
|
||||||
from awx.main.fields import AskForField
|
from awx.main.fields import AskForField
|
||||||
from awx.main.constants import ACTIVE_STATES, org_role_to_permission
|
from awx.main.constants import ACTIVE_STATES
|
||||||
|
|
||||||
logger = logging.getLogger('awx.main.models.mixins')
|
logger = logging.getLogger('awx.main.models.mixins')
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ResourceMixin',
|
|
||||||
'SurveyJobTemplateMixin',
|
'SurveyJobTemplateMixin',
|
||||||
'SurveyJobMixin',
|
'SurveyJobMixin',
|
||||||
'TaskManagerUnifiedJobMixin',
|
'TaskManagerUnifiedJobMixin',
|
||||||
@@ -45,57 +43,6 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ResourceMixin(models.Model):
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def accessible_objects(cls, accessor, role_field):
|
|
||||||
"""
|
|
||||||
Use instead of `MyModel.objects` when you want to only consider
|
|
||||||
resources that a user has specific permissions for. For example:
|
|
||||||
MyModel.accessible_objects(user, 'read_role').filter(name__istartswith='bar');
|
|
||||||
NOTE: This should only be used for list type things.
|
|
||||||
"""
|
|
||||||
return ResourceMixin._accessible_objects(cls, accessor, role_field)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def accessible_pk_qs(cls, accessor, role_field):
|
|
||||||
return ResourceMixin._accessible_pk_qs(cls, accessor, role_field)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _accessible_pk_qs(cls, accessor, role_field, content_types=None):
|
|
||||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
|
||||||
if cls._meta.model_name == 'organization' and role_field in org_role_to_permission:
|
|
||||||
# Organization roles can not use the DAB RBAC shortcuts
|
|
||||||
# like Organization.access_qs(user, 'change_jobtemplate') is needed
|
|
||||||
# not just Organization.access_qs(user, 'change') is needed
|
|
||||||
if accessor.is_superuser:
|
|
||||||
return cls.objects.values_list('id')
|
|
||||||
|
|
||||||
codename = org_role_to_permission[role_field]
|
|
||||||
|
|
||||||
return cls.access_ids_qs(accessor, codename, content_types=content_types)
|
|
||||||
return cls.access_ids_qs(accessor, to_permissions[role_field], content_types=content_types)
|
|
||||||
if accessor._meta.model_name == 'user':
|
|
||||||
ancestor_roles = accessor.roles.all()
|
|
||||||
elif type(accessor) == Role:
|
|
||||||
ancestor_roles = [accessor]
|
|
||||||
else:
|
|
||||||
raise RuntimeError(f'Role filters only valid for users and ancestor role, received {accessor}')
|
|
||||||
|
|
||||||
if content_types is None:
|
|
||||||
ct_kwarg = dict(content_type=ContentType.objects.get_for_model(cls))
|
|
||||||
else:
|
|
||||||
ct_kwarg = dict(content_type_id__in=content_types)
|
|
||||||
|
|
||||||
return RoleAncestorEntry.objects.filter(ancestor__in=ancestor_roles, role_field=role_field, **ct_kwarg).values_list('object_id').distinct()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _accessible_objects(cls, accessor, role_field):
|
|
||||||
return cls.objects.filter(pk__in=ResourceMixin._accessible_pk_qs(cls, accessor, role_field))
|
|
||||||
|
|
||||||
|
|
||||||
class SurveyJobTemplateMixin(models.Model):
|
class SurveyJobTemplateMixin(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ from awx.main.models.rbac import (
|
|||||||
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||||
)
|
)
|
||||||
from awx.main.models.unified_jobs import UnifiedJob
|
from awx.main.models.unified_jobs import UnifiedJob
|
||||||
from awx.main.models.mixins import ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin
|
from awx.main.models.mixins import CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin
|
||||||
|
|
||||||
__all__ = ['Organization', 'Team', 'UserSessionMembership']
|
__all__ = ['Organization', 'Team', 'UserSessionMembership']
|
||||||
|
|
||||||
|
|
||||||
class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin):
|
class Organization(CommonModel, NotificationFieldsModel, CustomVirtualEnvMixin, RelatedJobsMixin, OpaQueryPathMixin):
|
||||||
"""
|
"""
|
||||||
An organization is the basic unit of multi-tenancy divisions
|
An organization is the basic unit of multi-tenancy divisions
|
||||||
"""
|
"""
|
||||||
@@ -134,7 +134,7 @@ class OrganizationGalaxyCredentialMembership(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Team(CommonModelNameNotUnique, ResourceMixin):
|
class Team(CommonModelNameNotUnique):
|
||||||
"""
|
"""
|
||||||
A team is a group of users that work on common projects.
|
A team is a group of users that work on common projects.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from awx.main.models.unified_jobs import (
|
|||||||
UnifiedJobTemplate,
|
UnifiedJobTemplate,
|
||||||
)
|
)
|
||||||
from awx.main.models.jobs import Job
|
from awx.main.models.jobs import Job
|
||||||
from awx.main.models.mixins import ResourceMixin, TaskManagerProjectUpdateMixin, CustomVirtualEnvMixin, RelatedJobsMixin
|
from awx.main.models.mixins import TaskManagerProjectUpdateMixin, CustomVirtualEnvMixin, RelatedJobsMixin
|
||||||
from awx.main.utils import update_scm_url, polymorphic
|
from awx.main.utils import update_scm_url, polymorphic
|
||||||
from awx.main.utils.ansible import skip_directory, could_be_inventory, could_be_playbook
|
from awx.main.utils.ansible import skip_directory, could_be_inventory, could_be_playbook
|
||||||
from awx.main.utils.execution_environments import get_control_plane_execution_environment
|
from awx.main.utils.execution_environments import get_control_plane_execution_environment
|
||||||
@@ -249,7 +249,7 @@ class ProjectOptions(models.Model):
|
|||||||
return proj_path + '.lock'
|
return proj_path + '.lock'
|
||||||
|
|
||||||
|
|
||||||
class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin):
|
class Project(UnifiedJobTemplate, ProjectOptions, CustomVirtualEnvMixin, RelatedJobsMixin):
|
||||||
"""
|
"""
|
||||||
A project represents a playbook git repo that can access a set of inventories
|
A project represents a playbook git repo that can access a set of inventories
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel,
|
|||||||
from awx.main.dispatch import get_task_queuename
|
from awx.main.dispatch import get_task_queuename
|
||||||
from awx.main.registrar import activity_stream_registrar
|
from awx.main.registrar import activity_stream_registrar
|
||||||
from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
|
from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
|
||||||
from awx.main.models.rbac import to_permissions
|
|
||||||
from awx.main.utils.common import (
|
from awx.main.utils.common import (
|
||||||
camelcase_to_underscore,
|
camelcase_to_underscore,
|
||||||
get_model_for_type,
|
get_model_for_type,
|
||||||
@@ -214,21 +213,18 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn
|
|||||||
return [c for c in cls.__subclasses__() if permission_registry.is_registered(c)]
|
return [c for c in cls.__subclasses__() if permission_registry.is_registered(c)]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accessible_pk_qs(cls, accessor, role_field):
|
def access_ids_qs(cls, accessor, action):
|
||||||
"""
|
"""
|
||||||
A re-implementation of accessible pk queryset for the "normal" unified JTs.
|
Returns a queryset of IDs for UnifiedJobTemplates accessible to the user.
|
||||||
Does not return inventory sources or system JTs, these should
|
Handles the polymorphic nature by checking permissions across all submodels.
|
||||||
be handled inside of get_queryset where it is utilized.
|
|
||||||
"""
|
"""
|
||||||
# do not use this if in a subclass
|
# do not use this if in a subclass
|
||||||
if cls != UnifiedJobTemplate:
|
if cls != UnifiedJobTemplate:
|
||||||
return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field)
|
return super(UnifiedJobTemplate, cls).access_ids_qs(accessor, action)
|
||||||
|
|
||||||
action = to_permissions[role_field]
|
|
||||||
|
|
||||||
# Special condition for super auditor
|
# Special condition for super auditor
|
||||||
role_subclasses = cls._submodels_with_roles()
|
role_subclasses = cls._submodels_with_roles()
|
||||||
all_codenames = {f'{action}_{cls._meta.model_name}' for cls in role_subclasses}
|
all_codenames = {f'{action}_{subcls._meta.model_name}' for subcls in role_subclasses}
|
||||||
if not (all_codenames - accessor.singleton_permissions()):
|
if not (all_codenames - accessor.singleton_permissions()):
|
||||||
role_cts = ContentType.objects.get_for_models(*role_subclasses).values()
|
role_cts = ContentType.objects.get_for_models(*role_subclasses).values()
|
||||||
qs = cls.objects.filter(polymorphic_ctype__in=role_cts)
|
qs = cls.objects.filter(polymorphic_ctype__in=role_cts)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ from awx.main.models.base import CreatedModifiedModel, VarsDictProperty
|
|||||||
from awx.main.models.rbac import ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
|
from awx.main.models.rbac import ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
|
||||||
from awx.main.fields import ImplicitRoleField, JSONBlob, OrderedManyToManyField
|
from awx.main.fields import ImplicitRoleField, JSONBlob, OrderedManyToManyField
|
||||||
from awx.main.models.mixins import (
|
from awx.main.models.mixins import (
|
||||||
ResourceMixin,
|
|
||||||
SurveyJobTemplateMixin,
|
SurveyJobTemplateMixin,
|
||||||
SurveyJobMixin,
|
SurveyJobMixin,
|
||||||
RelatedJobsMixin,
|
RelatedJobsMixin,
|
||||||
@@ -460,7 +459,7 @@ class WorkflowJobOptions(LaunchTimeConfigBase):
|
|||||||
return new_workflow_job
|
return new_workflow_job
|
||||||
|
|
||||||
|
|
||||||
class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin, RelatedJobsMixin, WebhookTemplateMixin):
|
class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, RelatedJobsMixin, WebhookTemplateMixin):
|
||||||
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
|
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
|
||||||
FIELDS_TO_PRESERVE_AT_COPY = [
|
FIELDS_TO_PRESERVE_AT_COPY = [
|
||||||
'labels',
|
'labels',
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ def test_user_roles_unattach_functional(organization, alice, bob, get):
|
|||||||
def test_prefetch_jt_capabilities(job_template, rando):
|
def test_prefetch_jt_capabilities(job_template, rando):
|
||||||
job_template.execute_role.members.add(rando)
|
job_template.execute_role.members.add(rando)
|
||||||
qs = JobTemplate.objects.all()
|
qs = JobTemplate.objects.all()
|
||||||
mapping = prefetch_page_capabilities(JobTemplate, qs, ['admin', 'execute'], rando)
|
mapping = prefetch_page_capabilities(JobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], rando)
|
||||||
assert mapping[job_template.id] == {'edit': False, 'start': True}
|
assert mapping[job_template.id] == {'edit': False, 'start': True}
|
||||||
|
|
||||||
|
|
||||||
@@ -250,10 +250,10 @@ def test_prefetch_jt_capabilities(job_template, rando):
|
|||||||
def test_prefetch_ujt_job_template_capabilities(alice, bob, job_template):
|
def test_prefetch_ujt_job_template_capabilities(alice, bob, job_template):
|
||||||
job_template.execute_role.members.add(alice)
|
job_template.execute_role.members.add(alice)
|
||||||
qs = UnifiedJobTemplate.objects.all()
|
qs = UnifiedJobTemplate.objects.all()
|
||||||
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, ['admin', 'execute'], alice)
|
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], alice)
|
||||||
assert mapping[job_template.id] == {'edit': False, 'start': True}
|
assert mapping[job_template.id] == {'edit': False, 'start': True}
|
||||||
qs = UnifiedJobTemplate.objects.all()
|
qs = UnifiedJobTemplate.objects.all()
|
||||||
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, ['admin', 'execute'], bob)
|
mapping = prefetch_page_capabilities(UnifiedJobTemplate, qs, [{'edit': 'change'}, {'start': 'execute'}], bob)
|
||||||
assert mapping[job_template.id] == {'edit': False, 'start': False}
|
assert mapping[job_template.id] == {'edit': False, 'start': False}
|
||||||
|
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ def test_prefetch_ujt_project_capabilities(alice, project, job_template, mocker)
|
|||||||
def test_prefetch_group_capabilities(group, rando):
|
def test_prefetch_group_capabilities(group, rando):
|
||||||
group.inventory.adhoc_role.members.add(rando)
|
group.inventory.adhoc_role.members.add(rando)
|
||||||
qs = Group.objects.all()
|
qs = Group.objects.all()
|
||||||
mapping = prefetch_page_capabilities(Group, qs, ['inventory.admin', 'inventory.adhoc'], rando)
|
mapping = prefetch_page_capabilities(Group, qs, [{'edit': 'inventory.change'}, {'adhoc': 'inventory.adhoc'}], rando)
|
||||||
assert mapping[group.id] == {'edit': False, 'adhoc': True}
|
assert mapping[group.id] == {'edit': False, 'adhoc': True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def test_access_qs_external_auditor(ext_auditor_rd, rando, job_template):
|
|||||||
ujt_cls = apps.get_model('main', 'UnifiedJobTemplate')
|
ujt_cls = apps.get_model('main', 'UnifiedJobTemplate')
|
||||||
assert job_template in jt_cls.access_qs(rando)
|
assert job_template in jt_cls.access_qs(rando)
|
||||||
assert job_template.id in jt_cls.access_ids_qs(rando)
|
assert job_template.id in jt_cls.access_ids_qs(rando)
|
||||||
assert job_template.id in ujt_cls.accessible_pk_qs(rando, 'read_role')
|
assert job_template.id in ujt_cls.access_ids_qs(rando, 'view')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from crum import impersonate
|
|||||||
from awx.main.fields import ImplicitRoleField
|
from awx.main.fields import ImplicitRoleField
|
||||||
from awx.main.models.rbac import get_role_from_object_role, give_creator_permissions, get_role_codenames, get_role_definition
|
from awx.main.models.rbac import get_role_from_object_role, give_creator_permissions, get_role_codenames, get_role_definition
|
||||||
from awx.main.models import ActivityStream, User, Organization, WorkflowJobTemplate, WorkflowJobTemplateNode, Team
|
from awx.main.models import ActivityStream, User, Organization, WorkflowJobTemplate, WorkflowJobTemplateNode, Team
|
||||||
|
from awx.main.constants import org_role_to_permission
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
from ansible_base.rbac.models import RoleUserAssignment, RoleDefinition
|
from ansible_base.rbac.models import RoleUserAssignment, RoleDefinition
|
||||||
@@ -147,11 +148,11 @@ def test_organization_level_permissions(organization, inventory, setup_managed_r
|
|||||||
assert u1 not in organization.workflow_admin_role
|
assert u1 not in organization.workflow_admin_role
|
||||||
assert not (set(u1.has_roles.all()) & set(u2.has_roles.all())) # user have no roles in common
|
assert not (set(u1.has_roles.all()) & set(u2.has_roles.all())) # user have no roles in common
|
||||||
|
|
||||||
# Old style
|
# Old style (converted to new style)
|
||||||
assert set(Organization.accessible_objects(u1, 'inventory_admin_role')) == set([organization])
|
assert set(Organization.access_qs(u1, org_role_to_permission['inventory_admin_role'])) == set([organization])
|
||||||
assert set(Organization.accessible_objects(u2, 'inventory_admin_role')) == set()
|
assert set(Organization.access_qs(u2, org_role_to_permission['inventory_admin_role'])) == set()
|
||||||
assert set(Organization.accessible_objects(u1, 'workflow_admin_role')) == set()
|
assert set(Organization.access_qs(u1, org_role_to_permission['workflow_admin_role'])) == set()
|
||||||
assert set(Organization.accessible_objects(u2, 'workflow_admin_role')) == set([organization])
|
assert set(Organization.access_qs(u2, org_role_to_permission['workflow_admin_role'])) == set([organization])
|
||||||
|
|
||||||
# New style
|
# New style
|
||||||
assert set(Organization.access_qs(u1, 'add_inventory')) == set([organization])
|
assert set(Organization.access_qs(u1, 'add_inventory')) == set([organization])
|
||||||
@@ -165,7 +166,7 @@ def test_organization_level_permissions(organization, inventory, setup_managed_r
|
|||||||
def test_organization_execute_role(organization, rando, setup_managed_roles):
|
def test_organization_execute_role(organization, rando, setup_managed_roles):
|
||||||
organization.execute_role.members.add(rando)
|
organization.execute_role.members.add(rando)
|
||||||
assert rando in organization.execute_role
|
assert rando in organization.execute_role
|
||||||
assert set(Organization.accessible_objects(rando, 'execute_role')) == set([organization])
|
assert set(Organization.access_qs(rando, org_role_to_permission['execute_role'])) == set([organization])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ def test_team_accessible_by(team, user, project):
|
|||||||
u = user('team_member', False)
|
u = user('team_member', False)
|
||||||
|
|
||||||
team.member_role.children.add(project.use_role)
|
team.member_role.children.add(project.use_role)
|
||||||
assert list(Project.accessible_objects(team, 'read_role')) == [project]
|
assert list(Project.access_qs(team, 'view')) == [project]
|
||||||
assert u not in project.read_role
|
assert u not in project.read_role
|
||||||
|
|
||||||
team.member_role.members.add(u)
|
team.member_role.members.add(u)
|
||||||
@@ -104,11 +104,11 @@ def test_team_accessible_objects(team, user, project):
|
|||||||
u = user('team_member', False)
|
u = user('team_member', False)
|
||||||
|
|
||||||
team.member_role.children.add(project.use_role)
|
team.member_role.children.add(project.use_role)
|
||||||
assert len(Project.accessible_objects(team, 'read_role')) == 1
|
assert len(Project.access_qs(team, 'view')) == 1
|
||||||
assert not Project.accessible_objects(u, 'read_role')
|
assert not Project.access_qs(u, 'view')
|
||||||
|
|
||||||
team.member_role.members.add(u)
|
team.member_role.members.add(u)
|
||||||
assert len(Project.accessible_objects(u, 'read_role')) == 1
|
assert len(Project.access_qs(u, 'view')) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -117,7 +117,7 @@ def test_team_admin_member_access(team, user, project):
|
|||||||
team.member_role.children.add(project.use_role)
|
team.member_role.children.add(project.use_role)
|
||||||
team.admin_role.members.add(u)
|
team.admin_role.members.add(u)
|
||||||
|
|
||||||
assert len(Project.accessible_objects(u, 'use_role')) == 1
|
assert len(Project.access_qs(u, 'use')) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -154,7 +154,7 @@ def test_org_admin_team_access(organization, team, user, project):
|
|||||||
|
|
||||||
team.member_role.children.add(project.use_role)
|
team.member_role.children.add(project.use_role)
|
||||||
|
|
||||||
assert len(Project.accessible_objects(u, 'use_role')) == 1
|
assert len(Project.access_qs(u, 'use')) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
+25
-26
@@ -592,18 +592,18 @@ def prefetch_page_capabilities(model, page, prefetch_list, user):
|
|||||||
4: {'edit': True, 'start': True},
|
4: {'edit': True, 'start': True},
|
||||||
6: {'edit': False, 'start': False}
|
6: {'edit': False, 'start': False}
|
||||||
}
|
}
|
||||||
Each capability is produced for all items in the page in a single query
|
Each capability is produced for all items in the page in a single query.
|
||||||
|
|
||||||
Examples of prefetch language:
|
Examples of prefetch language (permission-based):
|
||||||
prefetch_list = ['admin', 'execute']
|
prefetch_list = [{'edit': 'change'}, {'start': 'execute'}]
|
||||||
--> prefetch the admin (edit) and execute (start) permissions for
|
--> prefetch change (edit) and execute (start) permissions for
|
||||||
items in list for current user
|
items in list for current user
|
||||||
prefetch_list = ['inventory.admin']
|
prefetch_list = [{'edit': 'inventory.change'}]
|
||||||
--> prefetch the related inventory FK permissions for current user,
|
--> prefetch related inventory FK change permissions for current user,
|
||||||
and put it into the object's cache
|
and put it into the object's cache
|
||||||
prefetch_list = [{'copy': ['inventory.admin', 'project.admin']}]
|
prefetch_list = [{'copy': ['inventory.use', 'project.use']}]
|
||||||
--> prefetch logical combination of admin permission to inventory AND
|
--> prefetch logical combination of use permission to inventory AND
|
||||||
project, put into cache dictionary as "copy"
|
project, put into cache dictionary as 'copy'
|
||||||
"""
|
"""
|
||||||
page_ids = [obj.id for obj in page]
|
page_ids = [obj.id for obj in page]
|
||||||
mapping = {}
|
mapping = {}
|
||||||
@@ -621,29 +621,28 @@ def prefetch_page_capabilities(model, page, prefetch_list, user):
|
|||||||
if type(paths) is not list:
|
if type(paths) is not list:
|
||||||
paths = [paths]
|
paths = [paths]
|
||||||
|
|
||||||
# Build the query for accessible_objects according the user & role(s)
|
# Build the query for access_qs according the user & permission(s)
|
||||||
filter_args = []
|
filter_args = []
|
||||||
for role_path in paths:
|
for perm_path in paths:
|
||||||
if '.' in role_path:
|
if '.' in perm_path:
|
||||||
res_path = '__'.join(role_path.split('.')[:-1])
|
res_path = '__'.join(perm_path.split('.')[:-1])
|
||||||
role_type = role_path.split('.')[-1]
|
action = perm_path.split('.')[-1]
|
||||||
parent_model = model
|
parent_model = model
|
||||||
for subpath in role_path.split('.')[:-1]:
|
for subpath in perm_path.split('.')[:-1]:
|
||||||
parent_model = parent_model._meta.get_field(subpath).related_model
|
parent_model = parent_model._meta.get_field(subpath).related_model
|
||||||
filter_args.append(
|
access_qs = parent_model.access_qs(user, action)
|
||||||
Q(Q(**{'%s__pk__in' % res_path: parent_model.accessible_pk_qs(user, '%s_role' % role_type)}) | Q(**{'%s__isnull' % res_path: True}))
|
filter_args.append(Q(Q(**{'%s__in' % res_path: access_qs}) | Q(**{'%s__isnull' % res_path: True})))
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
role_type = role_path
|
action = perm_path
|
||||||
filter_args.append(Q(**{'pk__in': model.accessible_pk_qs(user, '%s_role' % role_type)}))
|
if hasattr(model, 'access_qs'):
|
||||||
|
filter_args.append(Q(**{'pk__in': model.access_qs(user, action)}))
|
||||||
|
else:
|
||||||
|
# UnifiedJobTemplate is not registered with the permission registry,
|
||||||
|
# so it only has access_ids_qs (not access_qs)
|
||||||
|
filter_args.append(Q(**{'pk__in': model.access_ids_qs(user, action)}))
|
||||||
|
|
||||||
if display_method is None:
|
if display_method is None:
|
||||||
# Role name translation to UI names for methods
|
display_method = action
|
||||||
display_method = role_type
|
|
||||||
if role_type == 'admin':
|
|
||||||
display_method = 'edit'
|
|
||||||
elif role_type in ['execute', 'update']:
|
|
||||||
display_method = 'start'
|
|
||||||
|
|
||||||
# Union that query with the list of items on page
|
# Union that query with the list of items on page
|
||||||
filter_args.append(Q(pk__in=page_ids))
|
filter_args.append(Q(pk__in=page_ids))
|
||||||
|
|||||||
@@ -219,11 +219,6 @@ This example allows the Django debug toolbar to work.
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
# models/mixins.py
|
# models/mixins.py
|
||||||
class ResourceMixin(models.Model):
|
|
||||||
"""Common resource management functionality"""
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
class ExecutionEnvironmentMixin(models.Model):
|
class ExecutionEnvironmentMixin(models.Model):
|
||||||
"""Execution environment configuration"""
|
"""Execution environment configuration"""
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ be the same as `serializer_class` of corresponding resource detail view; for exa
|
|||||||
Third, for the underlying model of the resource, add two macros, `FIELDS_TO_PRESERVE_AT_COPY` and
|
Third, for the underlying model of the resource, add two macros, `FIELDS_TO_PRESERVE_AT_COPY` and
|
||||||
`FIELDS_TO_DISCARD_AT_COPY`, as needed. Here is an example:
|
`FIELDS_TO_DISCARD_AT_COPY`, as needed. Here is an example:
|
||||||
```python
|
```python
|
||||||
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, ResourceMixin):
|
class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin):
|
||||||
'''
|
'''
|
||||||
A job template is a reusable job definition for applying a project (with
|
A job template is a reusable job definition for applying a project (with
|
||||||
playbook) to an inventory source with a given credential.
|
playbook) to an inventory source with a given credential.
|
||||||
|
|||||||
Reference in New Issue
Block a user