diff --git a/awx/__init__.py b/awx/__init__.py index 1729f32d59..d2157b6b53 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -14,7 +14,7 @@ __all__ = ['__version__'] try: import awx.devonly MODE = 'development' -except ImportError: +except ImportError: # pragma: no cover MODE = 'production' def find_commands(management_dir): @@ -27,7 +27,7 @@ def find_commands(management_dir): continue elif f.endswith('.py') and f[:-3] not in commands: commands.append(f[:-3]) - elif f.endswith('.pyc') and f[:-4] not in commands: + elif f.endswith('.pyc') and f[:-4] not in commands: # pragma: no cover commands.append(f[:-4]) except OSError: pass @@ -43,7 +43,7 @@ def prepare_env(): # Hide DeprecationWarnings when running in production. Need to first load # settings to apply our filter after Django's own warnings filter. from django.conf import settings - if not settings.DEBUG: + if not settings.DEBUG: # pragma: no cover warnings.simplefilter('ignore', DeprecationWarning) # Monkeypatch Django find_commands to also work with .pyc files. import django.core.management @@ -53,7 +53,7 @@ def prepare_env(): import django.utils try: import django.utils.six - except ImportError: + except ImportError: # pragma: no cover import six sys.modules['django.utils.six'] = sys.modules['six'] django.utils.six = sys.modules['django.utils.six'] @@ -61,7 +61,7 @@ def prepare_env(): # Use the AWX_TEST_DATABASE_* environment variables to specify the test # database settings to use when management command is run as an external # program via unit tests. - for opt in ('ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'): + for opt in ('ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'): # pragma: no cover if os.environ.get('AWX_TEST_DATABASE_%s' % opt, None): settings.DATABASES['default'][opt] = os.environ['AWX_TEST_DATABASE_%s' % opt] # Disable capturing all SQL queries in memory when in DEBUG mode. @@ -75,7 +75,7 @@ def manage(): prepare_env() # Now run the command (or display the version). from django.core.management import execute_from_command_line - if len(sys.argv) >= 2 and sys.argv[1] in ('version', '--version'): + if len(sys.argv) >= 2 and sys.argv[1] in ('version', '--version'): # pragma: no cover sys.stdout.write('%s\n' % __version__) else: execute_from_command_line(sys.argv) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 2d7f4398be..51c2ccacdf 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -143,8 +143,10 @@ class BaseSerializer(serializers.ModelSerializer): def get_related(self, obj): res = SortedDict() - if getattr(obj, 'created_by', None): + if getattr(obj, 'created_by', None) and obj.created_by.is_active: res['created_by'] = reverse('api:user_detail', args=(obj.created_by.pk,)) + if getattr(obj, 'modified_by', None) and obj.modified_by.is_active: + res['modified_by'] = reverse('api:user_detail', args=(obj.modified_by.pk,)) return res def get_summary_fields(self, obj): @@ -154,12 +156,17 @@ class BaseSerializer(serializers.ModelSerializer): for fk, related_fields in SUMMARIZABLE_FK_FIELDS.items(): try: fkval = getattr(obj, fk, None) - if fkval is not None: - summary_fields[fk] = SortedDict() - for field in related_fields: - fval = getattr(fkval, field, None) - if fval is not None: - summary_fields[fk][field] = fval + if fkval is None: + continue + if hasattr(fkval, 'active') and not fkval.active: + continue + if hasattr(fkval, 'is_active') and not fkval.is_active: + continue + summary_fields[fk] = SortedDict() + for field in related_fields: + fval = getattr(fkval, field, None) + if fval is not None: + summary_fields[fk][field] = fval # Can be raised by the reverse accessor for a OneToOneField. except ObjectDoesNotExist: pass @@ -337,7 +344,7 @@ class ProjectSerializer(BaseSerializer): project_updates = reverse('api:project_updates_list', args=(obj.pk,)), activity_stream = reverse('api:project_activity_stream_list', args=(obj.pk,)), )) - if obj.credential: + if obj.credential and obj.credential.active: res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,)) if obj.current_update: @@ -364,6 +371,12 @@ class ProjectSerializer(BaseSerializer): raise serializers.ValidationError('Invalid path choice') return attrs + def to_native(self, obj): + ret = super(ProjectSerializer, self).to_native(obj) + if 'credential' in ret and (not obj.credential or not obj.credential.active): + ret['credential'] = None + return ret + class ProjectPlaybooksSerializer(ProjectSerializer): @@ -433,12 +446,19 @@ class InventorySerializer(BaseSerializerWithVariables): variable_data = reverse('api:inventory_variable_data', args=(obj.pk,)), script = reverse('api:inventory_script_view', args=(obj.pk,)), tree = reverse('api:inventory_tree_view', args=(obj.pk,)), - organization = reverse('api:organization_detail', args=(obj.organization.pk,)), inventory_sources = reverse('api:inventory_inventory_sources_list', args=(obj.pk,)), activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)), )) + if obj.organization and obj.organization.active: + res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,)) return res + def to_native(self, obj): + ret = super(InventorySerializer, self).to_native(obj) + if 'organization' in ret and (not obj.organization or not obj.organization.active): + ret['organization'] = None + return ret + class HostSerializer(BaseSerializerWithVariables): @@ -458,7 +478,6 @@ class HostSerializer(BaseSerializerWithVariables): res = super(HostSerializer, self).get_related(obj) res.update(dict( variable_data = reverse('api:host_variable_data', args=(obj.pk,)), - inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), groups = reverse('api:host_groups_list', args=(obj.pk,)), all_groups = reverse('api:host_all_groups_list', args=(obj.pk,)), job_events = reverse('api:host_job_events_list', args=(obj.pk,)), @@ -466,9 +485,11 @@ class HostSerializer(BaseSerializerWithVariables): activity_stream = reverse('api:host_activity_stream_list', args=(obj.pk,)), #inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)), )) - if obj.last_job: + if obj.inventory and obj.inventory.active: + res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) + if obj.last_job and obj.last_job.active: res['last_job'] = reverse('api:job_detail', args=(obj.last_job.pk,)) - if obj.last_job_host_summary: + if obj.last_job_host_summary and obj.last_job_host_summary.job.active: res['last_job_host_summary'] = reverse('api:job_host_summary_detail', args=(obj.last_job_host_summary.pk,)) return res @@ -543,6 +564,16 @@ class HostSerializer(BaseSerializerWithVariables): return attrs + def to_native(self, obj): + ret = super(HostSerializer, self).to_native(obj) + if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): + ret['inventory'] = None + if 'last_job' in ret and (not obj.last_job or not obj.last_job.active): + ret['last_job'] = None + if 'last_job_host_summary' in ret and (not obj.last_job_host_summary or not obj.last_job_host_summary.job.active): + ret['last_job_host_summary'] = None + return ret + class GroupSerializer(BaseSerializerWithVariables): @@ -563,13 +594,15 @@ class GroupSerializer(BaseSerializerWithVariables): potential_children = reverse('api:group_potential_children_list', args=(obj.pk,)), children = reverse('api:group_children_list', args=(obj.pk,)), all_hosts = reverse('api:group_all_hosts_list', args=(obj.pk,)), - inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), job_events = reverse('api:group_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('api:group_job_host_summaries_list', args=(obj.pk,)), - inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)), activity_stream = reverse('api:group_activity_stream_list', args=(obj.pk,)), #inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)), )) + if obj.inventory and obj.inventory.active: + res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) + if obj.inventory_source: + res['inventory_source'] = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)) return res def validate_name(self, attrs, source): @@ -578,6 +611,12 @@ class GroupSerializer(BaseSerializerWithVariables): raise serializers.ValidationError('Invalid group name') return attrs + def to_native(self, obj): + ret = super(GroupSerializer, self).to_native(obj) + if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): + ret['inventory'] = None + return ret + class GroupTreeSerializer(GroupSerializer): @@ -659,11 +698,11 @@ class InventorySourceSerializer(BaseSerializer): #hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)), #groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)), )) - if obj.inventory: + if obj.inventory and obj.inventory.active: res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) - if obj.group: + if obj.group and obj.group.active: res['group'] = reverse('api:group_detail', args=(obj.group.pk,)) - if obj.credential: + if obj.credential and obj.credential.active: res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,)) if obj.current_update: @@ -712,6 +751,16 @@ class InventorySourceSerializer(BaseSerializer): field_opts['rax_region_choices'] = self.opts.model.get_rax_region_choices() return metadata + def to_native(self, obj): + ret = super(InventorySourceSerializer, self).to_native(obj) + if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): + ret['inventory'] = None + if 'group' in ret and (not obj.group or not obj.group.active): + ret['group'] = None + if 'credential' in ret and (not obj.credential or not obj.credential.active): + ret['credential'] = None + return ret + class InventoryUpdateSerializer(BaseTaskSerializer): @@ -749,12 +798,19 @@ class TeamSerializer(BaseSerializer): projects = reverse('api:team_projects_list', args=(obj.pk,)), users = reverse('api:team_users_list', args=(obj.pk,)), credentials = reverse('api:team_credentials_list', args=(obj.pk,)), - organization = reverse('api:organization_detail', args=(obj.organization.pk,)), permissions = reverse('api:team_permissions_list', args=(obj.pk,)), activity_stream = reverse('api:team_activity_stream_list', args=(obj.pk,)), )) + if obj.organization: + res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,)) return res + def to_native(self, obj): + ret = super(TeamSerializer, self).to_native(obj) + if 'organization' in ret and (not obj.organization or not obj.organization.active): + ret['organization'] = None + return ret + class PermissionSerializer(BaseSerializer): @@ -792,6 +848,18 @@ class PermissionSerializer(BaseSerializer): 'assigning deployment permissions') return attrs + def to_native(self, obj): + ret = super(PermissionSerializer, self).to_native(obj) + if 'user' in ret and (not obj.user or not obj.user.is_active): + ret['user'] = None + if 'team' in ret and (not obj.team or not obj.team.active): + ret['team'] = None + if 'project' in ret and (not obj.project or not obj.project.active): + ret['project'] = None + if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): + ret['inventory'] = None + return ret + class CredentialSerializer(BaseSerializer): @@ -810,6 +878,10 @@ class CredentialSerializer(BaseSerializer): def to_native(self, obj): ret = super(CredentialSerializer, self).to_native(obj) + if 'user' in ret and (not obj.user or not obj.user.is_active): + ret['user'] = None + if 'team' in ret and (not obj.team or not obj.team.active): + ret['team'] = None # Replace the actual encrypted value with the string $encrypted$. for field in Credential.PASSWORD_FIELDS: if field in ret and unicode(ret[field]).startswith('$encrypted$'): @@ -852,20 +924,36 @@ class JobTemplateSerializer(BaseSerializer): return {} res = super(JobTemplateSerializer, self).get_related(obj) res.update(dict( - inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), - project = reverse('api:project_detail', args=(obj.project.pk,)), - jobs = reverse('api:job_template_jobs_list', args=(obj.pk,)), + jobs = reverse('api:job_template_jobs_list', args=(obj.pk,)), activity_stream = reverse('api:job_template_activity_stream_list', args=(obj.pk,)), )) - if obj.credential: + if obj.inventory and obj.inventory.active: + res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) + if obj.project and obj.project.active: + res['project'] = reverse('api:project_detail', args=(obj.project.pk,)), + if obj.credential and obj.credential.active: res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,)) - if obj.cloud_credential: + if obj.cloud_credential and obj.cloud_credential.active: res['cloud_credential'] = reverse('api:credential_detail', args=(obj.cloud_credential.pk,)) if obj.host_config_key: res['callback'] = reverse('api:job_template_callback', args=(obj.pk,)) return res + def to_native(self, obj): + ret = super(JobTemplateSerializer, self).to_native(obj) + if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): + ret['inventory'] = None + if 'project' in ret and (not obj.project or not obj.project.active): + ret['project'] = None + if 'playbook' in ret: + ret['playbook'] = '' + if 'credential' in ret and (not obj.credential or not obj.credential.active): + ret['credential'] = None + if 'cloud_credential' in ret and (not obj.cloud_credential or not obj.cloud_credential.active): + ret['cloud_credential'] = None + return ret + def validate_playbook(self, attrs, source): project = attrs.get('project', None) playbook = attrs.get('playbook', '') @@ -894,16 +982,23 @@ class JobSerializer(BaseTaskSerializer): return {} res = super(JobSerializer, self).get_related(obj) res.update(dict( - inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), - project = reverse('api:project_detail', args=(obj.project.pk,)), - credential = reverse('api:credential_detail', args=(obj.credential.pk,)), job_events = reverse('api:job_job_events_list', args=(obj.pk,)), job_host_summaries = reverse('api:job_job_host_summaries_list', args=(obj.pk,)), activity_stream = reverse('api:job_activity_stream_list', args=(obj.pk,)), )) - if obj.job_template: - res['job_template'] = reverse('api:job_template_detail', args=(obj.job_template.pk,)) - if obj.cloud_credential: + if obj.job_template and obj.job_template.active: + res['job_template'] = reverse('api:job_template_detail', + args=(obj.job_template.pk,)) + if obj.inventory and obj.inventory.active: + res['inventory'] = reverse('api:inventory_detail', + args=(obj.inventory.pk,)) + if obj.project and obj.project.active: + res['project'] = reverse('api:project_detail', + args=(obj.project.pk,)) + if obj.credential and obj.credential.active: + res['credential'] = reverse('api:credential_detail', + args=(obj.credential.pk,)) + if obj.cloud_credential and obj.cloud_credential.active: res['cloud_credential'] = reverse('api:credential_detail', args=(obj.cloud_credential.pk,)) if obj.can_start or True: @@ -923,9 +1018,11 @@ class JobSerializer(BaseTaskSerializer): return # Don't auto-populate name or description. data.setdefault('job_type', job_template.job_type) - data.setdefault('inventory', job_template.inventory.pk) - data.setdefault('project', job_template.project.pk) - data.setdefault('playbook', job_template.playbook) + if job_template.inventory: + data.setdefault('inventory', job_template.inventory.pk) + if job_template.project: + data.setdefault('project', job_template.project.pk) + data.setdefault('playbook', job_template.playbook) if job_template.credential: data.setdefault('credential', job_template.credential.pk) if job_template.cloud_credential: @@ -937,6 +1034,22 @@ class JobSerializer(BaseTaskSerializer): data.setdefault('job_tags', job_template.job_tags) return super(JobSerializer, self).from_native(data, files) + def to_native(self, obj): + ret = super(JobSerializer, self).to_native(obj) + if 'job_template' in ret and (not obj.job_template or not obj.job_template.active): + ret['job_template'] = None + if 'inventory' in ret and (not obj.inventory or not obj.inventory.active): + ret['inventory'] = None + if 'project' in ret and (not obj.project or not obj.project.active): + ret['project'] = None + if 'playbook' in ret: + ret['playbook'] = '' + if 'credential' in ret and (not obj.credential or not obj.credential.active): + ret['credential'] = None + if 'cloud_credential' in ret and (not obj.cloud_credential or not obj.cloud_credential.active): + ret['cloud_credential'] = None + return ret + class JobHostSummarySerializer(BaseSerializer): diff --git a/awx/main/access.py b/awx/main/access.py index 8d8f3f5726..615ed70424 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -120,7 +120,7 @@ class BaseAccess(object): return self.model.objects.none() def can_read(self, obj): - return bool(obj and self.get_queryset().filter(pk=obj.pk).count()) + return bool(obj and self.get_queryset().filter(pk=obj.pk).exists()) def can_add(self, data): return self.user.is_superuser @@ -172,18 +172,18 @@ class UserAccess(BaseAccess): qs = self.model.objects.filter(is_active=True).distinct() if self.user.is_superuser: return qs - if self.user.admin_of_organizations.count(): + if self.user.admin_of_organizations.filter(active=True).exists(): return qs return qs.filter( Q(pk=self.user.pk) | - Q(organizations__in=self.user.admin_of_organizations.all()) | - Q(organizations__in=self.user.organizations.all()) | - Q(teams__in=self.user.teams.all()) + Q(organizations__in=self.user.admin_of_organizations.filter(active=True)) | + Q(organizations__in=self.user.organizations.filter(active=True)) | + Q(teams__in=self.user.teams.filter(active=True)) ).distinct() def can_add(self, data): return bool(self.user.is_superuser or - self.user.admin_of_organizations.count()) + self.user.admin_of_organizations.filter(active=True).exists()) def can_change(self, obj, data): # A user can be changed if they are themselves, or by org admins or @@ -195,7 +195,8 @@ class UserAccess(BaseAccess): # Admin implies changing all user fields. if self.user.is_superuser: return True - return bool(obj.organizations.filter(admins__in=[self.user]).count()) + #return bool(obj.organizations.filter(active=True, admins__in=[self.user]).exists()) TODO: replace line below + return bool(obj.organizations.filter(admins__in=[self.user]).exists()) def can_delete(self, obj): if obj == self.user: @@ -206,7 +207,8 @@ class UserAccess(BaseAccess): # cannot delete the last active superuser return False return bool(self.user.is_superuser or - obj.organizations.filter(admins__in=[self.user]).count()) + #obj.organizations.filter(active=True, admins__in=[self.user]).exists()) TODO: replace line below + obj.organizations.filter(admins__in=[self.user]).exists()) class OrganizationAccess(BaseAccess): ''' @@ -258,21 +260,24 @@ class InventoryAccess(BaseAccess): qs = qs.select_related('created_by', 'organization') if self.user.is_superuser: return qs - admin_of = qs.filter(organization__admins__in=[self.user]).distinct() + admin_of = qs.filter(organization__admins__in=[self.user], + #organization__active=True).distinct() TODO: enable this line + ).distinct() has_user_perms = qs.filter( permissions__user__in=[self.user], permissions__permission_type__in=allowed, - permissions__active=True, + permissions__active=True, # TODO: add test for this case ).distinct() has_team_perms = qs.filter( permissions__team__users__in=[self.user], + #permissions__team__active=True, TODO: enable this line permissions__permission_type__in=allowed, - permissions__active=True, + permissions__active=True, # TODO: add test for this case ).distinct() return admin_of | has_user_perms | has_team_perms def has_permission_types(self, obj, allowed): - return bool(obj and self.get_queryset(allowed).filter(pk=obj.pk).count()) + return bool(obj and self.get_queryset(allowed).filter(pk=obj.pk).exists()) def can_read(self, obj): return self.has_permission_types(obj, PERMISSION_TYPES_ALLOWING_INVENTORY_READ) @@ -280,7 +285,9 @@ class InventoryAccess(BaseAccess): def can_add(self, data): # If no data is specified, just checking for generic add permission? if not data: - return bool(self.user.is_superuser or self.user.admin_of_organizations.count()) + return bool(self.user.is_superuser or + #self.user.admin_of_organizations.filter(active=True).exists()) TODO: replace line below + self.user.admin_of_organizations.all().exists()) # Otherwise, verify that the user has access to change the parent # organization of this inventory. if self.user.is_superuser: @@ -360,7 +367,6 @@ class HostAccess(BaseAccess): raise PermissionDenied("license has expired") if validation_info.get('free_instances', 0) > 0: - # BOOKMARK return True instances = validation_info.get('available_instances', 0) raise PermissionDenied("license range of %s instances has been exceeded" % instances) @@ -433,7 +439,6 @@ class GroupAccess(BaseAccess): parent_pks.add(obj.pk) child_pks = set(sub_obj.all_children.values_list('pk', flat=True)) child_pks.add(sub_obj.pk) - #print parent_pks, child_pks if parent_pks & child_pks: return False return True @@ -518,11 +523,14 @@ class CredentialAccess(BaseAccess): qs = qs.select_related('created_by', 'user', 'team') if self.user.is_superuser: return qs + #orgs_as_admin = self.user.admin_of_organizations.filter(active=True) TODO: replace line below orgs_as_admin = self.user.admin_of_organizations.all() return qs.filter( Q(user=self.user) | Q(user__organizations__in=orgs_as_admin) | Q(user__admin_of_organizations__in=orgs_as_admin) | + #Q(team__organization__in=orgs_as_admin, team__active=True) | TODO: replace both lines below + #Q(team__users__in=[self.user], team__active=True) Q(team__organization__in=orgs_as_admin) | Q(team__users__in=[self.user]) ) @@ -550,11 +558,14 @@ class CredentialAccess(BaseAccess): if obj.user: if self.user == obj.user: return True - if obj.user.organizations.filter(admins__in=[self.user]).count(): + #if obj.user.organizations.filter(active=True, admins__in=[self.user]).exists(): TODO: replace line below + if obj.user.organizations.filter(admins__in=[self.user]).exists(): return True - if obj.user.admin_of_organizations.filter(admins__in=[self.user]).count(): + #if obj.user.admin_of_organizations.filter(active=True, admins__in=[self.user]).exists(): TODO: replace line below + if obj.user.admin_of_organizations.filter(admins__in=[self.user]).exists(): return True if obj.team: + #if self.user in obj.team.organization.admins.filter(active=True): TODO: replace line below if self.user in obj.team.organization.admins.all(): return True return False @@ -585,6 +596,7 @@ class TeamAccess(BaseAccess): if self.user.is_superuser: return qs return qs.filter( + #Q(organization__admins__in=[self.user], organization__active=True) | TODO: replace line below Q(organization__admins__in=[self.user]) | Q(users__in=[self.user]) ) @@ -639,17 +651,21 @@ class ProjectAccess(BaseAccess): allowed = [PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] return qs.filter( Q(created_by=self.user) | + #Q(organizations__admins__in=[self.user], organizations__active=True) | TODO: replace 3 lines below + #Q(organizations__users__in=[self.user], organizations__active=True) | + #Q(teams__users__in=[self.user], teams__active=True) | Q(organizations__admins__in=[self.user]) | Q(organizations__users__in=[self.user]) | Q(teams__users__in=[self.user]) | - Q(permissions__user=self.user, permissions__permission_type__in=allowed, permissions__active=True) | + Q(permissions__user=self.user, permissions__permission_type__in=allowed, permissions__active=True) | # TODO: add tests for these 2 lines? Q(permissions__team__users__in=[self.user], permissions__permission_type__in=allowed, permissions__active=True) ) def can_add(self, data): if self.user.is_superuser: return True - if self.user.admin_of_organizations.count(): + #if self.user.admin_of_organizations.filter(active=True).exists(): TODO: replace line below + if self.user.admin_of_organizations.all().exists(): return True return False @@ -658,7 +674,8 @@ class ProjectAccess(BaseAccess): return True if obj.created_by == self.user: return True - if obj.organizations.filter(admins__in=[self.user]).count(): + #if obj.organizations.filter(active=True, admins__in=[self.user]).exists(): TODO: replace line below + if obj.organizations.filter(admins__in=[self.user]).exists(): return True return False @@ -707,12 +724,15 @@ class PermissionAccess(BaseAccess): 'project') if self.user.is_superuser: return qs + #orgs_as_admin = self.user.admin_of_organizations.filter(active=True) TODO: replace line below orgs_as_admin = self.user.admin_of_organizations.all() return qs.filter( Q(user__organizations__in=orgs_as_admin) | Q(user__admin_of_organizations__in=orgs_as_admin) | + #Q(team__organization__in=orgs_as_admin, team__active=True) | TODO: replace line below Q(team__organization__in=orgs_as_admin) | Q(user=self.user) | + #Q(team__users__in=[self.user], team__active=True) TODO: replace line below Q(team__users__in=[self.user]) ) @@ -802,7 +822,9 @@ class JobTemplateAccess(BaseAccess): credential_qs = self.user.get_queryset(Credential) base_qs = qs.filter( Q(credential__in=credential_qs) | Q(credential__isnull=True), + Q(cloud_credential__in=credential_qs) | Q(cloud_credential__isnull=True), ) + # FIXME: Check active status on related objects! org_admin_qs = base_qs.filter( project__organizations__admins__in=[self.user] ) @@ -821,13 +843,17 @@ class JobTemplateAccess(BaseAccess): def can_read(self, obj): # you can only see the job templates that you have permission to launch. - data = dict( - inventory = obj.inventory.pk, - project = obj.project.pk, - job_type = obj.job_type, - ) + data = { + 'job_type': obj.job_type, + } + if obj.inventory and obj.inventory.pk: + data['inventory'] = obj.inventory.pk + if obj.project and obj.project.pk: + data['project'] = obj.project.pk if obj.credential: data['credential'] = obj.credential.pk + if obj.cloud_credential: + data['cloud_credential'] = obj.cloud_credential.pk return self.can_add(data) def can_add(self, data): @@ -850,6 +876,14 @@ class JobTemplateAccess(BaseAccess): if not self.user.can_access(Credential, 'read', credential): return False + # If a cloud credential is provided, the user should have read access. + cloud_credential_pk = get_pk_from_dict(data, 'cloud_credential') + if cloud_credential_pk: + cloud_credential = get_object_or_400(Credential, + pk=cloud_credential_pk) + if not self.user.can_access(Credential, 'read', cloud_credential): + return False + # Check that the given inventory ID is valid. inventory_pk = get_pk_from_dict(data, 'inventory') inventory = get_object_or_400(Inventory, pk=inventory_pk) @@ -1004,7 +1038,7 @@ class JobEventAccess(BaseAccess): 'host', 'parent') qs = qs.prefetch_related('hosts', 'children') - # Filter certain "internal" events generating by async polling. + # Filter certain "internal" events generated by async polling. qs = qs.exclude(event__in=('runner_on_ok', 'runner_on_failed'), event_data__icontains='"ansible_job_id": "', event_data__contains='"module_name": "async_status"') diff --git a/awx/main/tests/users.py b/awx/main/tests/users.py index 54234db323..b5ca0629ef 100644 --- a/awx/main/tests/users.py +++ b/awx/main/tests/users.py @@ -45,6 +45,10 @@ class UsersTest(BaseTest): self.post(url, expect=400, data=new_user, auth=self.get_super_credentials()) self.post(url, expect=201, data=new_user2, auth=self.get_normal_credentials()) self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials()) + # Normal user cannot add users after his org is marked inactive. + self.organizations[0].mark_inactive() + new_user3 = dict(username='blippy3') + self.post(url, expect=403, data=new_user3, auth=self.get_normal_credentials()) def test_auth_token_login(self): auth_token_url = reverse('api:auth_token_view') @@ -230,8 +234,15 @@ class UsersTest(BaseTest): # Normal user is an org admin, can see all users. data2 = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data2['count'], 4) + # Other use can only see users in his org. data1 = self.get(url, expect=200, auth=self.get_other_credentials()) self.assertEquals(data1['count'], 2) + # Normal user can no longer see all users after the organization he + # admins is marked inactive, nor can he see any other users that were + # in that org, so he only sees himself. + self.organizations[0].mark_inactive() + data3 = self.get(url, expect=200, auth=self.get_normal_credentials()) + self.assertEquals(data3['count'], 1) def test_super_user_can_delete_a_user_but_only_marked_inactive(self): user_pk = self.normal_django_user.pk