diff --git a/awx/api/generics.py b/awx/api/generics.py index 23dcce4d35..cdd10e497d 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -13,6 +13,7 @@ from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.utils.encoding import smart_text from django.utils.safestring import mark_safe +from django.contrib.contenttypes.models import ContentType # Django REST Framework from rest_framework.authentication import get_authorization_header @@ -475,7 +476,9 @@ class ResourceAccessList(ListAPIView): resource_model = getattr(self, 'resource_model') obj = resource_model.objects.get(pk=self.object_id) - roles = set([p.role for p in obj.role_permissions.all()]) + content_type = ContentType.objects.get_for_model(obj) + roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id)) + ancestors = set() for r in roles: ancestors.update(set(r.ancestors.all())) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 190036da48..8884243a96 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1485,7 +1485,7 @@ class ResourceAccessListElementSerializer(UserSerializer): if 'summary_fields' not in ret: ret['summary_fields'] = {} - ret['summary_fields']['permissions'] = get_user_permissions_on_resource(obj, user) + ret['summary_fields']['permissions'] = get_roles_on_resource(obj, user) def format_role_perm(role): role_dict = { 'id': role.id, 'name': role.name, 'description': role.description} @@ -1495,7 +1495,7 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['related'] = reverse_gfk(role.content_object) except: pass - return { 'role': role_dict, 'permissions': get_role_permissions_on_resource(obj, role)} + return { 'role': role_dict, 'permissions': get_roles_on_resource(obj, role)} def format_team_role_perm(team_role, permissive_role_ids): role = team_role.children.filter(id__in=permissive_role_ids)[0] @@ -1513,13 +1513,15 @@ class ResourceAccessListElementSerializer(UserSerializer): role_dict['related'] = reverse_gfk(role.content_object) except: pass - return { 'role': role_dict, 'permissions': get_role_permissions_on_resource(obj, team_role)} + return { 'role': role_dict, 'permissions': get_roles_on_resource(obj, team_role)} team_content_type = ContentType.objects.get_for_model(Team) content_type = ContentType.objects.get_for_model(obj) - direct_permissive_role_ids = RolePermission.objects.filter(content_type=content_type, object_id=obj.id).values_list('role__id') - all_permissive_role_ids = RolePermission.objects.filter(content_type=content_type, object_id=obj.id).values_list('role__ancestors__id') + + content_type = ContentType.objects.get_for_model(obj) + direct_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('id', flat=True) + all_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('ancestors__id', flat=True) direct_access_roles = user.roles \ .filter(id__in=direct_permissive_role_ids).all() diff --git a/awx/api/views.py b/awx/api/views.py index e80d3edf4a..05d908f8e4 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -220,7 +220,7 @@ class ApiV1ConfigView(APIView): user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) data['user_ldap_fields'] = user_ldap_fields - if request.user.is_superuser or Organization.accessible_objects(request.user, {'write': True}).exists(): + if request.user.is_superuser or Organization.accessible_objects(request.user, 'admin_role').exists(): data.update(dict( project_base_dir = settings.PROJECTS_ROOT, project_local_paths = Project.get_local_path_choices(), @@ -566,7 +566,7 @@ class OrganizationList(ListCreateAPIView): serializer_class = OrganizationSerializer def get_queryset(self): - qs = Organization.accessible_objects(self.request.user, {'read': True}) + qs = Organization.accessible_objects(self.request.user, 'read_role') qs = qs.select_related('admin_role', 'auditor_role', 'member_role') return qs @@ -595,27 +595,27 @@ class OrganizationList(ListCreateAPIView): return full_context db_results = {} - org_qs = self.model.accessible_objects(self.request.user, {"read": True}) + org_qs = self.model.accessible_objects(self.request.user, 'read_role') org_id_list = org_qs.values('id') if len(org_id_list) == 0: if self.request.method == 'POST': full_context['related_field_counts'] = {} return full_context - inv_qs = Inventory.accessible_objects(self.request.user, {"read": True}) - project_qs = Project.accessible_objects(self.request.user, {"read": True}) + inv_qs = Inventory.accessible_objects(self.request.user, 'read_role') + project_qs = Project.accessible_objects(self.request.user, 'read_role') # Produce counts of Foreign Key relationships db_results['inventories'] = inv_qs\ .values('organization').annotate(Count('organization')).order_by('organization') db_results['teams'] = Team.accessible_objects( - self.request.user, {"read": True}).values('organization').annotate( + self.request.user, 'read_role').values('organization').annotate( Count('organization')).order_by('organization') JT_reference = 'project__organization' db_results['job_templates'] = JobTemplate.accessible_objects( - self.request.user, {"read": True}).values(JT_reference).annotate( + self.request.user, 'read_role').values(JT_reference).annotate( Count(JT_reference)).order_by(JT_reference) db_results['projects'] = project_qs\ @@ -667,7 +667,7 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView): org_id = int(self.kwargs['pk']) org_counts = {} - access_kwargs = {'accessor': self.request.user, 'permissions': {"read": True}} + access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'} direct_counts = Organization.objects.filter(id=org_id).annotate( users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True) @@ -784,8 +784,8 @@ class TeamList(ListCreateAPIView): serializer_class = TeamSerializer def get_queryset(self): - qs = Team.accessible_objects(self.request.user, {'read': True}) - qs = qs.select_related('admin_role', 'auditor_role', 'member_role') + qs = Team.accessible_objects(self.request.user, 'read_role').order_by() + qs = qs.select_related('admin_role', 'auditor_role', 'member_role', 'organization') return qs class TeamDetail(RetrieveUpdateDestroyAPIView): @@ -831,8 +831,8 @@ class TeamProjectsList(SubListAPIView): def get_queryset(self): team = self.get_parent_object() self.check_parent_access(team) - team_qs = Project.objects.filter(Q(member_role__parents=team.member_role) | Q(admin_role__parents=team.member_role)) - user_qs = Project.accessible_objects(self.request.user, {'read': True}) + team_qs = Project.objects.filter(Q(member_role__parents=team.member_role) | Q(admin_role__parents=team.member_role)).distinct() + user_qs = Project.accessible_objects(self.request.user, 'read_role').distinct() return team_qs & user_qs @@ -860,8 +860,8 @@ class TeamActivityStreamList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(Q(team=parent) | - Q(project__in=Project.accessible_objects(parent, {'read':True})) | - Q(credential__in=Credential.accessible_objects(parent, {'read':True}))) + Q(project__in=Project.accessible_objects(parent, 'read_role')) | + Q(credential__in=Credential.accessible_objects(parent, 'read_role'))) class TeamAccessList(ResourceAccessList): @@ -875,7 +875,7 @@ class ProjectList(ListCreateAPIView): serializer_class = ProjectSerializer def get_queryset(self): - projects_qs = Project.accessible_objects(self.request.user, {'read': True}) + projects_qs = Project.accessible_objects(self.request.user, 'read_role') projects_qs = projects_qs.select_related( 'organization', 'admin_role', @@ -1065,7 +1065,7 @@ class UserTeamsList(ListAPIView): u = get_object_or_404(User, pk=self.kwargs['pk']) if not self.request.user.can_access(User, 'read', u): raise PermissionDenied() - return Team.accessible_objects(self.request.user, {'read': True}).filter(member_role__members=u) + return Team.accessible_objects(self.request.user, 'read_role').filter(member_role__members=u) class UserRolesList(SubListCreateAttachDetachAPIView): @@ -1103,8 +1103,8 @@ class UserProjectsList(SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = Project.accessible_objects(self.request.user, {'read': True}) - user_qs = Project.accessible_objects(parent, {'read': True}) + my_qs = Project.accessible_objects(self.request.user, 'read_role') + user_qs = Project.accessible_objects(parent, 'read_role') return my_qs & user_qs class UserOrganizationsList(SubListAPIView): @@ -1117,7 +1117,7 @@ class UserOrganizationsList(SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = Organization.accessible_objects(self.request.user, {'read': True}) + my_qs = Organization.accessible_objects(self.request.user, 'read_role') user_qs = Organization.objects.filter(member_role__members=parent) return my_qs & user_qs @@ -1131,7 +1131,7 @@ class UserAdminOfOrganizationsList(SubListAPIView): def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) - my_qs = Organization.accessible_objects(self.request.user, {'read': True}) + my_qs = Organization.accessible_objects(self.request.user, 'read_role') user_qs = Organization.objects.filter(admin_role__members=parent) return my_qs & user_qs @@ -1217,7 +1217,7 @@ class CredentialList(ListCreateAPIView): organization = Organization.objects.get(pk=request.data['organization']) obj = organization - if not obj.accessible_by(self.request.user, {'write': True}): + if self.request.user not in obj.admin_role: raise PermissionDenied() ret = super(CredentialList, self).post(request, *args, **kwargs) @@ -1242,8 +1242,8 @@ class UserCredentialsList(CredentialList): if not self.request.user.can_access(User, 'read', user): raise PermissionDenied() - visible_creds = Credential.accessible_objects(self.request.user, {'read': True}) - user_creds = Credential.accessible_objects(user, {'read': True}) + visible_creds = Credential.accessible_objects(self.request.user, 'read_role') + user_creds = Credential.accessible_objects(user, 'read_role') return user_creds & visible_creds def post(self, request, *args, **kwargs): @@ -1262,7 +1262,7 @@ class TeamCredentialsList(CredentialList): if not self.request.user.can_access(Team, 'read', team): raise PermissionDenied() - visible_creds = Credential.accessible_objects(self.request.user, {'read': True}) + visible_creds = Credential.accessible_objects(self.request.user, 'read_role') team_creds = Credential.objects.filter(owner_role__parents=team.member_role) return team_creds & visible_creds @@ -1282,8 +1282,8 @@ class OrganizationCredentialList(CredentialList): if not self.request.user.can_access(Organization, 'read', organization): raise PermissionDenied() - user_visible = Credential.accessible_objects(self.request.user, {'read': True}).all() - org_set = Credential.accessible_objects(organization.admin_role, {'read': True}).all() + user_visible = Credential.accessible_objects(self.request.user, 'read_role').all() + org_set = Credential.accessible_objects(organization.admin_role, 'read_role').all() if self.request.user.is_superuser: return org_set @@ -1353,8 +1353,8 @@ class InventoryList(ListCreateAPIView): serializer_class = InventorySerializer def get_queryset(self): - qs = Inventory.accessible_objects(self.request.user, {'read': True}) - qs = qs.select_related('admin_role', 'auditor_role', 'updater_role', 'executor_role') + qs = Inventory.accessible_objects(self.request.user, 'read_role') + qs = qs.select_related('admin_role', 'auditor_role', 'update_role', 'execute_role') return qs class InventoryDetail(RetrieveUpdateDestroyAPIView): @@ -2120,12 +2120,12 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): if 'credential' in prompted_fields and prompted_fields['credential'] != getattrd(obj, 'credential.pk', None): new_credential = Credential.objects.get(pk=prompted_fields['credential']) - if not request.user.can_access(Credential, 'use', new_credential): + if request.user not in new_credential.use_role: raise PermissionDenied() if 'inventory' in prompted_fields and prompted_fields['inventory'] != getattrd(obj, 'inventory.pk', None): new_inventory = Inventory.objects.get(pk=prompted_fields['inventory']) - if not request.user.can_access(Inventory, 'use', new_inventory): + if request.user not in new_inventory.use_role: raise PermissionDenied() kv = prompted_fields diff --git a/awx/main/access.py b/awx/main/access.py index 6ea07c318b..7b28b89ae8 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -18,13 +18,12 @@ from rest_framework.exceptions import ParseError, PermissionDenied from awx.main.utils import * # noqa from awx.main.models import * # noqa from awx.main.models.mixins import ResourceMixin -from awx.main.models.rbac import ALL_PERMISSIONS from awx.api.license import LicenseForbids from awx.main.task_engine import TaskSerializer from awx.main.conf import tower_settings __all__ = ['get_user_queryset', 'check_user_access', - 'user_accessible_objects', 'user_accessible_by', + 'user_accessible_objects', 'user_admin_role',] PERMISSION_TYPES = [ @@ -64,19 +63,14 @@ def register_access(model_class, access_class): @property def user_admin_role(self): - return Role.objects.get(content_type=ContentType.objects.get_for_model(User), object_id=self.id) + return Role.objects.get( + content_type=ContentType.objects.get_for_model(User), + object_id=self.id, + role_field='admin_role' + ) -def user_accessible_objects(user, permissions): - return ResourceMixin._accessible_objects(User, user, permissions) - -def user_accessible_by(instance, user, permissions): - perms = get_user_permissions_on_resource(instance, user) - if perms is None: - return False - for k in permissions: - if k not in perms or perms[k] < permissions[k]: - return False - return True +def user_accessible_objects(user, role_name): + return ResourceMixin._accessible_objects(User, user, role_name) def get_user_queryset(user, model_class): ''' @@ -218,11 +212,18 @@ class UserAccess(BaseAccess): if tower_settings.ORG_ADMINS_CAN_SEE_ALL_USERS and self.user.admin_of_organizations.exists(): return User.objects.all() - viewable_users_set = set() - viewable_users_set.update(self.user.roles.values_list('ancestors__members__id', flat=True)) - viewable_users_set.update(self.user.roles.values_list('descendents__members__id', flat=True)) + return ( + User.objects.filter( + pk__in=Organization.accessible_objects(self.user, 'read_role').values('member_role__members') + ) | + User.objects.filter( + pk=self.user.id + ) | + User.objects.filter( + pk__in=Role.objects.filter(singleton_name__in = [ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR]).values('members') + ) + ).distinct() - return User.objects.filter(id__in=viewable_users_set) def can_add(self, data): if data is not None and 'is_superuser' in data: @@ -230,7 +231,7 @@ class UserAccess(BaseAccess): return False if self.user.is_superuser: return True - return Organization.accessible_objects(self.user, ALL_PERMISSIONS).exists() + return Organization.accessible_objects(self.user, 'admin_role').exists() def can_change(self, obj, data): if data is not None and 'is_superuser' in data: @@ -257,7 +258,7 @@ class UserAccess(BaseAccess): return False if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'delete': True}) + return False class OrganizationAccess(BaseAccess): @@ -273,13 +274,13 @@ class OrganizationAccess(BaseAccess): model = Organization def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by').all() def can_change(self, obj, data): if self.user.is_superuser: return True - return obj.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.admin_role def can_delete(self, obj): self.check_license(feature='multiple_organizations', check_expiration=False) @@ -308,29 +309,29 @@ class InventoryAccess(BaseAccess): model = Inventory def get_queryset(self, allowed=None, ad_hoc=None): - qs = self.model.accessible_objects(self.user, {'read': True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by', 'organization').all() def can_read(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'read': True}) + return self.user in obj.read_role def can_use(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'use': True}) + return self.user in obj.use_role def can_add(self, data): # If no data is specified, just checking for generic add permission? if not data: - return Organization.accessible_objects(self.user, ALL_PERMISSIONS).exists() + return Organization.accessible_objects(self.user, 'admin_role').exists() if self.user.is_superuser: return True org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - return org.accessible_by(self.user, {'read': True, 'create':True, 'update': True, 'delete': True}) + return self.user in org.admin_role def can_change(self, obj, data): # Verify that the user has access to the new organization if moving an @@ -338,10 +339,10 @@ class InventoryAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: org = get_object_or_400(Organization, pk=org_pk) - if not org.accessible_by(self.user, {'read': True, 'create':True, 'update': True, 'delete': True}): + if self.user not in org.admin_role: return False # Otherwise, just check for write permission. - return obj.accessible_by(self.user, {'read': True, 'create':True, 'update': True, 'delete': True}) + return self.user in obj.admin_role def can_admin(self, obj, data): # Verify that the user has access to the new organization if moving an @@ -349,16 +350,16 @@ class InventoryAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: org = get_object_or_400(Organization, pk=org_pk) - if not org.accessible_by(self.user, ALL_PERMISSIONS): + if self.user not in org.admin_role: return False # Otherwise, just check for admin permission. - return obj.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.admin_role def can_delete(self, obj): return self.can_admin(obj, None) def can_run_ad_hoc_commands(self, obj): - return obj.accessible_by(self.user, {'execute': True}) + return self.user in obj.adhoc_role class HostAccess(BaseAccess): ''' @@ -369,14 +370,17 @@ class HostAccess(BaseAccess): model = Host def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) - qs = qs.select_related('created_by', 'modified_by', 'inventory', - 'last_job__job_template', - 'last_job_host_summary__job') - return qs.prefetch_related('groups').all() + inv_qs = Inventory.accessible_objects(self.user, 'read_role') + group_qs = Group.accessible_objects(self.user, 'read_role') + qs = (self.model.objects.filter(inventory=inv_qs) | self.model.objects.filter(groups=group_qs)).distinct() + #qs = qs.select_related('created_by', 'modified_by', 'inventory', + # 'last_job__job_template', + # 'last_job_host_summary__job') + #return qs.prefetch_related('groups').all() + return qs def can_read(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'read':True}) + return obj and any(self.user in grp.read_role for grp in obj.groups.all()) or self.user in obj.inventory.read_role def can_add(self, data): if not data or 'inventory' not in data: @@ -385,7 +389,7 @@ class HostAccess(BaseAccess): # Checks for admin or change permission on inventory. inventory_pk = get_pk_from_dict(data, 'inventory') inventory = get_object_or_400(Inventory, pk=inventory_pk) - if not inventory.accessible_by(self.user, {'read':True, 'create':True}): + if self.user not in inventory.admin_role: return False # Check to see if we have enough licenses @@ -399,7 +403,7 @@ class HostAccess(BaseAccess): raise PermissionDenied('Unable to change inventory on a host') # Checks for admin or change permission on inventory, controls whether # the user can edit variable data. - return obj and obj.inventory.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + return obj and self.user in obj.inventory.admin_role def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): @@ -412,7 +416,7 @@ class HostAccess(BaseAccess): return True def can_delete(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'delete':True}) + return obj and self.user in obj.inventory.admin_role class GroupAccess(BaseAccess): ''' @@ -423,12 +427,12 @@ class GroupAccess(BaseAccess): model = Group def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') qs = qs.select_related('created_by', 'modified_by', 'inventory') return qs.prefetch_related('parents', 'children', 'inventory_source').all() def can_read(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'read':True}) + return obj and self.user in obj.inventory.read_role def can_add(self, data): if not data or 'inventory' not in data: @@ -436,7 +440,7 @@ class GroupAccess(BaseAccess): # Checks for admin or change permission on inventory. inventory_pk = get_pk_from_dict(data, 'inventory') inventory = get_object_or_400(Inventory, pk=inventory_pk) - return inventory.accessible_by(self.user, {'read':True, 'create':True}) + return self.user in inventory.admin_role def can_change(self, obj, data): # Prevent moving a group to a different inventory. @@ -445,7 +449,7 @@ class GroupAccess(BaseAccess): raise PermissionDenied('Unable to change inventory on a group') # Checks for admin or change permission on inventory, controls whether # the user can attach subgroups or edit variable data. - return obj and obj.inventory.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + return obj and self.user in obj.inventory.admin_role def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): @@ -466,7 +470,7 @@ class GroupAccess(BaseAccess): return True def can_delete(self, obj): - return obj and obj.inventory.accessible_by(self.user, {'delete':True}) + return obj and self.user in obj.inventory.admin_role class InventorySourceAccess(BaseAccess): ''' @@ -485,9 +489,9 @@ class InventorySourceAccess(BaseAccess): def can_read(self, obj): if obj and obj.group: - return obj.group.accessible_by(self.user, {'read':True}) + return self.user in obj.group.read_role elif obj and obj.inventory: - return obj.inventory.accessible_by(self.user, {'read':True}) + return self.user in obj.inventory.read_role else: return False @@ -498,7 +502,7 @@ class InventorySourceAccess(BaseAccess): def can_change(self, obj, data): # Checks for admin or change permission on group. if obj and obj.group: - return obj.group.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + return self.user in obj.group.admin_role # Can't change inventory sources attached to only the inventory, since # these are created automatically from the management command. else: @@ -548,11 +552,11 @@ class CredentialAccess(BaseAccess): """Return the queryset for credentials, based on what the user is permitted to see. """ - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by').all() def can_read(self, obj): - return obj.accessible_by(self.user, {'read': True}) + return self.user in obj.read_role def can_add(self, data): # Access enforced in our view where we have context enough to make a decision @@ -561,12 +565,12 @@ class CredentialAccess(BaseAccess): def can_use(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'use': True}) + return self.user in obj.use_role def can_change(self, obj, data): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'read':True, 'update': True, 'delete':True}) + return self.user in obj.owner_role def can_delete(self, obj): # Unassociated credentials may be marked deleted by anyone, though we @@ -579,17 +583,17 @@ class TeamAccess(BaseAccess): ''' I can see a team when: - I'm a superuser. - - I'm an admin of the team's organization. + - I'm an admin of the team - I'm a member of that team. I can create/change a team when: - I'm a superuser. - - I'm an org admin for the team's org. + - I'm an admin for the team ''' model = Team def get_queryset(self): - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by', 'organization').all() def can_add(self, data): @@ -598,7 +602,7 @@ class TeamAccess(BaseAccess): else: org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - if org.accessible_by(self.user, {'read':True, 'update':True, 'write':True}): + if self.user in org.admin_role: return True return False @@ -607,7 +611,7 @@ class TeamAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: raise PermissionDenied('Unable to change organization on a team') - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.admin_role def can_delete(self, obj): return self.can_change(obj, None) @@ -633,19 +637,19 @@ class ProjectAccess(BaseAccess): def get_queryset(self): if self.user.is_superuser: return self.model.objects.all() - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('modified_by', 'credential', 'current_job', 'last_job').all() def can_add(self, data): if self.user.is_superuser: return True - qs = Organization.accessible_objects(self.user, ALL_PERMISSIONS) + qs = Organization.accessible_objects(self.user, 'admin_role') return qs.exists() def can_change(self, obj, data): if self.user.is_superuser: return True - return obj.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.admin_role def can_delete(self, obj): return self.can_change(obj, None) @@ -674,7 +678,7 @@ class ProjectUpdateAccess(BaseAccess): return self.can_change(obj, {}) and obj.can_cancel def can_delete(self, obj): - return obj and obj.project.accessible_by(self.user, {'delete':True}) + return obj and self.user in obj.project.admin_role class JobTemplateAccess(BaseAccess): ''' @@ -695,7 +699,7 @@ class JobTemplateAccess(BaseAccess): if self.user.is_superuser: qs = self.model.objects.all() else: - qs = self.model.accessible_objects(self.user, {'read':True}) + qs = self.model.accessible_objects(self.user, 'read_role') return qs.select_related('created_by', 'modified_by', 'inventory', 'project', 'credential', 'cloud_credential', 'next_schedule').all() @@ -727,7 +731,7 @@ class JobTemplateAccess(BaseAccess): credential_pk = get_pk_from_dict(data, 'credential') if credential_pk: credential = get_object_or_400(Credential, pk=credential_pk) - if not credential.accessible_by(self.user, {'read':True}): + if self.user not in credential.read_role: return False # If a cloud credential is provided, the user should have read access. @@ -735,7 +739,7 @@ class JobTemplateAccess(BaseAccess): if cloud_credential_pk: cloud_credential = get_object_or_400(Credential, pk=cloud_credential_pk) - if not cloud_credential.accessible_by(self.user, {'read':True}): + if self.user not in cloud_credential.read_role: return False # Check that the given inventory ID is valid. @@ -747,7 +751,7 @@ class JobTemplateAccess(BaseAccess): project_pk = get_pk_from_dict(data, 'project') if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN: org = inventory[0].organization - accessible = org.accessible_by(self.user, {'read':True, 'update':True, 'write':True}) + accessible = self.user in org.admin_role if not project_pk and accessible: return True elif not accessible: @@ -755,10 +759,10 @@ class JobTemplateAccess(BaseAccess): # If the user has admin access to the project (as an org admin), should # be able to proceed without additional checks. project = get_object_or_400(Project, pk=project_pk) - if project.accessible_by(self.user, ALL_PERMISSIONS): + if self.user in project.admin_role: return True - return project.accessible_by(self.user, ALL_PERMISSIONS) and inventory.accessible_by(self.user, {'read':True}) + return self.user in project.admin_role and self.user in inventory.read_role def can_start(self, obj, validate_license=True): # Check license. @@ -776,10 +780,10 @@ class JobTemplateAccess(BaseAccess): if obj.job_type == PERM_INVENTORY_SCAN: # Scan job with default project, must have JT execute or be org admin if obj.project is None and obj.inventory: - return (obj.accessible_by(self.user, {'execute': True}) or - obj.inventory.organization.accessible_by(self.user, ALL_PERMISSIONS)) + return (self.user in obj.execute_role or + self.user in obj.inventory.organization.admin_role) - return obj.accessible_by(self.user, {'execute':True}) + return self.user in obj.execute_role def can_change(self, obj, data): data_for_change = data @@ -814,7 +818,7 @@ class JobAccess(BaseAccess): credential_ids = self.user.get_queryset(Credential) return qs.filter( credential_id__in=credential_ids, - job_template__in=JobTemplate.accessible_objects(self.user, {'read': True}) + job_template__in=JobTemplate.accessible_objects(self.user, 'read_role') ) def can_add(self, data): @@ -847,7 +851,7 @@ class JobAccess(BaseAccess): # Allow org admins and superusers to delete jobs if self.user.is_superuser: return True - return obj.inventory.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.inventory.admin_role def can_start(self, obj): self.check_license() @@ -859,12 +863,12 @@ class JobAccess(BaseAccess): # If a user can launch the job template then they can relaunch a job from that # job template if obj.job_template is not None: - return obj.job_template.accessible_by(self.user, {'execute': True}) + return self.user in obj.job_template.execute_role - inventory_access = obj.inventory.accessible_by(self.user, {'use':True}) + inventory_access = self.user in obj.inventory.use_role - org_access = obj.inventory.organization.accessible_by(self.user, ALL_PERMISSIONS) - project_access = obj.project is None or obj.project.accessible_by(self.user, ALL_PERMISSIONS) + org_access = self.user in obj.inventory.organization.admin_role + project_access = obj.project is None or self.user in obj.project.admin_role return inventory_access and (org_access or project_access) @@ -906,7 +910,7 @@ class AdHocCommandAccess(BaseAccess): return qs.all() credential_ids = set(self.user.get_queryset(Credential).values_list('id', flat=True)) - inventory_qs = Inventory.accessible_objects(self.user, {'read': True, 'execute': True}) + inventory_qs = Inventory.accessible_objects(self.user, 'execute_role') return qs.filter(credential_id__in=credential_ids, inventory__in=inventory_qs) @@ -921,7 +925,7 @@ class AdHocCommandAccess(BaseAccess): credential_pk = get_pk_from_dict(data, 'credential') if credential_pk: credential = get_object_or_400(Credential, pk=credential_pk) - if not credential.accessible_by(self.user, {'read':True}): + if self.user not in credential.read_role: return False # Check that the user has the run ad hoc command permission on the @@ -929,7 +933,7 @@ class AdHocCommandAccess(BaseAccess): inventory_pk = get_pk_from_dict(data, 'inventory') if inventory_pk: inventory = get_object_or_400(Inventory, pk=inventory_pk) - if not inventory.accessible_by(self.user, {'execute': True}): + if self.user not in inventory.execute_role: return False return True @@ -1189,23 +1193,23 @@ class NotifierAccess(BaseAccess): qs = self.model.objects.all() if self.user.is_superuser: return qs - return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, ALL_PERMISSIONS).all()) + return self.model.objects.filter(organization__in=Organization.accessible_objects(self.user, 'admin_role').all()) def can_read(self, obj): if self.user.is_superuser: return True if obj.organization is not None: - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.organization.admin_role return False def can_add(self, data): if self.user.is_superuser: return True if not data: - return Organization.accessible_objects(self.user, ALL_PERMISSIONS).exists() + return Organization.accessible_objects(self.user, 'admin_role').exists() org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - return org.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in org.admin_role def can_change(self, obj, data): if self.user.is_superuser: @@ -1213,10 +1217,10 @@ class NotifierAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') if obj and org_pk and obj.organization.pk != org_pk: org = get_object_or_400(Organization, pk=org_pk) - if not org.accessible_by(self.user, ALL_PERMISSIONS): + if self.user not in org.admin_role: return False if obj.organization is not None: - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.organization.admin_role return False def can_admin(self, obj, data): @@ -1235,7 +1239,7 @@ class NotificationAccess(BaseAccess): qs = self.model.objects.all() if self.user.is_superuser: return qs - return self.model.objects.filter(notifier__organization__in=Organization.accessible_objects(self.user, ALL_PERMISSIONS)) + return self.model.objects.filter(notifier__organization__in=Organization.accessible_objects(self.user, 'admin_role')) def can_read(self, obj): return self.user.can_access(Notifier, 'read', obj.notifier) @@ -1253,13 +1257,13 @@ class LabelAccess(BaseAccess): if self.user.is_superuser: return self.model.objects.all() return self.model.objects.filter( - organization__in=Organization.accessible_objects(self.user, {'read': True}) + organization__in=Organization.accessible_objects(self.user, 'read_role') ) def can_read(self, obj): if self.user.is_superuser: return True - return obj.organization.accessible_by(self.user, {'read': True}) + return self.user in obj.organization.read_role def can_add(self, data): if self.user.is_superuser: @@ -1270,7 +1274,7 @@ class LabelAccess(BaseAccess): org_pk = get_pk_from_dict(data, 'organization') org = get_object_or_400(Organization, pk=org_pk) - return org.accessible_by(self.user, {'read': True}) + return self.user in org.read_role def can_change(self, obj, data): if self.user.is_superuser: @@ -1279,7 +1283,7 @@ class LabelAccess(BaseAccess): if self.can_add(data) is False: return False - return obj.organization.accessible_by(self.user, ALL_PERMISSIONS) + return self.user in obj.organization.admin_role def can_delete(self, obj): return self.can_change(obj, None) @@ -1370,12 +1374,12 @@ class CustomInventoryScriptAccess(BaseAccess): def get_queryset(self): if self.user.is_superuser: return self.model.objects.distinct().all() - return self.model.accessible_objects(self.user, {'read':True}).all() + return self.model.accessible_objects(self.user, 'read_role').all() def can_read(self, obj): if self.user.is_superuser: return True - return obj.accessible_by(self.user, {'read':True}) + return self.user in obj.read_role def can_add(self, data): if self.user.is_superuser: @@ -1464,7 +1468,7 @@ class RoleAccess(BaseAccess): return True if obj.object_id and \ isinstance(obj.content_object, ResourceMixin) and \ - obj.content_object.accessible_by(self.user, {'write': True}): + self.user in obj.content_object.admin_role: return True return False diff --git a/awx/main/fields.py b/awx/main/fields.py index 7e57c29c7d..50d73ee4c7 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -93,10 +93,9 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor): class ImplicitRoleField(models.ForeignKey): """Implicitly creates a role entry for a resource""" - def __init__(self, role_name=None, role_description=None, permissions=None, parent_role=None, *args, **kwargs): + def __init__(self, role_name=None, role_description=None, parent_role=None, *args, **kwargs): self.role_name = role_name self.role_description = role_description if role_description else "" - self.permissions = permissions self.parent_role = parent_role kwargs.setdefault('to', 'Role') @@ -108,7 +107,6 @@ class ImplicitRoleField(models.ForeignKey): name, path, args, kwargs = super(ImplicitRoleField, self).deconstruct() kwargs['role_name'] = self.role_name kwargs['role_description'] = self.role_description - kwargs['permissions'] = self.permissions kwargs['parent_role'] = self.parent_role return name, path, args, kwargs @@ -185,45 +183,17 @@ class ImplicitRoleField(models.ForeignKey): role = Role_.objects.create( created=now(), modified=now(), + role_field=self.name, name=self.role_name, description=self.role_description ) setattr(instance, self.name, role) - def _patch_role_content_object_and_grant_permissions(self, instance): + def _patch_role_content_object(self, instance): role = getattr(instance, self.name) role.content_object = instance role.save() - if self.permissions is not None: - RolePermission_ = get_current_apps().get_model('main', 'RolePermission') - ContentType = get_current_apps().get_model('contenttypes', "ContentType") - instance_content_type = ContentType.objects.get_for_model(instance) - - permissions = RolePermission_( - created=now(), - modified=now(), - role=role, - content_type=instance_content_type, - object_id=instance.id, - auto_generated=True - ) - - if 'all' in self.permissions and self.permissions['all']: - del self.permissions['all'] - self.permissions['create'] = True - self.permissions['read'] = True - self.permissions['write'] = True - self.permissions['update'] = True - self.permissions['delete'] = True - self.permissions['scm_update'] = True - self.permissions['use'] = True - self.permissions['execute'] = True - - for k,v in self.permissions.items(): - setattr(permissions, k, v) - permissions.save() - def _pre_save(self, instance, *args, **kwargs): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): implicit_role_field._create_role_instance_if_not_exists(instance) @@ -231,7 +201,7 @@ class ImplicitRoleField(models.ForeignKey): def _post_save(self, instance, created, *args, **kwargs): if created: for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): - implicit_role_field._patch_role_content_object_and_grant_permissions(instance) + implicit_role_field._patch_role_content_object(instance) with batch_role_ancestor_rebuilding(): for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): @@ -264,6 +234,7 @@ class ImplicitRoleField(models.ForeignKey): else: role = Role_.objects.create(created=now(), modified=now(), + role_field=path, singleton_name=singleton_name, name=singleton_name, description=singleton_name) diff --git a/awx/main/management/commands/generate_dummy_data.py b/awx/main/management/commands/generate_dummy_data.py deleted file mode 100644 index 9f1b2cf83f..0000000000 --- a/awx/main/management/commands/generate_dummy_data.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright (c) 2016 Ansible, Inc. -# All Rights Reserved - -# Python -import sys -from collections import defaultdict -from optparse import make_option - - -# Django -from django.core.management.base import BaseCommand -from django.utils.timezone import now -from django.contrib.auth.models import User -from django.db import transaction - -# awx -from awx.main.models import * # noqa - - - -class Rollback(Exception): - pass - -class Command(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option('--organizations', action='store', type='int', default=3, - help='Number of organizations to create'), - make_option('--users', action='store', type='int', default=10, - help='Number of users to create'), - make_option('--teams', action='store', type='int', default=5, - help='Number of teams to create'), - make_option('--projects', action='store', type='int', default=10, - help='Number of projects to create'), - make_option('--job-templates', action='store', type='int', default=20, - help='Number of job templates to create'), - make_option('--credentials', action='store', type='int', default=5, - help='Number of credentials to create'), - make_option('--inventories', action='store', type='int', default=5, - help='Number of credentials to create'), - make_option('--inventory-groups', action='store', type='int', default=10, - help='Number of credentials to create'), - make_option('--inventory-hosts', action='store', type='int', default=40, - help='number of credentials to create'), - make_option('--jobs', action='store', type='int', default=200, - help='number of job entries to create'), - make_option('--job-events', action='store', type='int', default=500, - help='number of job event entries to create'), - make_option('--pretend', action='store_true', - help="Don't commit the data to the database"), - make_option('--prefix', action='store', type='string', default='', - help="Prefix generated names with this string"), - #make_option('--spread-bias', action='store', type='string', default='exponential', - # help='"exponential" to bias associations exponentially front loaded for - for ex'), - ) - - def handle(self, *args, **options): - n_organizations = int(options['organizations']) - n_users = int(options['users']) - n_teams = int(options['teams']) - n_projects = int(options['projects']) - n_job_templates = int(options['job_templates']) - n_credentials = int(options['credentials']) - n_inventories = int(options['inventories']) - n_inventory_groups = int(options['inventory_groups']) - n_inventory_hosts = int(options['inventory_hosts']) - n_jobs = int(options['jobs']) - n_job_events = int(options['job_events']) - prefix = options['prefix'] - - organizations = [] - users = [] - teams = [] - projects = [] - job_templates = [] - credentials = [] - inventories = [] - inventory_groups = [] - inventory_hosts = [] - jobs = [] - #job_events = [] - - def spread(n, m): - ret = [] - # At least one in each slot, split up the rest exponentially so the first - # buckets contain a lot of entries - for i in xrange(m): - if n > 0: - ret.append(1) - n -= 1 - else: - ret.append(0) - - for i in xrange(m): - n_in_this_slot = n // 2 - n-= n_in_this_slot - ret[i] += n_in_this_slot - if n > 0 and len(ret): - ret[0] += n - return ret - - ids = defaultdict(lambda: 0) - - - try: - - with transaction.atomic(): - with batch_role_ancestor_rebuilding(): - - print('# Creating %d organizations' % n_organizations) - for i in xrange(n_organizations): - sys.stdout.write('\r%d ' % (i + 1)) - sys.stdout.flush() - organizations.append(Organization.objects.create(name='%s Organization %d' % (prefix, i))) - print('') - - print('# Creating %d users' % n_users) - org_idx = 0 - for n in spread(n_users, n_organizations): - for i in range(n): - ids['user'] += 1 - user_id = ids['user'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, organizations[org_idx].name, i+ 1)) - sys.stdout.flush() - user = User.objects.create(username='%suser-%d' % (prefix, user_id)) - organizations[org_idx].member_role.members.add(user) - users.append(user) - org_idx += 1 - print('') - - print('# Creating %d teams' % n_teams) - org_idx = 0 - for n in spread(n_teams, n_organizations): - org = organizations[org_idx] - for i in range(n): - ids['team'] += 1 - team_id = ids['team'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) - sys.stdout.flush() - team = Team.objects.create(name='%s Team %d Org %d' % (prefix, team_id, org_idx), organization=org) - teams.append(team) - org_idx += 1 - print('') - - print('# Adding users to teams') - for org in organizations: - org_teams = [t for t in org.teams.all()] - org_users = [u for u in org.member_role.members.all()] - print(' Spreading %d users accross %d teams for %s' % (len(org_users), len(org_teams), org.name)) - # Our normal spread for most users - cur_user_idx = 0 - cur_team_idx = 0 - for n in spread(len(org_users), len(org_teams)): - team = org_teams[cur_team_idx] - for i in range(n): - if cur_user_idx < len(org_users): - user = org_users[cur_user_idx] - team.member_role.members.add(user) - cur_user_idx += 1 - cur_team_idx += 1 - - # First user gets added to all teams - for team in org_teams: - team.member_role.members.add(org_users[0]) - - - print('# Creating %d credentials for users' % (n_credentials - n_credentials // 2)) - user_idx = 0 - for n in spread(n_credentials - n_credentials // 2, n_users): - user = users[user_idx] - for i in range(n): - ids['credential'] += 1 - sys.stdout.write('\r %d ' % (ids['credential'])) - sys.stdout.flush() - credential_id = ids['credential'] - credential = Credential.objects.create(name='%s Credential %d User %d' % (prefix, credential_id, user_idx)) - credential.owner_role.members.add(user) - credentials.append(credential) - user_idx += 1 - print('') - - print('# Creating %d credentials for teams' % (n_credentials // 2)) - team_idx = 0 - starting_credential_id = ids['credential'] - for n in spread(n_credentials - n_credentials // 2, n_teams): - team = teams[team_idx] - for i in range(n): - ids['credential'] += 1 - sys.stdout.write('\r %d ' % (ids['credential'] - starting_credential_id)) - sys.stdout.flush() - credential_id = ids['credential'] - credential = Credential.objects.create(name='%s Credential %d team %d' % (prefix, credential_id, team_idx)) - credential.owner_role.parents.add(team.member_role) - credentials.append(credential) - team_idx += 1 - print('') - - print('# Creating %d projects' % n_projects) - org_idx = 0 - for n in spread(n_projects, n_organizations): - org = organizations[org_idx] - for i in range(n): - ids['project'] += 1 - project_id = ids['project'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) - sys.stdout.flush() - project = Project.objects.create(name='%s Project %d Org %d' % (prefix, project_id, org_idx), organization=org) - projects.append(project) - - org_idx += 1 - print('') - - - print('# Creating %d inventories' % n_inventories) - org_idx = 0 - for n in spread(n_inventories, min(n_inventories // 4 + 1, n_organizations)): - org = organizations[org_idx] - for i in range(n): - ids['inventory'] += 1 - inventory_id = ids['inventory'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) - sys.stdout.flush() - inventory = Inventory.objects.create(name='%s Inventory %d Org %d' % (prefix, inventory_id, org_idx), organization=org) - inventories.append(inventory) - - org_idx += 1 - print('') - - - print('# Creating %d inventory_groups' % n_inventory_groups) - inv_idx = 0 - for n in spread(n_inventory_groups, n_inventories): - inventory = inventories[inv_idx] - parent_list = [None] * 3 - for i in range(n): - ids['group'] += 1 - group_id = ids['group'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, inventory.name, i+ 1)) - sys.stdout.flush() - group = Group.objects.create( - name='%s Group %d Inventory %d' % (prefix, group_id, inv_idx), - inventory=inventory, - ) - # Have each group have up to 3 parent groups - for parent_n in range(3): - if i // 4 + parent_n < len(parent_list) and parent_list[i // 4 + parent_n]: - group.parents.add(parent_list[i // 4 + parent_n]) - if parent_list[i // 4] is None: - parent_list[i // 4] = group - else: - parent_list.append(group) - inventory_groups.append(group) - - inv_idx += 1 - print('') - - - print('# Creating %d inventory_hosts' % n_inventory_hosts) - group_idx = 0 - for n in spread(n_inventory_hosts, n_inventory_groups): - group = inventory_groups[group_idx] - for i in range(n): - ids['host'] += 1 - host_id = ids['host'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, group.name, i+ 1)) - sys.stdout.flush() - host = Host.objects.create(name='%s Host %d Group %d' % (prefix, host_id, group_idx), inventory=group.inventory) - # Add the host to up to 3 groups - host.groups.add(group) - for m in range(2): - if group_idx + m < len(inventory_groups) and group.inventory.id == inventory_groups[group_idx + m].inventory.id: - host.groups.add(inventory_groups[group_idx + m]) - - inventory_hosts.append(host) - - group_idx += 1 - print('') - - print('# Creating %d job_templates' % n_job_templates) - project_idx = 0 - inv_idx = 0 - for n in spread(n_job_templates, n_projects): - project = projects[project_idx] - for i in range(n): - ids['job_template'] += 1 - job_template_id = ids['job_template'] - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, project.name, i+ 1)) - sys.stdout.flush() - - inventory = None - org_inv_count = project.organization.inventories.count() - if org_inv_count > 0: - inventory = project.organization.inventories.all()[inv_idx % org_inv_count] - - job_template = JobTemplate.objects.create( - name='%s Job Template %d Project %d' % (prefix, job_template_id, project_idx), - inventory=inventory, - project=project, - ) - job_templates.append(job_template) - inv_idx += 1 - project_idx += 1 - print('') - - print('# Creating %d jobs' % n_jobs) - group_idx = 0 - job_template_idx = 0 - for n in spread(n_jobs, n_job_templates): - job_template = job_templates[job_template_idx] - for i in range(n): - sys.stdout.write('\r Assigning %d to %s: %d ' % (n, job_template.name, i+ 1)) - sys.stdout.flush() - job = Job.objects.create(job_template=job_template) - jobs.append(job) - - if job_template.inventory: - inv_groups = [g for g in job_template.inventory.groups.all()] - if len(inv_groups): - JobHostSummary.objects.bulk_create([ - JobHostSummary( - job=job, host=h, host_name=h.name, processed=1, - created=now(), modified=now() - ) - for h in inv_groups[group_idx % len(inv_groups)].hosts.all()[:100] - ]) - group_idx += 1 - job_template_idx += 1 - if n: - print('') - - print('# Creating %d job events' % n_job_events) - job_idx = 0 - for n in spread(n_job_events, n_jobs): - job = jobs[job_idx] - sys.stdout.write('\r Creating %d job events for job %d' % (n, job.id)) - sys.stdout.flush() - JobEvent.objects.bulk_create([ - JobEvent( - created=now(), - modified=now(), - job=job, - event='runner_on_ok' - ) - for i in range(n) - ]) - job_idx += 1 - if n: - print('') - - if options['pretend']: - raise Rollback() - except Rollback: - print('Rolled back changes') - pass - return diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 91b3a0a544..47d20cc1dd 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -780,10 +780,10 @@ class Command(NoArgsCommand): if settings.SQL_DEBUG: queries_before = len(connection.queries) if self.inventory_source.group: - groups_qs = self.inventory_source.group.all_children + groups_qs = self.inventory_source.group.all_children.all() # FIXME: Also include groups from inventory_source.managed_groups? else: - groups_qs = self.inventory.groups + groups_qs = self.inventory.groups.all() # Build list of all group pks, remove those that should not be deleted. del_group_pks = set(groups_qs.values_list('pk', flat=True)) all_group_names = self.all_group.all_groups.keys() @@ -1273,42 +1273,43 @@ class Command(NoArgsCommand): self.is_custom) self.all_group.debug_tree() - # Ensure that this is managed as an atomic SQL transaction, - # and thus properly rolled back if there is an issue. - with transaction.atomic(): - # Merge/overwrite inventory into database. - if settings.SQL_DEBUG: - self.logger.warning('loading into database...') - with ignore_inventory_computed_fields(): - if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): - self.load_into_database() - else: - with disable_activity_stream(): + with batch_role_ancestor_rebuilding(): + # Ensure that this is managed as an atomic SQL transaction, + # and thus properly rolled back if there is an issue. + with transaction.atomic(): + # Merge/overwrite inventory into database. + if settings.SQL_DEBUG: + self.logger.warning('loading into database...') + with ignore_inventory_computed_fields(): + if getattr(settings, 'ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', True): self.load_into_database() - if settings.SQL_DEBUG: - queries_before2 = len(connection.queries) - self.inventory.update_computed_fields() - if settings.SQL_DEBUG: - self.logger.warning('update computed fields took %d queries', - len(connection.queries) - queries_before2) - try: - self.check_license() - except CommandError as e: - self.mark_license_failure(save=True) - raise e + else: + with disable_activity_stream(): + self.load_into_database() + if settings.SQL_DEBUG: + queries_before2 = len(connection.queries) + self.inventory.update_computed_fields() + if settings.SQL_DEBUG: + self.logger.warning('update computed fields took %d queries', + len(connection.queries) - queries_before2) + try: + self.check_license() + except CommandError as e: + self.mark_license_failure(save=True) + raise e - if self.inventory_source.group: - inv_name = 'group "%s"' % (self.inventory_source.group.name) - else: - inv_name = '"%s" (id=%s)' % (self.inventory.name, - self.inventory.id) - if settings.SQL_DEBUG: - self.logger.warning('Inventory import completed for %s in %0.1fs', - inv_name, time.time() - begin) - else: - self.logger.info('Inventory import completed for %s in %0.1fs', - inv_name, time.time() - begin) - status = 'successful' + if self.inventory_source.group: + inv_name = 'group "%s"' % (self.inventory_source.group.name) + else: + inv_name = '"%s" (id=%s)' % (self.inventory.name, + self.inventory.id) + if settings.SQL_DEBUG: + self.logger.warning('Inventory import completed for %s in %0.1fs', + inv_name, time.time() - begin) + else: + self.logger.info('Inventory import completed for %s in %0.1fs', + inv_name, time.time() - begin) + status = 'successful' # If we're in debug mode, then log the queries and time # used to do the operation. diff --git a/awx/main/migrations/0008_v300_rbac_changes.py b/awx/main/migrations/0008_v300_rbac_changes.py index ba9299ca19..0cd6abca3c 100644 --- a/awx/main/migrations/0008_v300_rbac_changes.py +++ b/awx/main/migrations/0008_v300_rbac_changes.py @@ -48,6 +48,19 @@ class Migration(migrations.Migration): field=models.ManyToManyField(related_name='deprecated_teams', to='main.Project', blank=True), ), + migrations.CreateModel( + name='RoleAncestorEntry', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('role_field', models.TextField()), + ('content_type_id', models.PositiveIntegerField(null=False)), + ('object_id', models.PositiveIntegerField(null=False)), + ], + options={ + 'db_table': 'main_rbac_role_ancestors', + 'verbose_name_plural': 'role_ancestors', + }, + ), migrations.CreateModel( name='Role', fields=[ @@ -58,7 +71,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=512)), ('singleton_name', models.TextField(default=None, unique=True, null=True, db_index=True)), ('object_id', models.PositiveIntegerField(default=None, null=True)), - ('ancestors', models.ManyToManyField(related_name='descendents', to='main.Role')), + ('ancestors', models.ManyToManyField(related_name='descendents', through='main.RoleAncestorEntry', to='main.Role')), ('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType', null=True)), ('created_by', models.ForeignKey(related_name="{u'class': 'role', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)), ('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)), @@ -72,177 +85,262 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'roles', }, ), - migrations.CreateModel( - name='RolePermission', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', models.DateTimeField(default=None, editable=False)), - ('modified', models.DateTimeField(default=None, editable=False)), - ('auto_generated', models.BooleanField(default=False)), - ('object_id', models.PositiveIntegerField(default=None)), - ('create', models.IntegerField(default=0)), - ('read', models.IntegerField(default=0)), - ('write', models.IntegerField(default=0)), - ('update', models.IntegerField(default=0)), - ('delete', models.IntegerField(default=0)), - ('execute', models.IntegerField(default=0)), - ('scm_update', models.IntegerField(default=0)), - ('use', models.IntegerField(default=0)), - ('content_type', models.ForeignKey(default=None, to='contenttypes.ContentType')), - ('role', models.ForeignKey(related_name='permissions', to='main.Role')), - ], - options={ - 'db_table': 'main_rbac_permissions', - 'verbose_name_plural': 'permissions', - }, + migrations.AddField( + model_name='roleancestorentry', + name='ancestor', + field=models.ForeignKey(related_name='+', to='main.Role'), + ), + migrations.AddField( + model_name='roleancestorentry', + name='descendent', + field=models.ForeignKey(related_name='+', to='main.Role'), + ), + migrations.AlterIndexTogether( + name='roleancestorentry', + index_together=set([('ancestor', 'content_type_id', 'object_id'), ('ancestor', 'content_type_id', 'role_field')]), ), migrations.AddField( model_name='credential', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Auditor of the credential', parent_role=[b'singleton:System Auditor'], to='main.Role', role_name=b'Credential Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Auditor of the credential', parent_role=[b'singleton:System Auditor'], to='main.Role', role_name=b'Credential Auditor', null=b'True'), ), migrations.AddField( model_name='credential', name='owner_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Owner of the credential', parent_role=[b'singleton:System Administrator'], to='main.Role', role_name=b'Credential Owner', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Owner of the credential', parent_role=[b'singleton:System Administrator'], to='main.Role', role_name=b'Credential Owner', null=b'True'), ), migrations.AddField( model_name='credential', - name='usage_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True', permissions={b'use': True}), + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Credential User', null=b'True'), ), migrations.AddField( model_name='custominventoryscript', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'CustomInventory Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'CustomInventory Administrator', null=b'True'), ), migrations.AddField( model_name='custominventoryscript', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'CustomInventory Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'CustomInventory Auditor', null=b'True'), ), migrations.AddField( model_name='custominventoryscript', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.member_role', to='main.Role', role_name=b'CustomInventory Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.member_role', to='main.Role', role_name=b'CustomInventory Member', null=b'True'), ), migrations.AddField( model_name='group', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', role_name=b'Inventory Group Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.admin_role', b'parents.admin_role'], to='main.Role', role_name=b'Inventory Group Administrator', null=b'True'), ), migrations.AddField( model_name='group', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', role_name=b'Inventory Group Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.auditor_role', b'parents.auditor_role'], to='main.Role', role_name=b'Inventory Group Auditor', null=b'True'), ), migrations.AddField( model_name='group', - name='executor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.executor_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True', permissions={b'read': True, b'execute': True}), + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.executor_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'), ), migrations.AddField( model_name='group', - name='updater_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.updater_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True', permissions={b'read': True, b'write': True, b'create': True, b'use': True}), + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.updater_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'), ), migrations.AddField( model_name='inventory', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Inventory Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this inventory', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Inventory Administrator', null=b'True'), ), migrations.AddField( model_name='inventory', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Inventory Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Inventory Auditor', null=b'True'), ), migrations.AddField( model_name='inventory', - name='executor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True', permissions={b'read': True, b'execute': True}), + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=None, to='main.Role', role_name=b'Inventory Executor', null=b'True'), ), migrations.AddField( model_name='inventory', - name='updater_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True', permissions={b'read': True, b'update': True}), + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=None, to='main.Role', role_name=b'Inventory Updater', null=b'True'), ), migrations.AddField( model_name='inventory', - name='usage_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True', permissions={b'use': True}), + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=None, to='main.Role', role_name=b'Inventory User', null=b'True'), ), migrations.AddField( model_name='jobtemplate', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=b'project.admin_role', to='main.Role', role_name=b'Job Template Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Full access to all settings', parent_role=b'project.admin_role', to='main.Role', role_name=b'Job Template Administrator', null=b'True'), ), migrations.AddField( model_name='jobtemplate', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=b'project.auditor_role', to='main.Role', role_name=b'Job Template Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read-only access to all settings', parent_role=b'project.auditor_role', to='main.Role', role_name=b'Job Template Auditor', null=b'True'), ), migrations.AddField( model_name='jobtemplate', - name='executor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True', permissions={b'read': True, b'execute': True}), + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=None, to='main.Role', role_name=b'Job Template Runner', null=b'True'), ), migrations.AddField( model_name='organization', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage all aspects of this organization', parent_role=b'singleton:System Administrator', to='main.Role', role_name=b'Organization Administrator', null=b'True', permissions={b'write': True, b'use': True, b'scm_update': True, b'execute': True, b'read': True, b'create': True, b'update': True, b'delete': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage all aspects of this organization', parent_role=b'singleton:System Administrator', to='main.Role', role_name=b'Organization Administrator', null=b'True'), ), migrations.AddField( model_name='organization', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this organization', parent_role=b'singleton:System Auditor', to='main.Role', role_name=b'Organization Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this organization', parent_role=b'singleton:System Auditor', to='main.Role', role_name=b'Organization Auditor', null=b'True'), ), migrations.AddField( model_name='organization', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this organization', parent_role=b'admin_role', to='main.Role', role_name=b'Organization Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this organization', parent_role=b'admin_role', to='main.Role', role_name=b'Organization Member', null=b'True'), ), migrations.AddField( model_name='project', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this project', parent_role=[b'organization.admin_role', b'singleton:System Administrator'], to='main.Role', role_name=b'Project Administrator', null=b'True', permissions={b'all': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this project', parent_role=[b'organization.admin_role', b'singleton:System Administrator'], to='main.Role', role_name=b'Project Administrator', null=b'True'), ), migrations.AddField( model_name='project', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this project', parent_role=[b'organization.auditor_role', b'singleton:System Auditor'], to='main.Role', role_name=b'Project Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this project', parent_role=[b'organization.auditor_role', b'singleton:System Auditor'], to='main.Role', role_name=b'Project Auditor', null=b'True'), ), migrations.AddField( model_name='project', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=None, to='main.Role', role_name=b'Project Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=None, to='main.Role', role_name=b'Project Member', null=b'True'), ), migrations.AddField( model_name='project', name='scm_update_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update this project from the source control management system', parent_role=b'admin_role', to='main.Role', role_name=b'Project Updater', null=b'True', permissions={b'scm_update': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update this project from the source control management system', parent_role=b'admin_role', to='main.Role', role_name=b'Project Updater', null=b'True'), ), migrations.AddField( model_name='team', name='admin_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this team', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Team Administrator', null=b'True', permissions={b'write': True, b'use': True, b'scm_update': True, b'execute': True, b'read': True, b'create': True, b'update': True, b'delete': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May manage this team', parent_role=b'organization.admin_role', to='main.Role', role_name=b'Team Administrator', null=b'True'), ), migrations.AddField( model_name='team', name='auditor_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this team', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Team Auditor', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read all settings associated with this team', parent_role=b'organization.auditor_role', to='main.Role', role_name=b'Team Auditor', null=b'True'), ), migrations.AddField( model_name='team', name='member_role', - field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', parent_role=b'admin_role', to='main.Role', role_name=b'Team Member', null=b'True', permissions={b'read': True}), + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'A member of this team', to='main.Role', role_name=b'Team Member', null=b'True'), + ), + + migrations.AddField( + model_name='credential', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May read this credential', parent_role=[b'use_role', b'auditor_role', b'owner_role'], to='main.Role', role_name=b'Credential REad', null=b'True'), + ), + migrations.AddField( + model_name='custominventoryscript', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view but not modify this inventory', parent_role=[b'auditor_role', b'member_role', b'admin_role'], to='main.Role', role_name=b'CustomInventory Read', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='adhoc_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'inventory.adhoc_role', b'parents.adhoc_role', b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'), + ), + migrations.AddField( + model_name='group', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'execute_role', b'update_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='adhoc_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute ad hoc commands against this inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Ad Hoc', null=b'True'), + ), + migrations.AddField( + model_name='inventory', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May view this inventory', parent_role=[b'auditor_role', b'execute_role', b'update_role', b'use_role', b'admin_role'], to='main.Role', role_name=b'Read', null=b'True'), + ), + migrations.AddField( + model_name='jobtemplate', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'execute_role', b'auditor_role', b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'), + ), + migrations.AddField( + model_name='organization', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read an organization', parent_role=[b'member_role', b'auditor_role'], to='main.Role', role_name=b'Organization Read Access', null=b'True'), + ), + migrations.AddField( + model_name='project', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Read access to this project', parent_role=[b'member_role', b'auditor_role', b'scm_update_role'], to='main.Role', role_name=b'Project Read Access', null=b'True'), + ), + migrations.AddField( + model_name='role', + name='role_field', + field=models.TextField(default=b''), + ), + migrations.AddField( + model_name='team', + name='read_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Can view this team', parent_role=[b'admin_role', b'auditor_role', b'member_role'], to='main.Role', role_name=b'Read', null=b'True'), + ), + migrations.AlterField( + model_name='credential', + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this credential, but not read sensitive portions or modify it', parent_role=[b'owner_role'], to='main.Role', role_name=b'Credential User', null=b'True'), + ), + migrations.AlterField( + model_name='group', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.execute_role', b'parents.execute_role', b'adhoc_role'], to='main.Role', role_name=b'Inventory Group Executor', null=b'True'), + ), + migrations.AlterField( + model_name='group', + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'', parent_role=[b'inventory.update_role', b'parents.update_role', b'admin_role'], to='main.Role', role_name=b'Inventory Group Updater', null=b'True'), + ), + migrations.AlterField( + model_name='inventory', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May execute jobs against this inventory', parent_role=b'adhoc_role', to='main.Role', role_name=b'Inventory Executor', null=b'True'), + ), + migrations.AlterField( + model_name='inventory', + name='update_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May update the inventory', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory Updater', null=b'True'), + ), + migrations.AlterField( + model_name='inventory', + name='use_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May use this inventory, but not read sensitive portions or modify it', parent_role=[b'admin_role'], to='main.Role', role_name=b'Inventory User', null=b'True'), + ), + migrations.AlterField( + model_name='jobtemplate', + name='execute_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'May run the job template', parent_role=[b'admin_role'], to='main.Role', role_name=b'Job Template Runner', null=b'True'), + ), + migrations.AlterField( + model_name='project', + name='member_role', + field=awx.main.fields.ImplicitRoleField(related_name='+', role_description=b'Implies membership within this project', parent_role=b'admin_role', to='main.Role', role_name=b'Project Member', null=b'True'), ), - migrations.AlterIndexTogether( - name='rolepermission', - index_together=set([('content_type', 'object_id')]), - ), + + + migrations.RenameField( model_name='organization', old_name='projects', diff --git a/awx/main/migrations/0018_v300_host_ordering.py b/awx/main/migrations/0018_v300_host_ordering.py new file mode 100644 index 0000000000..0309875de6 --- /dev/null +++ b/awx/main/migrations/0018_v300_host_ordering.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0017_v300_prompting_migrations'), + ] + + operations = [ + migrations.AlterModelOptions( + name='host', + options={'ordering': ('name',)}, + ), + ] diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 32c399df81..0d7aa3ecb7 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -34,7 +34,6 @@ def init_rbac_migration(apps, schema_editor): def migrate_users(apps, schema_editor): User = apps.get_model('auth', "User") Role = apps.get_model('main', "Role") - RolePermission = apps.get_model('main', "RolePermission") ContentType = apps.get_model('contenttypes', "ContentType") user_content_type = ContentType.objects.get_for_model(User) @@ -52,15 +51,6 @@ def migrate_users(apps, schema_editor): object_id = user.id ) role.members.add(user) - RolePermission.objects.create( - created=now(), - modified=now(), - role = role, - content_type = user_content_type, - object_id = user.id, - create=1, read=1, write=1, delete=1, update=1, - execute=1, scm_update=1, use=1, - ) logger.info(smart_text(u"migrating to new role for user: {}".format(user.username))) if user.is_superuser: @@ -113,7 +103,7 @@ def attrfunc(attr_path): def _update_credential_parents(org, cred): org.admin_role.children.add(cred.owner_role) - org.member_role.children.add(cred.usage_role) + org.member_role.children.add(cred.use_role) cred.deprecated_user, cred.deprecated_team = None, None cred.save() @@ -147,7 +137,7 @@ def _discover_credentials(instances, cred, orgfunc): # Unlink the old information from the new credential cred.deprecated_user, cred.deprecated_team = None, None - cred.owner_role, cred.usage_role = None, None + cred.owner_role, cred.use_role = None, None cred.save() for i in orgs[org]: @@ -189,7 +179,7 @@ def migrate_credential(apps, schema_editor): if cred.deprecated_team is not None: cred.deprecated_team.admin_role.children.add(cred.owner_role) - cred.deprecated_team.member_role.children.add(cred.usage_role) + cred.deprecated_team.member_role.children.add(cred.use_role) cred.deprecated_user, cred.deprecated_team = None, None cred.save() logger.info(smart_text(u"added Credential(name={}, kind={}, host={}) at user level".format(cred.name, cred.kind, cred.host))) @@ -214,7 +204,7 @@ def migrate_inventory(apps, schema_editor): elif perm.permission_type == 'read': return inventory.auditor_role elif perm.permission_type == 'write': - return inventory.updater_role + return inventory.update_role elif perm.permission_type == 'check' or perm.permission_type == 'run': # These permission types are handled differntly in RBAC now, nothing to migrate. return False @@ -232,7 +222,7 @@ def migrate_inventory(apps, schema_editor): raise Exception(smart_text(u'Unhandled permission type for inventory: {}'.format( perm.permission_type))) if perm.run_ad_hoc_commands: - execrole = inventory.executor_role + execrole = inventory.execute_role if perm.team: if role: @@ -392,20 +382,20 @@ def migrate_job_templates(apps, schema_editor): for team in Team.objects.iterator(): if permission.filter(team=team).exists(): - team.member_role.children.add(jt.executor_role) + team.member_role.children.add(jt.execute_role) logger.info(smart_text(u'adding Team({}) access to JobTemplate({})'.format(team.name, jt.name))) for user in User.objects.iterator(): if permission.filter(user=user).exists(): - jt.executor_role.members.add(user) + jt.execute_role.members.add(user) logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name))) - if jt.accessible_by(user, {'execute': True}): + if user in jt.execute_role: # If the job template is already accessible by the user, because they # are a sytem, organization, or project admin, then don't add an explicit # role entry for them continue if old_access.check_user_access(user, jt.__class__, 'start', jt, False): - jt.executor_role.members.add(user) + jt.execute_role.members.add(user) logger.info(smart_text(u'adding User({}) access to JobTemplate({})'.format(user.username, jt.name))) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 847f52bb35..f6c9702d4b 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -3,7 +3,6 @@ # Django from django.conf import settings # noqa -from django.contrib.contenttypes.fields import GenericRelation # AWX from awx.main.models.base import * # noqa @@ -43,10 +42,8 @@ from awx.main.access import * # noqa User.add_to_class('get_queryset', get_user_queryset) User.add_to_class('can_access', check_user_access) -User.add_to_class('accessible_by', user_accessible_by) User.add_to_class('accessible_objects', user_accessible_objects) User.add_to_class('admin_role', user_admin_role) -User.add_to_class('role_permissions', GenericRelation('main.RolePermission')) @property def user_get_organizations(user): diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index d247154fd7..bd24a72de5 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -174,7 +174,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ], - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Credential Auditor', @@ -182,12 +181,18 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ], - permissions = {'read': True} ) - usage_role = ImplicitRoleField( + use_role = ImplicitRoleField( role_name='Credential User', role_description='May use this credential, but not read sensitive portions or modify it', - permissions = {'use': True} + parent_role=['owner_role'] + ) + read_role = ImplicitRoleField( + role_name='Credential REad', + role_description='May read this credential', + parent_role=[ + 'use_role', 'auditor_role', 'owner_role' + ], ) @property diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 2af823885d..c42bb77967 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -100,28 +100,36 @@ class Inventory(CommonModel, ResourceMixin): role_name='Inventory Administrator', role_description='May manage this inventory', parent_role='organization.admin_role', - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Inventory Auditor', role_description='May view but not modify this inventory', parent_role='organization.auditor_role', - permissions = {'read': True} ) - updater_role = ImplicitRoleField( + update_role = ImplicitRoleField( role_name='Inventory Updater', role_description='May update the inventory', - permissions = {'read': True, 'update': True} + parent_role=['admin_role'], ) - usage_role = ImplicitRoleField( + use_role = ImplicitRoleField( role_name='Inventory User', role_description='May use this inventory, but not read sensitive portions or modify it', - permissions = {'use': True} + parent_role=['admin_role'], ) - executor_role = ImplicitRoleField( + adhoc_role = ImplicitRoleField( + role_name='Inventory Ad Hoc', + role_description='May execute ad hoc commands against this inventory', + parent_role=['admin_role'], + ) + execute_role = ImplicitRoleField( role_name='Inventory Executor', role_description='May execute jobs against this inventory', - permissions = {'read': True, 'execute': True} + parent_role='adhoc_role', + ) + read_role = ImplicitRoleField( + role_name='Read', + parent_role=['auditor_role', 'execute_role', 'update_role', 'use_role', 'admin_role'], + role_description='May view this inventory', ) def get_absolute_url(self): @@ -335,7 +343,7 @@ class Host(CommonModelNameNotUnique, ResourceMixin): class Meta: app_label = 'main' unique_together = (("name", "inventory"),) # FIXME: Add ('instance_id', 'inventory') after migration. - ordering = ('inventory', 'name') + ordering = ('name',) inventory = models.ForeignKey( 'Inventory', @@ -525,22 +533,27 @@ class Group(CommonModelNameNotUnique, ResourceMixin): admin_role = ImplicitRoleField( role_name='Inventory Group Administrator', parent_role=['inventory.admin_role', 'parents.admin_role'], - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Inventory Group Auditor', parent_role=['inventory.auditor_role', 'parents.auditor_role'], - permissions = {'read': True} ) - updater_role = ImplicitRoleField( + update_role = ImplicitRoleField( role_name='Inventory Group Updater', - parent_role=['inventory.updater_role', 'parents.updater_role'], - permissions = {'read': True, 'write': True, 'create': True, 'use': True}, + parent_role=['inventory.update_role', 'parents.update_role', 'admin_role'], ) - executor_role = ImplicitRoleField( + adhoc_role = ImplicitRoleField( + role_name='Inventory Ad Hoc', + parent_role=['inventory.adhoc_role', 'parents.adhoc_role', 'admin_role'], + role_description='May execute ad hoc commands against this inventory', + ) + execute_role = ImplicitRoleField( role_name='Inventory Group Executor', - parent_role=['inventory.executor_role', 'parents.executor_role'], - permissions = {'read':True, 'execute':True}, + parent_role=['inventory.execute_role', 'parents.execute_role', 'adhoc_role'], + ) + read_role = ImplicitRoleField( + role_name='Inventory Group Executor', + parent_role=['execute_role', 'update_role', 'auditor_role', 'admin_role'], ) def __unicode__(self): @@ -1296,21 +1309,23 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): role_name='CustomInventory Administrator', role_description='May manage this inventory', parent_role='organization.admin_role', - permissions = {'all': True} ) member_role = ImplicitRoleField( role_name='CustomInventory Member', role_description='May view but not modify this inventory', parent_role='organization.member_role', - permissions = {'read': True} ) auditor_role = ImplicitRoleField( role_name='CustomInventory Auditor', role_description='May view but not modify this inventory', parent_role='organization.auditor_role', - permissions = {'read': True} + ) + read_role = ImplicitRoleField( + role_name='CustomInventory Read', + role_description='May view but not modify this inventory', + parent_role=['auditor_role', 'member_role', 'admin_role'], ) def get_absolute_url(self): diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 83d507615d..585b4ed43a 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -229,18 +229,21 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): role_name='Job Template Administrator', role_description='Full access to all settings', parent_role='project.admin_role', - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Job Template Auditor', role_description='Read-only access to all settings', parent_role='project.auditor_role', - permissions = {'read': True} ) - executor_role = ImplicitRoleField( + execute_role = ImplicitRoleField( role_name='Job Template Runner', role_description='May run the job template', - permissions = {'read': True, 'execute': True} + parent_role=['admin_role'], + ) + read_role = ImplicitRoleField( + role_name='Job Template Runner', + role_description='May run the job template', + parent_role=['execute_role', 'auditor_role', 'admin_role'], ) @classmethod diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index bcf95fa4ac..45d259bacd 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -1,15 +1,11 @@ # Django from django.db import models -from django.db.models.aggregates import Max -from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User # noqa # AWX from awx.main.models.rbac import ( - get_user_permissions_on_resource, - get_role_permissions_on_resource, - Role, + Role, RoleAncestorEntry, get_roles_on_resource ) @@ -20,88 +16,47 @@ class ResourceMixin(models.Model): class Meta: abstract = True - role_permissions = GenericRelation('main.RolePermission') - @classmethod - def accessible_objects(cls, accessor, permissions): + 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': True}).filter(name__istartswith='bar'); + MyModel.accessible_objects(user, 'read_role').filter(name__istartswith='bar'); NOTE: This should only be used for list type things. If you have a specific resource you want to check permissions on, it is more performant to resolve the resource in question then call `myresource.get_permissions(user)`. ''' - return ResourceMixin._accessible_objects(cls, accessor, permissions) + return ResourceMixin._accessible_objects(cls, accessor, role_field) @staticmethod - def _accessible_objects(cls, accessor, permissions): + def _accessible_objects(cls, accessor, role_field): if type(accessor) == User: - qs = cls.objects.filter( - role_permissions__role__ancestors__members=accessor - ) + ancestor_roles = accessor.roles.all() elif type(accessor) == Role: - qs = cls.objects.filter( - role_permissions__role__ancestors=accessor - ) + ancestor_roles = [accessor] else: accessor_type = ContentType.objects.get_for_model(accessor) - roles = Role.objects.filter(content_type__pk=accessor_type.id, - object_id=accessor.id) - qs = cls.objects.filter( - role_permissions__role__ancestors__in=roles - ) - - for perm in permissions: - qs = qs.annotate(**{'max_' + perm: Max('role_permissions__' + perm)}) - qs = qs.filter(**{'max_' + perm: int(permissions[perm])}) - - #return cls.objects.filter(resource__in=qs) + ancestor_roles = Role.objects.filter(content_type__pk=accessor_type.id, + object_id=accessor.id) + qs = cls.objects.filter(pk__in = + RoleAncestorEntry.objects.filter( + ancestor__in=ancestor_roles, + content_type_id = ContentType.objects.get_for_model(cls).id, + role_field = role_field + ).values_list('object_id').distinct() + ) return qs - def get_permissions(self, user): + def get_permissions(self, accessor): ''' - Returns a dict (or None) of the permissions a user has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to the roles that are applicable for the given - user. - - In example, if a user has been granted read access through a permission - on one role and write access through a permission on a separate role, - the returned dict will denote that the user has both read and write - access. + Returns a dict (or None) of the roles a accessor has for a given resource. + An accessor can be either a User, Role, or an arbitrary resource that + contains one or more Roles associated with it. ''' - return get_user_permissions_on_resource(self, user) + return get_roles_on_resource(self, accessor) - - def get_role_permissions(self, role): - ''' - Returns a dict (or None) of the permissions a role has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to either the role or any descendents of that role. - ''' - - return get_role_permissions_on_resource(self, role) - - - def accessible_by(self, user, permissions): - ''' - Returns true if the user has all of the specified permissions - ''' - - perms = self.get_permissions(user) - if perms is None: - return False - for k in permissions: - if k not in perms or perms[k] < permissions[k]: - return False - return True diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 615a9104fe..571f9117ab 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -19,7 +19,6 @@ from django.utils.translation import ugettext_lazy as _ from awx.main.fields import AutoOneToOneField, ImplicitRoleField from awx.main.models.base import * # noqa from awx.main.models.rbac import ( - ALL_PERMISSIONS, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR, ) @@ -57,19 +56,21 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin): role_name='Organization Administrator', role_description='May manage all aspects of this organization', parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, - permissions = ALL_PERMISSIONS, ) auditor_role = ImplicitRoleField( role_name='Organization Auditor', role_description='May read all settings associated with this organization', parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, - permissions = {'read': True} ) member_role = ImplicitRoleField( role_name='Organization Member', role_description='A member of this organization', parent_role='admin_role', - permissions = {'read': True} + ) + read_role = ImplicitRoleField( + role_name='Organization Read Access', + role_description='Read an organization', + parent_role=['member_role', 'auditor_role'], ) @@ -112,19 +113,20 @@ class Team(CommonModelNameNotUnique, ResourceMixin): role_name='Team Administrator', role_description='May manage this team', parent_role='organization.admin_role', - permissions = ALL_PERMISSIONS, ) auditor_role = ImplicitRoleField( role_name='Team Auditor', role_description='May read all settings associated with this team', parent_role='organization.auditor_role', - permissions = {'read': True} ) member_role = ImplicitRoleField( role_name='Team Member', role_description='A member of this team', - parent_role='admin_role', - permissions = {'read':True}, + ) + read_role = ImplicitRoleField( + role_name='Read', + role_description='Can view this team', + parent_role=['admin_role', 'auditor_role', 'member_role'], ) def get_absolute_url(self): diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 0f931cea91..12357fca2e 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -227,7 +227,6 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): 'organization.admin_role', 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ], - permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Project Auditor', @@ -236,18 +235,21 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin): 'organization.auditor_role', 'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR, ], - permissions = {'read': True} ) member_role = ImplicitRoleField( role_name='Project Member', role_description='Implies membership within this project', - permissions = {'read': True} + parent_role='admin_role', ) scm_update_role = ImplicitRoleField( role_name='Project Updater', role_description='May update this project from the source control management system', parent_role='admin_role', - permissions = {'scm_update': True} + ) + read_role = ImplicitRoleField( + role_name='Project Read Access', + role_description='Read access to this project', + parent_role=['member_role', 'auditor_role', 'scm_update_role'], ) @classmethod diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py index 91e055f6eb..6ca5ef4109 100644 --- a/awx/main/models/rbac.py +++ b/awx/main/models/rbac.py @@ -7,24 +7,22 @@ import threading import contextlib # Django -from django.db import models, transaction +from django.db import models, transaction, connection from django.db.models import Q -from django.db.models.aggregates import Max from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey + # AWX from django.contrib.auth.models import User # noqa from awx.main.models.base import * # noqa __all__ = [ 'Role', - 'RolePermission', 'batch_role_ancestor_rebuilding', - 'get_user_permissions_on_resource', - 'get_role_permissions_on_resource', + 'get_roles_on_resource', 'ROLE_SINGLETON_SYSTEM_ADMINISTRATOR', 'ROLE_SINGLETON_SYSTEM_AUDITOR', ] @@ -34,10 +32,6 @@ logger = logging.getLogger('awx.main.models.rbac') ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator' ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor' -ALL_PERMISSIONS = {'create': True, 'read': True, 'update': True, 'delete': True, - 'write': True, 'scm_update': True, 'use': True, 'execute': True} - - tls = threading.local() # thread local storage @contextlib.contextmanager @@ -65,9 +59,11 @@ def batch_role_ancestor_rebuilding(allow_nesting=False): if not batch_role_rebuilding: rebuild_set = getattr(tls, 'roles_needing_rebuilding') with transaction.atomic(): - for role in Role.objects.filter(id__in=list(rebuild_set)).all(): - # TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this - role.rebuild_role_ancestor_list() + Role._simultaneous_ancestry_rebuild(list(rebuild_set)) + + #for role in Role.objects.filter(id__in=list(rebuild_set)).all(): + # # TODO: We can reduce this to one rebuild call with our new upcoming rebuild method.. do this + # role.rebuild_role_ancestor_list() delattr(tls, 'roles_needing_rebuilding') @@ -82,9 +78,15 @@ class Role(CommonModelNameNotUnique): db_table = 'main_rbac_roles' singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True) + role_field = models.TextField(null=False, default='') parents = models.ManyToManyField('Role', related_name='children') implicit_parents = models.TextField(null=False, default='[]') - ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list` + ancestors = models.ManyToManyField( + 'Role', + through='RoleAncestorEntry', + through_fields=('descendent', 'ancestor'), + related_name='descendents' + ) # auto-generated by `rebuild_role_ancestor_list` members = models.ManyToManyField('auth.User', related_name='roles') content_type = models.ForeignKey(ContentType, null=True, default=None) object_id = models.PositiveIntegerField(null=True, default=None) @@ -97,6 +99,18 @@ class Role(CommonModelNameNotUnique): def get_absolute_url(self): return reverse('api:role_detail', args=(self.pk,)) + def __contains__(self, accessor): + if type(accessor) == User: + return self.ancestors.filter(members=accessor).exists() + elif accessor.__class__.__name__ == 'Team': + return self.ancestors.filter(pk=accessor.member_role.id).exists() + elif type(accessor) == Role: + return self.ancestors.filter(pk=accessor).exists() + else: + accessor_type = ContentType.objects.get_for_model(accessor) + roles = Role.objects.filter(content_type__pk=accessor_type.id, + object_id=accessor.id) + return self.ancestors.filter(pk__in=roles).exists() def rebuild_role_ancestor_list(self): ''' @@ -115,21 +129,191 @@ class Role(CommonModelNameNotUnique): roles_needing_rebuilding.add(self.id) return - actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True)) - actual_ancestors.add(self.id) - if None in actual_ancestors: - actual_ancestors.remove(None) - stored_ancestors = set(self.ancestors.all().values_list('id', flat=True)) + Role._simultaneous_ancestry_rebuild([self.id]) + + + @staticmethod + def _simultaneous_ancestry_rebuild(role_ids_to_rebuild): + # + # The simple version of what this function is doing + # ================================================= + # + # When something changes in our role "hierarchy", we need to update + # the `Role.ancestors` mapping to reflect these changes. The basic + # idea, which the code in this method is modeled after, is to do + # this: When a change happens to a role's parents list, we update + # that role's ancestry list, then we recursively update any child + # roles ancestry lists. Because our role relationships are not + # strictly hierarchical, and can even have loops, this process may + # necessarily visit the same nodes more than once. To handle this + # without having to keep track of what should be updated (again) and + # in what order, we simply use the termination condition of stopping + # when our stored ancestry list matches what our list should be, eg, + # when nothing changes. This can be simply implemented: + # + # if actual_ancestors != stored_ancestors: + # for id in actual_ancestors - stored_ancestors: + # self.ancestors.add(id) + # for id in stored_ancestors - actual_ancestors: + # self.ancestors.remove(id) + # + # for child in self.children.all(): + # child.rebuild_role_ancestor_list() + # + # However this results in a lot of calls to the database, so the + # optimized implementation below effectively does this same thing, + # but we update all children at once, so effectively we sweep down + # through our hierarchy one layer at a time instead of one node at a + # time. Because of how this method works, we can also start from many + # roots at once and sweep down a large set of roles, which we take + # advantage of when performing bulk operations. + # + # + # SQL Breakdown + # ============= + # The Role ancestors has three columns, (id, from_role_id, to_role_id) + # + # id: Unqiue row ID + # from_role_id: Descendent role ID + # to_role_id: Ancestor role ID + # + # *NOTE* In addition to mapping roles to parents, there also + # always exists must exist an entry where + # + # from_role_id == role_id == to_role_id + # + # this makes our joins simple when we go to derive permissions or + # accessible objects. + # + # + # We operate under the assumption that our parent's ancestor list is + # correct, thus we can always compute what our ancestor list should + # be by taking the union of our parent's ancestor lists and adding + # our self reference entry from_role_id == role_id == to_role_id + # + # The inner query for the two SQL statements compute this union, + # the union of the parent's ancestors and the self referncing entry, + # for all roles in the current set of roles to rebuild. + # + # The DELETE query uses this to select all entries on disk for the + # roles we're dealing with, and removes the entries that are not in + # this list. + # + # The INSERT query uses this to select all entries in the list that + # are not in the database yet, and inserts all of the missing + # records. + # + # Once complete, we select all of the children for the roles we are + # working with, this list becomes the new role list we are working + # with. + # + # When our delete or insert query return that they have not performed + # any work, then we know that our children will also not need to be + # updated, and so we can terminate our loop. + # + # + + if len(role_ids_to_rebuild) == 0: + return + + cursor = connection.cursor() + loop_ct = 0 + + sql_params = { + 'ancestors_table': Role.ancestors.through._meta.db_table, + 'parents_table': Role.parents.through._meta.db_table, + 'roles_table': Role._meta.db_table, + } + + + def split_ids_for_sqlite(role_ids): + for i in xrange(0, len(role_ids), 999): + yield role_ids[i:i + 999] + + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE ancestor_id IN (%(ids)s) + ''' % sql_params) + + + while role_ids_to_rebuild: + if loop_ct > 1000: + raise Exception('Ancestry role rebuilding error: infinite loop detected') + loop_ct += 1 + + delete_ct = 0 + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + cursor.execute(''' + DELETE FROM %(ancestors_table)s + WHERE descendent_id IN (%(ids)s) + AND + id NOT IN ( + SELECT %(ancestors_table)s.id FROM ( + SELECT parents.from_role_id from_id, ancestors.ancestor_id to_id + FROM %(parents_table)s as parents + LEFT JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.descendent_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL + + UNION + + SELECT id from_id, id to_id from %(roles_table)s WHERE id IN (%(ids)s) + ) new_ancestry_list + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id + AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) + WHERE %(ancestors_table)s.id IS NOT NULL + ) + ''' % sql_params) + delete_ct += cursor.rowcount + + insert_ct = 0 + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + cursor.execute(''' + INSERT INTO %(ancestors_table)s (descendent_id, ancestor_id, role_field, content_type_id, object_id) + SELECT from_id, to_id, new_ancestry_list.role_field, new_ancestry_list.content_type_id, new_ancestry_list.object_id FROM ( + SELECT parents.from_role_id from_id, + ancestors.ancestor_id to_id, + roles.role_field, + COALESCE(roles.content_type_id, 0) content_type_id, + COALESCE(roles.object_id, 0) object_id + FROM %(parents_table)s as parents + INNER JOIN %(roles_table)s as roles ON (parents.from_role_id = roles.id) + LEFT OUTER JOIN %(ancestors_table)s as ancestors + ON (parents.to_role_id = ancestors.descendent_id) + WHERE parents.from_role_id IN (%(ids)s) AND ancestors.ancestor_id IS NOT NULL + + UNION + + SELECT id from_id, + id to_id, + role_field, + COALESCE(content_type_id, 0) content_type_id, + COALESCE(object_id, 0) object_id + from %(roles_table)s WHERE id IN (%(ids)s) + ) new_ancestry_list + LEFT JOIN %(ancestors_table)s ON (new_ancestry_list.from_id = %(ancestors_table)s.descendent_id + AND new_ancestry_list.to_id = %(ancestors_table)s.ancestor_id) + WHERE %(ancestors_table)s.id IS NULL + ''' % sql_params) + insert_ct += cursor.rowcount + + if insert_ct == 0 and delete_ct == 0: + break + + new_role_ids_to_rebuild = set() + for ids in split_ids_for_sqlite(role_ids_to_rebuild): + sql_params['ids'] = ','.join(str(x) for x in ids) + new_role_ids_to_rebuild.update(set(Role.objects.distinct() + .filter(id__in=ids, children__id__isnull=False) + .values_list('children__id', flat=True))) + role_ids_to_rebuild = list(new_role_ids_to_rebuild) + - # If it differs, update, and then update all of our children - if actual_ancestors != stored_ancestors: - for id in actual_ancestors - stored_ancestors: - self.ancestors.add(id) - for id in stored_ancestors - actual_ancestors: - self.ancestors.remove(id) - for child in self.children.all(): - child.rebuild_role_ancestor_list() @staticmethod def visible_roles(user): @@ -143,109 +327,48 @@ class Role(CommonModelNameNotUnique): def is_ancestor_of(self, role): return role.ancestors.filter(id=self.id).exists() - -class RolePermission(CreatedModifiedModel): - ''' - Defines the permissions a role has - ''' +class RoleAncestorEntry(models.Model): class Meta: app_label = 'main' - verbose_name_plural = _('permissions') - db_table = 'main_rbac_permissions' + verbose_name_plural = _('role_ancestors') + db_table = 'main_rbac_role_ancestors' index_together = [ - ('content_type', 'object_id') + ("ancestor", "content_type_id", "object_id"), # used by get_roles_on_resource + ("ancestor", "content_type_id", "role_field"), # used by accessible_objects ] - role = models.ForeignKey( - Role, - null=False, - on_delete=models.CASCADE, - related_name='permissions', - ) - content_type = models.ForeignKey(ContentType, null=False, default=None) - object_id = models.PositiveIntegerField(null=False, default=None) - resource = GenericForeignKey('content_type', 'object_id') - auto_generated = models.BooleanField(default=False) - - create = models.IntegerField(default = 0) - read = models.IntegerField(default = 0) - write = models.IntegerField(default = 0) - delete = models.IntegerField(default = 0) - update = models.IntegerField(default = 0) - execute = models.IntegerField(default = 0) - scm_update = models.IntegerField(default = 0) - use = models.IntegerField(default = 0) + descendent = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') + ancestor = models.ForeignKey(Role, null=False, on_delete=models.CASCADE, related_name='+') + role_field = models.TextField(null=False) + #content_type_id = models.PositiveIntegerField(null=False) + #object_id = models.PositiveIntegerField(null=False) + content_type_id = models.PositiveIntegerField(null=False) + object_id = models.PositiveIntegerField(null=False) - -def get_user_permissions_on_resource(resource, user): +def get_roles_on_resource(resource, accessor): ''' - Returns a dict (or None) of the permissions a user has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to the roles that are applicable for the given - user. - - In example, if a user has been granted read access through a permission - on one role and write access through a permission on a separate role, - the returned dict will denote that the user has both read and write - access. + Returns a dict (or None) of the roles a accessor has for a given resource. + An accessor can be either a User, Role, or an arbitrary resource that + contains one or more Roles associated with it. ''' - if type(user) == User: - roles = user.roles.all() + if type(accessor) == User: + roles = accessor.roles.all() + elif type(accessor) == Role: + roles = [accessor] else: - accessor_type = ContentType.objects.get_for_model(user) + accessor_type = ContentType.objects.get_for_model(accessor) roles = Role.objects.filter(content_type__pk=accessor_type.id, - object_id=user.id) + object_id=accessor.id) - qs = RolePermission.objects.filter( - content_type=ContentType.objects.get_for_model(resource), - object_id=resource.id, - role__ancestors__in=roles, - ) + return { + role_field: True for role_field in + RoleAncestorEntry.objects.filter( + ancestor__in=roles, + content_type_id=ContentType.objects.get_for_model(resource).id, + object_id=resource.id + ).values_list('role_field', flat=True) + } - res = qs = qs.aggregate( - create = Max('create'), - read = Max('read'), - write = Max('write'), - update = Max('update'), - delete = Max('delete'), - scm_update = Max('scm_update'), - execute = Max('execute'), - use = Max('use') - ) - if res['read'] is None: - return None - return res - -def get_role_permissions_on_resource(resource, role): - ''' - Returns a dict (or None) of the permissions a role has for a given - resource. - - Note: Each field in the dict is the `or` of all respective permissions - that have been granted to either the role or any descendents of that role. - ''' - - qs = RolePermission.objects.filter( - content_type=ContentType.objects.get_for_model(resource), - object_id=resource.id, - role__ancestors=role - ) - - res = qs = qs.aggregate( - create = Max('create'), - read = Max('read'), - write = Max('write'), - update = Max('update'), - delete = Max('delete'), - scm_update = Max('scm_update'), - execute = Max('execute'), - use = Max('use') - ) - if res['read'] is None: - return None - return res diff --git a/awx/main/signals.py b/awx/main/signals.py index 891c60b75b..e4893d34c2 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -106,13 +106,14 @@ def emit_update_inventory_on_created_or_deleted(sender, **kwargs): if inventory is not None: update_inventory_computed_fields.delay(inventory.id, True) -def rebuild_role_ancestor_list(reverse, model, instance, pk_set, **kwargs): +def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwargs): 'When a role parent is added or removed, update our role hierarchy list' - if reverse: - for id in pk_set: - model.objects.get(id=id).rebuild_role_ancestor_list() - else: - instance.rebuild_role_ancestor_list() + if action in ['post_add', 'post_remove', 'post_clear']: + if reverse: + for id in pk_set: + model.objects.get(id=id).rebuild_role_ancestor_list() + else: + instance.rebuild_role_ancestor_list() def sync_superuser_status_to_rbac(instance, **kwargs): 'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role' @@ -126,21 +127,15 @@ def create_user_role(instance, **kwargs): Role.objects.get( content_type=ContentType.objects.get_for_model(instance), object_id=instance.id, - name = 'Owner' + name = 'User Admin' ) except Role.DoesNotExist: role = Role.objects.create( - name = 'Owner', + name = 'User Admin', + role_field='admin_role', content_object = instance, ) role.members.add(instance) - RolePermission.objects.create( - role = role, - resource = instance, - auto_generated = True, - create=1, read=1, write=1, delete=1, update=1, - execute=1, scm_update=1, use=1, - ) def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): content_type = ContentType.objects.get_for_model(Organization) @@ -157,101 +152,6 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): if action == 'pre_remove': instance.content_object.admin_role.children.remove(user.admin_role) -def grant_host_access_to_group_roles(instance, action, model, reverse, pk_set, **kwargs): - 'Add/remove RolePermission entries for Group roles that contain this host' - - if action == 'post_add': - def grant(host, group): - RolePermission.objects.create( - resource=host, - role=group.admin_role, - auto_generated=True, - create=1, - read=1, write=1, - delete=1, - update=1, - execute=1, - scm_update=1, - use=1, - ) - RolePermission.objects.create( - resource=host, - role=group.auditor_role, - auto_generated=True, - read=1, - ) - RolePermission.objects.create( - resource=host, - role=group.updater_role, - auto_generated=True, - read=1, - write=1, - create=1, - use=1 - ) - RolePermission.objects.create( - resource=host, - role=group.executor_role, - auto_generated=True, - read=1, - execute=1 - ) - - if reverse: - host = instance - for group_id in pk_set: - grant(host, Group.objects.get(id=group_id)) - else: - group = instance - for host_id in pk_set: - grant(Host.objects.get(id=host_id), group) - - if action == 'pre_remove': - host_content_type = ContentType.objects.get_for_model(Host) - - def remove_grant(host, group): - RolePermission.objects.filter( - content_type = host_content_type, - object_id = host.id, - auto_generated = True, - role__in = [group.admin_role, group.updater_role, group.auditor_role, group.executor_role] - ).delete() - - if reverse: - host = instance - for group_id in pk_set: - remove_grant(host, Group.objects.get(id=group_id)) - else: - group = instance - for host_id in pk_set: - remove_grant(Host.objects.get(id=host_id), group) - - -def grant_host_access_to_inventory(instance, **kwargs): - 'Add/remove RolePermission entries for the Inventory that contains this host' - host_content_type = ContentType.objects.get_for_model(Host) - inventory_content_type = ContentType.objects.get_for_model(Inventory) - - # Clear out any existing perms.. in case we switched inventory or something - qs = RolePermission.objects.filter( - content_type=host_content_type, - object_id=instance.id, - auto_generated=True, - role__content_type=inventory_content_type - ) - if qs.count() == 1 and qs[0].role.object_id == instance.inventory.id: - # No change - return - qs.delete() - - RolePermission.objects.create( - resource=instance, - role=instance.inventory.admin_role, - auto_generated=True, - create=1, read=1, write=1, delete=1, update=1, - execute=1, scm_update=1, use=1, - ) - post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host) post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host) @@ -269,8 +169,6 @@ post_save.connect(emit_job_event_detail, sender=JobEvent) post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent) m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through) m2m_changed.connect(org_admin_edit_members, Role.members.through) -m2m_changed.connect(grant_host_access_to_group_roles, Group.hosts.through) -post_save.connect(grant_host_access_to_inventory, Host) post_save.connect(sync_superuser_status_to_rbac, sender=User) post_save.connect(create_user_role, sender=User) diff --git a/awx/main/south_migrations/0002_v12b2_changes.py b/awx/main/south_migrations/0002_v12b2_changes.py index 06fd7bd219..da4992b514 100644 --- a/awx/main/south_migrations/0002_v12b2_changes.py +++ b/awx/main/south_migrations/0002_v12b2_changes.py @@ -661,4 +661,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0004_v12b2_changes.py b/awx/main/south_migrations/0004_v12b2_changes.py index f8984566dc..796f3a3314 100644 --- a/awx/main/south_migrations/0004_v12b2_changes.py +++ b/awx/main/south_migrations/0004_v12b2_changes.py @@ -340,4 +340,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0005_v12b2_changes.py b/awx/main/south_migrations/0005_v12b2_changes.py index 7b136aa916..6a451eec97 100644 --- a/awx/main/south_migrations/0005_v12b2_changes.py +++ b/awx/main/south_migrations/0005_v12b2_changes.py @@ -270,4 +270,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0006_v12b2_changes.py b/awx/main/south_migrations/0006_v12b2_changes.py index 8193359d54..9440d54b72 100644 --- a/awx/main/south_migrations/0006_v12b2_changes.py +++ b/awx/main/south_migrations/0006_v12b2_changes.py @@ -255,4 +255,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0007_v12b2_changes.py b/awx/main/south_migrations/0007_v12b2_changes.py index 1a08e77746..2894db993a 100644 --- a/awx/main/south_migrations/0007_v12b2_changes.py +++ b/awx/main/south_migrations/0007_v12b2_changes.py @@ -268,4 +268,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0008_v12changes.py b/awx/main/south_migrations/0008_v12changes.py index c8be2a290e..ea98d42f3e 100644 --- a/awx/main/south_migrations/0008_v12changes.py +++ b/awx/main/south_migrations/0008_v12changes.py @@ -257,4 +257,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0009_v13_changes.py b/awx/main/south_migrations/0009_v13_changes.py index 9fdf67f616..51db04032b 100644 --- a/awx/main/south_migrations/0009_v13_changes.py +++ b/awx/main/south_migrations/0009_v13_changes.py @@ -405,4 +405,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0010_v13_changes.py b/awx/main/south_migrations/0010_v13_changes.py index f57569f820..239852d336 100644 --- a/awx/main/south_migrations/0010_v13_changes.py +++ b/awx/main/south_migrations/0010_v13_changes.py @@ -518,4 +518,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0012_v13_changes.py b/awx/main/south_migrations/0012_v13_changes.py index eb3bf704a4..1826d17d8e 100644 --- a/awx/main/south_migrations/0012_v13_changes.py +++ b/awx/main/south_migrations/0012_v13_changes.py @@ -327,4 +327,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0013_v13_changes.py b/awx/main/south_migrations/0013_v13_changes.py index 723416eba6..0fe5bddf26 100644 --- a/awx/main/south_migrations/0013_v13_changes.py +++ b/awx/main/south_migrations/0013_v13_changes.py @@ -331,4 +331,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0014_v13_changes.py b/awx/main/south_migrations/0014_v13_changes.py index f706a6938d..4416539998 100644 --- a/awx/main/south_migrations/0014_v13_changes.py +++ b/awx/main/south_migrations/0014_v13_changes.py @@ -336,4 +336,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0015_v14_changes.py b/awx/main/south_migrations/0015_v14_changes.py index 042a79a3a9..bcb8dd06ea 100644 --- a/awx/main/south_migrations/0015_v14_changes.py +++ b/awx/main/south_migrations/0015_v14_changes.py @@ -335,4 +335,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0016_v14_changes.py b/awx/main/south_migrations/0016_v14_changes.py index b480df3a30..eae2f42094 100644 --- a/awx/main/south_migrations/0016_v14_changes.py +++ b/awx/main/south_migrations/0016_v14_changes.py @@ -427,4 +427,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0017_v14_changes.py b/awx/main/south_migrations/0017_v14_changes.py index 3593f369fa..af1c4821e4 100644 --- a/awx/main/south_migrations/0017_v14_changes.py +++ b/awx/main/south_migrations/0017_v14_changes.py @@ -388,4 +388,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0019_v14_changes.py b/awx/main/south_migrations/0019_v14_changes.py index a541e998d0..20932fbf8f 100644 --- a/awx/main/south_migrations/0019_v14_changes.py +++ b/awx/main/south_migrations/0019_v14_changes.py @@ -497,4 +497,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0020_v14_changes.py b/awx/main/south_migrations/0020_v14_changes.py index d561dc8d1a..829870b256 100644 --- a/awx/main/south_migrations/0020_v14_changes.py +++ b/awx/main/south_migrations/0020_v14_changes.py @@ -451,4 +451,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0021_v14_changes.py b/awx/main/south_migrations/0021_v14_changes.py index e8678f652a..1d2b4c02e4 100644 --- a/awx/main/south_migrations/0021_v14_changes.py +++ b/awx/main/south_migrations/0021_v14_changes.py @@ -501,4 +501,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0023_v14_changes.py b/awx/main/south_migrations/0023_v14_changes.py index 0173c0f9c6..cf0425adff 100644 --- a/awx/main/south_migrations/0023_v14_changes.py +++ b/awx/main/south_migrations/0023_v14_changes.py @@ -464,4 +464,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0024_v14_changes.py b/awx/main/south_migrations/0024_v14_changes.py index 25d5a58fb1..596dbb5f52 100644 --- a/awx/main/south_migrations/0024_v14_changes.py +++ b/awx/main/south_migrations/0024_v14_changes.py @@ -395,4 +395,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0026_v14_changes.py b/awx/main/south_migrations/0026_v14_changes.py index 54fdad9e30..cbf6351ec0 100644 --- a/awx/main/south_migrations/0026_v14_changes.py +++ b/awx/main/south_migrations/0026_v14_changes.py @@ -417,4 +417,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py b/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py index ba67bd835f..a963531fd0 100644 --- a/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py +++ b/awx/main/south_migrations/0033_auto__chg_field_jobevent_created__chg_field_jobevent_modified.py @@ -427,4 +427,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0034_v148_changes.py b/awx/main/south_migrations/0034_v148_changes.py index be0d8f88ed..8a6e982c4d 100644 --- a/awx/main/south_migrations/0034_v148_changes.py +++ b/awx/main/south_migrations/0034_v148_changes.py @@ -442,4 +442,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0035_v148_changes.py b/awx/main/south_migrations/0035_v148_changes.py index 354b7b166f..0b0fd4bb04 100644 --- a/awx/main/south_migrations/0035_v148_changes.py +++ b/awx/main/south_migrations/0035_v148_changes.py @@ -1408,4 +1408,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0037_v148_changes.py b/awx/main/south_migrations/0037_v148_changes.py index 587f471430..36fff3cb02 100644 --- a/awx/main/south_migrations/0037_v148_changes.py +++ b/awx/main/south_migrations/0037_v148_changes.py @@ -792,4 +792,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0038_v148_changes.py b/awx/main/south_migrations/0038_v148_changes.py index 4c1d877d7e..d172989417 100644 --- a/awx/main/south_migrations/0038_v148_changes.py +++ b/awx/main/south_migrations/0038_v148_changes.py @@ -497,4 +497,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0039_v148_changes.py b/awx/main/south_migrations/0039_v148_changes.py index 2fdaf3800a..977f69df69 100644 --- a/awx/main/south_migrations/0039_v148_changes.py +++ b/awx/main/south_migrations/0039_v148_changes.py @@ -449,4 +449,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0043_v1411_changes.py b/awx/main/south_migrations/0043_v1411_changes.py index 8abe29675a..1233cb28b2 100644 --- a/awx/main/south_migrations/0043_v1411_changes.py +++ b/awx/main/south_migrations/0043_v1411_changes.py @@ -436,4 +436,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0046_v200_changes.py b/awx/main/south_migrations/0046_v200_changes.py index 17b38fed54..71272ae571 100644 --- a/awx/main/south_migrations/0046_v200_changes.py +++ b/awx/main/south_migrations/0046_v200_changes.py @@ -437,4 +437,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0051_v200_changes.py b/awx/main/south_migrations/0051_v200_changes.py index 268e075769..5d948183fe 100644 --- a/awx/main/south_migrations/0051_v200_changes.py +++ b/awx/main/south_migrations/0051_v200_changes.py @@ -440,4 +440,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0053_v210_changes.py b/awx/main/south_migrations/0053_v210_changes.py index 43b6c52de9..6bcdc9e609 100644 --- a/awx/main/south_migrations/0053_v210_changes.py +++ b/awx/main/south_migrations/0053_v210_changes.py @@ -450,4 +450,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0054_v210_changes.py b/awx/main/south_migrations/0054_v210_changes.py index 762515e3c5..84a63c8222 100644 --- a/awx/main/south_migrations/0054_v210_changes.py +++ b/awx/main/south_migrations/0054_v210_changes.py @@ -493,4 +493,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0061_v210_changes.py b/awx/main/south_migrations/0061_v210_changes.py index 26573c9e05..2553a29906 100644 --- a/awx/main/south_migrations/0061_v210_changes.py +++ b/awx/main/south_migrations/0061_v210_changes.py @@ -510,4 +510,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0064_v220_changes.py b/awx/main/south_migrations/0064_v220_changes.py index a58c74ac6b..a94f867266 100644 --- a/awx/main/south_migrations/0064_v220_changes.py +++ b/awx/main/south_migrations/0064_v220_changes.py @@ -572,4 +572,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0072_v240_changes.py b/awx/main/south_migrations/0072_v240_changes.py index d6d30695d5..721b453087 100644 --- a/awx/main/south_migrations/0072_v240_changes.py +++ b/awx/main/south_migrations/0072_v240_changes.py @@ -519,4 +519,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/south_migrations/0073_v240_changes.py b/awx/main/south_migrations/0073_v240_changes.py index 3c844bc09b..743aaee279 100644 --- a/awx/main/south_migrations/0073_v240_changes.py +++ b/awx/main/south_migrations/0073_v240_changes.py @@ -520,4 +520,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['main'] \ No newline at end of file + complete_apps = ['main'] diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index cc417111db..470e43423f 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -3,6 +3,7 @@ import yaml from awx.api.serializers import JobLaunchSerializer from awx.main.models.credential import Credential +from awx.main.models.inventory import Inventory from awx.main.models.jobs import Job, JobTemplate from django.core.urlresolvers import reverse @@ -93,8 +94,7 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user job_template = job_template_prompts(True) admin_user = user('admin', True) - job_template.inventory.executor_role.members.add(admin_user) - job_template.inventory.save() + job_template.inventory.execute_role.members.add(admin_user) response = post(reverse('api:job_template_launch', args=[job_template.pk]), runtime_data, admin_user) @@ -112,20 +112,19 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user assert job_obj.job_tags == runtime_data['job_tags'] @pytest.mark.django_db -@pytest.mark.skip(reason="JT can_start without inventory needs to be fixed before passing") @pytest.mark.job_runtime_vars def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, post, user): job_template = job_template_prompts_null - common_user = user('admin', False) + common_user = user('not-admin', False) - job_template.executor_role.members.add(common_user) - job_template.save() - job_template.project.member_role.members.add(common_user) - job_template.project.save() + # Give user permission to execute the job template + job_template.execute_role.members.add(common_user) + # Give user permission to use inventory and credential at runtime credential = Credential.objects.get(pk=runtime_data['credential']) - credential.usage_role.members.add(common_user) - credential.save() + credential.use_role.members.add(common_user) + inventory = Inventory.objects.get(pk=runtime_data['inventory']) + inventory.use_role.members.add(common_user) response = post(reverse('api:job_template_launch', args=[job_template.pk]), runtime_data, common_user) @@ -186,15 +185,12 @@ def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, user): def test_job_launch_fails_without_inventory_access(deploy_jobtemplate, machine_credential, post, user): deploy_jobtemplate.ask_inventory_on_launch = True deploy_jobtemplate.credential = machine_credential - common_user = user('test-user', False) - deploy_jobtemplate.executor_role.members.add(common_user) deploy_jobtemplate.save() - deploy_jobtemplate.inventory.usage_role.members.add(common_user) - deploy_jobtemplate.inventory.save() + common_user = user('test-user', False) + deploy_jobtemplate.execute_role.members.add(common_user) + deploy_jobtemplate.inventory.use_role.members.add(common_user) deploy_jobtemplate.project.member_role.members.add(common_user) - deploy_jobtemplate.project.save() - deploy_jobtemplate.credential.usage_role.members.add(common_user) - deploy_jobtemplate.credential.save() + deploy_jobtemplate.credential.use_role.members.add(common_user) # Assure that the base job template can be launched to begin with response = post(reverse('api:job_template_launch', diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index 550068da63..5aaef06b3d 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -3,72 +3,118 @@ import pytest from django.core.urlresolvers import reverse @pytest.fixture -def resourced_organization(organization, project, team, inventory, user): - admin_user = user('test-admin', True) - member_user = user('org-member') +def organization_resource_creator(organization, user): + def rf(users, admins, job_templates, projects, inventories, teams): - # Associate one resource of every type with the organization - organization.member_role.members.add(member_user) - organization.admin_role.members.add(admin_user) - # organization.teams.create(name='org-team') - # inventory = organization.inventories.create(name="associated-inv") - project.jobtemplates.create(name="test-jt", - description="test-job-template-desc", - inventory=inventory, - playbook="test_playbook.yml") + # Associate one resource of every type with the organization + for i in range(users): + member_user = user('org-member %s' % i) + organization.member_role.members.add(member_user) + for i in range(admins): + admin_user = user('org-admin %s' % i) + organization.admin_role.members.add(admin_user) + for i in range(teams): + organization.teams.create(name='org-team %s' % i) + for i in range(inventories): + inventory = organization.inventories.create(name="associated-inv %s" % i) + for i in range(projects): + organization.projects.create(name="test-proj %s" % i, + description="test-proj-desc", + scm_type="git", + scm_url="https://github.com/jlaska/ansible-playbooks") + # Mix up the inventories and projects used by the job templates + i_proj = 0 + i_inv = 0 + for i in range(job_templates): + project = organization.projects.all()[i_proj] + inventory = organization.inventories.all()[i_inv] + project.jobtemplates.create(name="test-jt %s" % i, + description="test-job-template-desc", + inventory=inventory, + playbook="test_playbook.yml") + i_proj += 1 + i_inv += 1 + if i_proj >= organization.projects.count(): + i_proj = 0 + if i_inv >= organization.inventories.count(): + i_inv = 0 - return organization + return organization + return rf +COUNTS_PRIMES = { + 'users': 11, + 'admins': 5, + 'job_templates': 3, + 'projects': 3, + 'inventories': 7, + 'teams': 5 +} +COUNTS_ZEROS = { + 'users': 0, + 'admins': 0, + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 +} + +@pytest.fixture +def resourced_organization(organization_resource_creator): + return organization_resource_creator(**COUNTS_PRIMES) @pytest.mark.django_db -def test_org_counts_detail_view(resourced_organization, user, get): +def test_org_counts_detail_admin(resourced_organization, user, get): # Check that all types of resources are counted by a superuser external_admin = user('admin', True) response = get(reverse('api:organization_detail', args=[resourced_organization.pk]), external_admin) assert response.status_code == 200 + counts = response.data['summary_fields']['related_field_counts'] + assert counts == COUNTS_PRIMES + +@pytest.mark.django_db +def test_org_counts_detail_member(resourced_organization, user, get): + # Check that a non-admin org member can only see users / admin in detail view + member_user = resourced_organization.member_role.members.get(username='org-member 1') + response = get(reverse('api:organization_detail', + args=[resourced_organization.pk]), member_user) + assert response.status_code == 200 + counts = response.data['summary_fields']['related_field_counts'] assert counts == { - 'users': 1, - 'admins': 1, - 'job_templates': 1, - 'projects': 1, - 'inventories': 1, - 'teams': 1 + 'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins + 'admins': COUNTS_PRIMES['admins'], + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 } @pytest.mark.django_db -@pytest.mark.skipif("True") # XXX: This needs to be implemented -def test_org_counts_admin(resourced_organization, user, get): +def test_org_counts_list_admin(resourced_organization, user, get): # Check that all types of resources are counted by a superuser external_admin = user('admin', True) response = get(reverse('api:organization_list', args=[]), external_admin) assert response.status_code == 200 counts = response.data['results'][0]['summary_fields']['related_field_counts'] - assert counts == { - 'users': 1, - 'admins': 1, - 'job_templates': 1, - 'projects': 1, - 'inventories': 1, - 'teams': 1 - } + assert counts == COUNTS_PRIMES @pytest.mark.django_db -def test_org_counts_member(resourced_organization, get): +def test_org_counts_list_member(resourced_organization, user, get): # Check that a non-admin user can only see the full project and # user count, consistent with the RBAC rules - member_user = resourced_organization.member_role.members.get(username='org-member') + member_user = resourced_organization.member_role.members.get(username='org-member 1') response = get(reverse('api:organization_list', args=[]), member_user) assert response.status_code == 200 counts = response.data['results'][0]['summary_fields']['related_field_counts'] assert counts == { - 'users': 1, # Policy is that members can see other users and admins - 'admins': 1, + 'users': COUNTS_PRIMES['users'], # Policy is that members can see other users and admins + 'admins': COUNTS_PRIMES['admins'], 'job_templates': 0, 'projects': 0, 'inventories': 0, @@ -86,17 +132,9 @@ def test_new_org_zero_counts(user, post): new_org_list = post_response.render().data counts_dict = new_org_list['summary_fields']['related_field_counts'] - assert counts_dict == { - 'users': 0, - 'admins': 0, - 'job_templates': 0, - 'projects': 0, - 'inventories': 0, - 'teams': 0 - } + assert counts_dict == COUNTS_ZEROS @pytest.mark.django_db -@pytest.mark.skipif("True") # XXX: This needs to be implemented def test_two_organizations(resourced_organization, organizations, user, get): # Check correct results for two organizations are returned external_admin = user('admin', True) @@ -111,26 +149,10 @@ def test_two_organizations(resourced_organization, organizations, user, get): org_id = response.data['results'][i]['id'] counts[org_id] = response.data['results'][i]['summary_fields']['related_field_counts'] - assert counts[org_id_full] == { - 'users': 1, - 'admins': 1, - 'job_templates': 1, - 'projects': 1, - 'inventories': 1, - 'teams': 1 - } - assert counts[org_id_zero] == { - 'users': 0, - 'admins': 0, - 'job_templates': 0, - 'projects': 0, - 'inventories': 0, - 'teams': 0 - } + assert counts[org_id_full] == COUNTS_PRIMES + assert counts[org_id_zero] == COUNTS_ZEROS -@pytest.mark.skip(reason="resolution planned for after RBAC merge") @pytest.mark.django_db -@pytest.mark.skipif("True") # XXX: This needs to be implemented def test_JT_associated_with_project(organizations, project, user, get): # Check that adding a project to an organization gets the project's JT # included in the organization's JT count @@ -163,4 +185,3 @@ def test_JT_associated_with_project(organizations, project, user, get): 'inventories': 0, 'teams': 0 } - diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index cfed6ddfcf..aaf0802791 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -11,9 +11,11 @@ from awx.main.models import Project # @pytest.mark.django_db(transaction=True) -def test_user_project_list(get, project_factory, admin, alice, bob): +def test_user_project_list(get, project_factory, organization, admin, alice, bob): 'List of projects a user has access to, filtered by projects you can also see' + organization.member_role.members.add(alice, bob) + alice_project = project_factory('alice project') alice_project.admin_role.members.add(alice) diff --git a/awx/main/tests/functional/test_rbac_api.py b/awx/main/tests/functional/test_rbac_api.py index 6200a46289..1fc1485e14 100644 --- a/awx/main/tests/functional/test_rbac_api.py +++ b/awx/main/tests/functional/test_rbac_api.py @@ -176,8 +176,9 @@ def test_get_teams_roles_list(get, team, organization, admin): response = get(url, admin) assert response.status_code == 200 roles = response.data - assert roles['count'] == 1 - assert roles['results'][0]['id'] == organization.admin_role.id + + assert roles['count'] == 2 + assert roles['results'][0]['id'] == organization.admin_role.id or roles['results'][1]['id'] == organization.admin_role.id @pytest.mark.django_db @@ -272,13 +273,11 @@ def test_org_admin_add_user_to_job_template(post, organization, check_jobtemplat joe = user('joe') organization.admin_role.members.add(org_admin) - assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert org_admin in check_jobtemplate.admin_role + assert joe not in check_jobtemplate.execute_role - res =post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, org_admin) - - print(res.data) - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'id': joe.id}, org_admin) + assert joe in check_jobtemplate.execute_role @pytest.mark.django_db(transaction=True) @@ -287,14 +286,14 @@ def test_org_admin_remove_user_to_job_template(post, organization, check_jobtemp org_admin = user('org-admin') joe = user('joe') organization.admin_role.members.add(org_admin) - check_jobtemplate.executor_role.members.add(joe) + check_jobtemplate.execute_role.members.add(joe) - assert check_jobtemplate.accessible_by(org_admin, {'write': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert org_admin in check_jobtemplate.admin_role + assert joe in check_jobtemplate.execute_role - post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin) + post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, org_admin) + assert joe not in check_jobtemplate.execute_role - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False @pytest.mark.django_db(transaction=True) def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemplate, user): @@ -302,14 +301,13 @@ def test_user_fail_to_add_user_to_job_template(post, organization, check_jobtemp rando = user('rando') joe = user('joe') - assert check_jobtemplate.accessible_by(rando, {'write': True}) is False - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert rando not in check_jobtemplate.admin_role + assert joe not in check_jobtemplate.execute_role - res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'id': joe.id}, rando) - print(res.data) + res = post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'id': joe.id}, rando) assert res.status_code == 403 - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe not in check_jobtemplate.execute_role @pytest.mark.django_db(transaction=True) @@ -317,16 +315,15 @@ def test_user_fail_to_remove_user_to_job_template(post, organization, check_jobt 'Tests that a user without permissions to assign/revoke membership to a particular role cannot do so' rando = user('rando') joe = user('joe') - check_jobtemplate.executor_role.members.add(joe) + check_jobtemplate.execute_role.members.add(joe) - assert check_jobtemplate.accessible_by(rando, {'write': True}) is False - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert rando not in check_jobtemplate.admin_role + assert joe in check_jobtemplate.execute_role - res = post(reverse('api:role_users_list', args=(check_jobtemplate.executor_role.id,)), {'disassociate': True, 'id': joe.id}, rando) + res = post(reverse('api:role_users_list', args=(check_jobtemplate.execute_role.id,)), {'disassociate': True, 'id': joe.id}, rando) assert res.status_code == 403 - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True - + assert joe in check_jobtemplate.execute_role # # /roles//teams/ @@ -384,8 +381,8 @@ def test_role_children(get, team, admin, role): url = reverse('api:role_children_list', args=(team.member_role.id,)) response = get(url, admin) assert response.status_code == 200 - assert response.data['count'] == 1 - assert response.data['results'][0]['id'] == role.id + assert response.data['count'] == 2 + assert response.data['results'][0]['id'] == role.id or response.data['results'][1]['id'] == role.id @@ -421,7 +418,7 @@ def test_ensure_permissions_is_present(organization, get, user): assert 'summary_fields' in org assert 'permissions' in org['summary_fields'] - assert org['summary_fields']['permissions']['read'] > 0 + assert org['summary_fields']['permissions']['read_role'] > 0 @pytest.mark.django_db def test_ensure_role_summary_is_present(organization, get, user): diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 2ad1250f81..c70f221c7d 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -2,7 +2,6 @@ import pytest from awx.main.models import ( Role, - RolePermission, Organization, Project, ) @@ -10,27 +9,25 @@ from awx.main.models import ( @pytest.mark.django_db def test_auto_inheritance_by_children(organization, alice): - A = Role.objects.create(name='A') - B = Role.objects.create(name='B') + A = Role.objects.create(name='A', role_field='') + B = Role.objects.create(name='B', role_field='') A.members.add(alice) - - - assert organization.accessible_by(alice, {'read': True}) is False - assert Organization.accessible_objects(alice, {'read': True}).count() == 0 + assert alice not in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 A.children.add(B) - assert organization.accessible_by(alice, {'read': True}) is False - assert Organization.accessible_objects(alice, {'read': True}).count() == 0 + assert alice not in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 A.children.add(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is True - assert Organization.accessible_objects(alice, {'read': True}).count() == 1 + assert alice in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 1 A.children.remove(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role B.children.add(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.admin_role B.children.remove(organization.admin_role) - assert organization.accessible_by(alice, {'read': True}) is False - assert Organization.accessible_objects(alice, {'read': True}).count() == 0 + assert alice not in organization.admin_role + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 # We've had the case where our pre/post save init handlers in our field descriptors # end up creating a ton of role objects because of various not-so-obvious issues @@ -43,32 +40,19 @@ def test_auto_inheritance_by_parents(organization, alice): B = Role.objects.create(name='B') A.members.add(alice) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role B.parents.add(A) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role organization.admin_role.parents.add(A) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.admin_role organization.admin_role.parents.remove(A) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role organization.admin_role.parents.add(B) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.admin_role organization.admin_role.parents.remove(B) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.admin_role -@pytest.mark.django_db -def test_permission_union(organization, alice): - A = Role.objects.create(name='A') - A.members.add(alice) - B = Role.objects.create(name='B') - B.members.add(alice) - - assert organization.accessible_by(alice, {'read': True, 'write': True}) is False - RolePermission.objects.create(role=A, resource=organization, read=True) - assert organization.accessible_by(alice, {'read': True, 'write': True}) is False - RolePermission.objects.create(role=A, resource=organization, write=True) - assert organization.accessible_by(alice, {'read': True, 'write': True}) is True - @pytest.mark.django_db def test_accessible_objects(organization, alice, bob): @@ -78,57 +62,54 @@ def test_accessible_objects(organization, alice, bob): B.members.add(alice) B.members.add(bob) - assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 0 - RolePermission.objects.create(role=A, resource=organization, read=True) - assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 0 - assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0 - RolePermission.objects.create(role=B, resource=organization, write=True) - assert Organization.accessible_objects(alice, {'read': True, 'write': True}).count() == 1 - assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0 - assert Organization.accessible_objects(bob, {'read': True, 'write': True}).count() == 0 + assert Organization.accessible_objects(alice, 'admin_role').count() == 0 + assert Organization.accessible_objects(bob, 'admin_role').count() == 0 + A.children.add(organization.admin_role) + assert Organization.accessible_objects(alice, 'admin_role').count() == 1 + assert Organization.accessible_objects(bob, 'admin_role').count() == 0 @pytest.mark.django_db def test_team_symantics(organization, team, alice): - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.auditor_role team.member_role.children.add(organization.auditor_role) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.auditor_role team.member_role.members.add(alice) - assert organization.accessible_by(alice, {'read': True}) is True + assert alice in organization.auditor_role team.member_role.members.remove(alice) - assert organization.accessible_by(alice, {'read': True}) is False + assert alice not in organization.auditor_role @pytest.mark.django_db -def test_auto_m2m_adjuments(organization, inventory, group, alice): +def test_auto_m2m_adjustments(organization, inventory, group, alice): 'Ensures the auto role reparenting is working correctly through m2m maps' g1 = group(name='g1') g1.admin_role.members.add(alice) - assert g1.accessible_by(alice, {'read': True}) is True + assert alice in g1.admin_role g2 = group(name='g2') - assert g2.accessible_by(alice, {'read': True}) is False + assert alice not in g2.admin_role g2.parents.add(g1) - assert g2.accessible_by(alice, {'read': True}) is True + assert alice in g2.admin_role g2.parents.remove(g1) - assert g2.accessible_by(alice, {'read': True}) is False + assert alice not in g2.admin_role g1.children.add(g2) - assert g2.accessible_by(alice, {'read': True}) is True + assert alice in g2.admin_role g1.children.remove(g2) - assert g2.accessible_by(alice, {'read': True}) is False + assert alice not in g2.admin_role @pytest.mark.django_db -def test_auto_field_adjuments(organization, inventory, team, alice): +def test_auto_field_adjustments(organization, inventory, team, alice): 'Ensures the auto role reparenting is working correctly through non m2m fields' org2 = Organization.objects.create(name='Org 2', description='org 2') org2.admin_role.members.add(alice) - assert inventory.accessible_by(alice, {'read': True}) is False + assert alice not in inventory.admin_role inventory.organization = org2 inventory.save() - assert inventory.accessible_by(alice, {'read': True}) is True + assert alice in inventory.admin_role inventory.organization = organization inventory.save() - assert inventory.accessible_by(alice, {'read': True}) is False + assert alice not in inventory.admin_role #assert False @pytest.mark.django_db @@ -147,14 +128,12 @@ def test_implicit_deletes(alice): assert Role.objects.filter(id=auditor_role_id).count() == 1 n_alice_roles = alice.roles.count() n_system_admin_children = Role.singleton('System Administrator').children.count() - rp = RolePermission.objects.create(role=delorg.admin_role, resource=delorg, read=True) delorg.delete() assert Role.objects.filter(id=admin_role_id).count() == 0 assert Role.objects.filter(id=auditor_role_id).count() == 0 assert alice.roles.count() == (n_alice_roles - 1) - assert RolePermission.objects.filter(id=rp.id).count() == 0 assert Role.singleton('System Administrator').children.count() == (n_system_admin_children - 1) assert child.ancestors.count() == 1 assert child.ancestors.all()[0] == child @@ -168,8 +147,8 @@ def test_content_object(user): assert org.admin_role.content_object.id == org.id @pytest.mark.django_db -def test_hierarchy_rebuilding(): - 'Tests some subdtle cases around role hierarchy rebuilding' +def test_hierarchy_rebuilding_multi_path(): + 'Tests a subdtle cases around role hierarchy rebuilding when you have multiple paths to the same role of different length' X = Role.objects.create(name='X') A = Role.objects.create(name='A') @@ -196,6 +175,100 @@ def test_hierarchy_rebuilding(): assert X.is_ancestor_of(D) is False +@pytest.mark.django_db +def test_hierarchy_rebuilding_loops1(organization, team): + 'Tests ancestry rebuilding loops are involved' + + + assert team.admin_role.is_ancestor_of(organization.admin_role) is False + assert organization.admin_role.is_ancestor_of(team.admin_role) + + team.admin_role.children.add(organization.admin_role) + + assert team.admin_role.is_ancestor_of(organization.admin_role) + assert organization.admin_role.is_ancestor_of(team.admin_role) + + team.admin_role.children.remove(organization.admin_role) + + assert team.admin_role.is_ancestor_of(organization.admin_role) is False + assert organization.admin_role.is_ancestor_of(team.admin_role) + + team.admin_role.children.add(organization.admin_role) + + + X = Role.objects.create(name='X') + X.children.add(organization.admin_role) + assert X.is_ancestor_of(team.admin_role) + assert X.is_ancestor_of(organization.admin_role) + assert organization.admin_role.is_ancestor_of(X) is False + assert team.admin_role.is_ancestor_of(X) is False + + #print(X.descendents.filter(id=organization.admin_role.id).count()) + #print(X.children.filter(id=organization.admin_role.id).count()) + X.children.remove(organization.admin_role) + X.rebuild_role_ancestor_list() + #print(X.descendents.filter(id=organization.admin_role.id).count()) + #print(X.children.filter(id=organization.admin_role.id).count()) + + assert X.is_ancestor_of(team.admin_role) is False + assert X.is_ancestor_of(organization.admin_role) is False + + + +@pytest.mark.django_db +def test_hierarchy_rebuilding_loops(): + 'Tests ancestry rebuilding loops are involved' + + X = Role.objects.create(name='X') + A = Role.objects.create(name='A') + B = Role.objects.create(name='B') + C = Role.objects.create(name='C') + + A.children.add(B) + B.children.add(C) + C.children.add(A) + X.children.add(A) + + assert X.is_ancestor_of(A) + assert X.is_ancestor_of(B) + assert X.is_ancestor_of(C) + + assert A.is_ancestor_of(B) + assert A.is_ancestor_of(C) + assert B.is_ancestor_of(C) + assert B.is_ancestor_of(A) + assert C.is_ancestor_of(A) + assert C.is_ancestor_of(B) + + X.children.remove(A) + X.rebuild_role_ancestor_list() + + assert X.is_ancestor_of(A) is False + assert X.is_ancestor_of(B) is False + assert X.is_ancestor_of(C) is False + + X.children.add(A) + + assert X.is_ancestor_of(A) + assert X.is_ancestor_of(B) + assert X.is_ancestor_of(C) + + C.children.remove(A) + + assert A.is_ancestor_of(B) + assert A.is_ancestor_of(C) + assert B.is_ancestor_of(C) + assert B.is_ancestor_of(A) is False + assert C.is_ancestor_of(A) is False + assert C.is_ancestor_of(B) is False + + assert X.is_ancestor_of(A) + assert X.is_ancestor_of(B) + assert X.is_ancestor_of(C) + + + + @pytest.mark.django_db def test_auto_parenting(): org1 = Organization.objects.create(name='org1') diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 39a4767ad6..e48b964bc8 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -16,13 +16,13 @@ def test_credential_migration_user(credential, user, permissions): rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, permissions['admin']) + assert u in credential.owner_role @pytest.mark.django_db -def test_credential_usage_role(credential, user, permissions): +def test_credential_use_role(credential, user, permissions): u = user('user', False) - credential.usage_role.members.add(u) - assert credential.accessible_by(u, permissions['usage']) + credential.use_role.members.add(u) + assert u in credential.use_role @pytest.mark.django_db def test_credential_migration_team_member(credential, team, user, permissions): @@ -34,13 +34,13 @@ def test_credential_migration_team_member(credential, team, user, permissions): # No permissions pre-migration (this happens automatically so we patch this) team.admin_role.children.remove(credential.owner_role) - team.member_role.children.remove(credential.usage_role) - assert not credential.accessible_by(u, permissions['admin']) + team.member_role.children.remove(credential.use_role) + assert u not in credential.owner_role rbac.migrate_credential(apps, None) # Admin permissions post migration - assert credential.accessible_by(u, permissions['admin']) + assert u in credential.owner_role @pytest.mark.django_db def test_credential_migration_team_admin(credential, team, user, permissions): @@ -49,11 +49,11 @@ def test_credential_migration_team_admin(credential, team, user, permissions): credential.deprecated_team = team credential.save() - assert not credential.accessible_by(u, permissions['usage']) + assert u not in credential.use_role # Usage permissions post migration rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, permissions['usage']) + assert u in credential.use_role def test_credential_access_superuser(): u = User(username='admin', is_superuser=True) @@ -166,10 +166,10 @@ def test_cred_inventory_source(user, inventory, credential): inventory=inventory, ) - assert not credential.accessible_by(u, {'use':True}) + assert u not in credential.use_role rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, {'use':True}) + assert u in credential.use_role @pytest.mark.django_db def test_cred_project(user, credential, project): @@ -178,10 +178,10 @@ def test_cred_project(user, credential, project): project.credential = credential project.save() - assert not credential.accessible_by(u, {'use':True}) + assert u not in credential.use_role rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, {'use':True}) + assert u in credential.use_role @pytest.mark.django_db def test_cred_no_org(user, credential): @@ -196,7 +196,7 @@ def test_cred_team(user, team, credential): credential.deprecated_team = team credential.save() - assert not credential.accessible_by(u, {'use':True}) + assert u not in credential.use_role rbac.migrate_credential(apps, None) - assert credential.accessible_by(u, {'use':True}) + assert u in credential.use_role diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index b2196eb626..0727b73e10 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -6,7 +6,7 @@ from awx.main.models import ( Host, CustomInventoryScript, ) -from awx.main.access import InventoryAccess +from awx.main.access import InventoryAccess, HostAccess from django.apps import apps @pytest.mark.django_db @@ -16,10 +16,10 @@ def test_custom_inv_script_access(organization, user): custom_inv = CustomInventoryScript.objects.create(name='test', script='test', description='test') custom_inv.organization = organization custom_inv.save() - assert not custom_inv.accessible_by(u, {'read':True}) + assert u not in custom_inv.read_role organization.member_role.members.add(u) - assert custom_inv.accessible_by(u, {'read':True}) + assert u in custom_inv.read_role @pytest.mark.django_db def test_inventory_admin_user(inventory, permissions, user): @@ -27,13 +27,13 @@ def test_inventory_admin_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='admin') perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False + assert u not in inventory.admin_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) - assert inventory.executor_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert u in inventory.admin_role + assert inventory.execute_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False @pytest.mark.django_db def test_inventory_auditor_user(inventory, permissions, user): @@ -41,15 +41,15 @@ def test_inventory_auditor_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='read') perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is True - assert inventory.executor_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert u not in inventory.admin_role + assert u in inventory.auditor_role + assert inventory.execute_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False @pytest.mark.django_db def test_inventory_updater_user(inventory, permissions, user): @@ -57,14 +57,14 @@ def test_inventory_updater_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='write') perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() + assert u not in inventory.admin_role + assert inventory.execute_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() @pytest.mark.django_db def test_inventory_executor_user(inventory, permissions, user): @@ -72,15 +72,15 @@ def test_inventory_executor_user(inventory, permissions, user): perm = Permission(user=u, inventory=inventory, permission_type='read', run_ad_hoc_commands=True) perm.save() - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_inventory(apps, None) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is True - assert inventory.executor_role.members.filter(id=u.id).exists() - assert inventory.updater_role.members.filter(id=u.id).exists() is False + assert u not in inventory.admin_role + assert u in inventory.read_role + assert inventory.execute_role.members.filter(id=u.id).exists() + assert inventory.update_role.members.filter(id=u.id).exists() is False @@ -91,7 +91,7 @@ def test_inventory_admin_team(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False + assert u not in inventory.admin_role rbac.migrate_team(apps, None) rbac.migrate_inventory(apps, None) @@ -99,10 +99,10 @@ def test_inventory_admin_team(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False - assert inventory.accessible_by(u, permissions['auditor']) - assert inventory.accessible_by(u, permissions['admin']) + assert inventory.execute_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False + assert u in inventory.read_role + assert u in inventory.admin_role @pytest.mark.django_db @@ -112,8 +112,8 @@ def test_inventory_auditor(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_team(apps,None) rbac.migrate_inventory(apps, None) @@ -121,10 +121,10 @@ def test_inventory_auditor(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False - assert inventory.accessible_by(u, permissions['auditor']) - assert inventory.accessible_by(u, permissions['admin']) is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False + assert u in inventory.read_role + assert u not in inventory.admin_role @pytest.mark.django_db def test_inventory_updater(inventory, permissions, user, team): @@ -133,8 +133,8 @@ def test_inventory_updater(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_team(apps,None) rbac.migrate_inventory(apps, None) @@ -142,10 +142,10 @@ def test_inventory_updater(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False - assert team.member_role.is_ancestor_of(inventory.updater_role) - assert team.member_role.is_ancestor_of(inventory.executor_role) is False + assert inventory.execute_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False + assert team.member_role.is_ancestor_of(inventory.update_role) + assert team.member_role.is_ancestor_of(inventory.execute_role) is False @pytest.mark.django_db @@ -155,8 +155,8 @@ def test_inventory_executor(inventory, permissions, user, team): perm.save() team.deprecated_users.add(u) - assert inventory.accessible_by(u, permissions['admin']) is False - assert inventory.accessible_by(u, permissions['auditor']) is False + assert u not in inventory.admin_role + assert u not in inventory.auditor_role rbac.migrate_team(apps, None) rbac.migrate_inventory(apps, None) @@ -164,10 +164,10 @@ def test_inventory_executor(inventory, permissions, user, team): assert team.member_role.members.count() == 1 assert inventory.admin_role.members.filter(id=u.id).exists() is False assert inventory.auditor_role.members.filter(id=u.id).exists() is False - assert inventory.executor_role.members.filter(id=u.id).exists() is False - assert inventory.updater_role.members.filter(id=u.id).exists() is False - assert team.member_role.is_ancestor_of(inventory.updater_role) is False - assert team.member_role.is_ancestor_of(inventory.executor_role) + assert inventory.execute_role.members.filter(id=u.id).exists() is False + assert inventory.update_role.members.filter(id=u.id).exists() is False + assert team.member_role.is_ancestor_of(inventory.update_role) is False + assert team.member_role.is_ancestor_of(inventory.execute_role) @pytest.mark.django_db def test_group_parent_admin(group, permissions, user): @@ -177,21 +177,21 @@ def test_group_parent_admin(group, permissions, user): childA = group('child-1') parent1.admin_role.members.add(u) - assert parent1.accessible_by(u, permissions['admin']) - assert not parent2.accessible_by(u, permissions['admin']) - assert not childA.accessible_by(u, permissions['admin']) + assert u in parent1.admin_role + assert u not in parent2.admin_role + assert u not in childA.admin_role childA.parents.add(parent1) - assert childA.accessible_by(u, permissions['admin']) + assert u in childA.admin_role childA.parents.remove(parent1) - assert not childA.accessible_by(u, permissions['admin']) + assert u not in childA.admin_role parent2.children.add(childA) - assert not childA.accessible_by(u, permissions['admin']) + assert u not in childA.admin_role parent2.admin_role.members.add(u) - assert childA.accessible_by(u, permissions['admin']) + assert u in childA.admin_role @pytest.mark.django_db def test_access_admin(organization, inventory, user): @@ -237,33 +237,35 @@ def test_host_access(organization, inventory, user, group): not_my_group = group('not-my-group') group_admin = user('group_admin', False) + inventory_admin_access = HostAccess(inventory_admin) + group_admin_access = HostAccess(group_admin) h1 = Host.objects.create(inventory=inventory, name='host1') h2 = Host.objects.create(inventory=inventory, name='host2') h1.groups.add(my_group) h2.groups.add(not_my_group) - assert h1.accessible_by(inventory_admin, {'read': True}) is False - assert h1.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) is False + assert group_admin_access.can_read(h1) is False inventory.admin_role.members.add(inventory_admin) my_group.admin_role.members.add(group_admin) - assert h1.accessible_by(inventory_admin, {'read': True}) - assert h2.accessible_by(inventory_admin, {'read': True}) - assert h1.accessible_by(group_admin, {'read': True}) - assert h2.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) + assert inventory_admin_access.can_read(h2) + assert group_admin_access.can_read(h1) + assert group_admin_access.can_read(h2) is False my_group.hosts.remove(h1) - assert h1.accessible_by(inventory_admin, {'read': True}) - assert h1.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) + assert group_admin_access.can_read(h1) is False h1.inventory = other_inventory h1.save() - assert h1.accessible_by(inventory_admin, {'read': True}) is False - assert h1.accessible_by(group_admin, {'read': True}) is False + assert inventory_admin_access.can_read(h1) is False + assert group_admin_access.can_read(h1) is False diff --git a/awx/main/tests/functional/test_rbac_job_start.py b/awx/main/tests/functional/test_rbac_job_start.py index 67e661431d..18060126e1 100644 --- a/awx/main/tests/functional/test_rbac_job_start.py +++ b/awx/main/tests/functional/test_rbac_job_start.py @@ -24,7 +24,7 @@ def test_admin_executing_permissions(deploy_jobtemplate, inventory, machine_cred def test_job_template_start_access(deploy_jobtemplate, user): common_user = user('test-user', False) - deploy_jobtemplate.executor_role.members.add(common_user) + deploy_jobtemplate.execute_role.members.add(common_user) assert common_user.can_access(JobTemplate, 'start', deploy_jobtemplate) @@ -33,7 +33,7 @@ def test_job_template_start_access(deploy_jobtemplate, user): def test_credential_use_access(machine_credential, user): common_user = user('test-user', False) - machine_credential.usage_role.members.add(common_user) + machine_credential.use_role.members.add(common_user) assert common_user.can_access(Credential, 'use', machine_credential) @@ -42,6 +42,6 @@ def test_credential_use_access(machine_credential, user): def test_inventory_use_access(inventory, user): common_user = user('test-user', False) - inventory.usage_role.members.add(common_user) + inventory.use_role.members.add(common_user) assert common_user.can_access(Inventory, 'use', inventory) diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 7cf083da2e..93538f67f0 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -27,16 +27,16 @@ def test_job_template_migration_check(deploy_jobtemplate, check_jobtemplate, use rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert check_jobtemplate.project.accessible_by(joe, {'read': True}) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in check_jobtemplate.project.read_role + assert admin in check_jobtemplate.execute_role + assert joe not in check_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role @pytest.mark.django_db def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, user): @@ -55,16 +55,16 @@ def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, us rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert deploy_jobtemplate.project.accessible_by(joe, {'read': True}) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in deploy_jobtemplate.project.read_role + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in deploy_jobtemplate.execute_role + assert joe in deploy_jobtemplate.execute_role + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role @pytest.mark.django_db @@ -87,17 +87,17 @@ def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert check_jobtemplate.project.accessible_by(joe, {'read': True}) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in check_jobtemplate.read_role + assert admin in check_jobtemplate.execute_role + assert joe not in check_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role @pytest.mark.django_db @@ -120,17 +120,17 @@ def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplat rbac.migrate_projects(apps, None) rbac.migrate_inventory(apps, None) - assert deploy_jobtemplate.project.accessible_by(joe, {'read': True}) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is False + assert joe in deploy_jobtemplate.read_role + assert admin in deploy_jobtemplate.execute_role + assert joe not in deploy_jobtemplate.execute_role rbac.migrate_job_templates(apps, None) - assert deploy_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert deploy_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in deploy_jobtemplate.execute_role + assert joe in deploy_jobtemplate.execute_role - assert check_jobtemplate.accessible_by(admin, {'execute': True}) is True - assert check_jobtemplate.accessible_by(joe, {'execute': True}) is True + assert admin in check_jobtemplate.execute_role + assert joe in check_jobtemplate.execute_role @mock.patch.object(BaseAccess, 'check_license', return_value=None) diff --git a/awx/main/tests/functional/test_rbac_organization.py b/awx/main/tests/functional/test_rbac_organization.py index 89a0298df6..77558c0e7c 100644 --- a/awx/main/tests/functional/test_rbac_organization.py +++ b/awx/main/tests/functional/test_rbac_organization.py @@ -16,11 +16,11 @@ def test_organization_migration_admin(organization, permissions, user): # Undo some automatic work that we're supposed to be testing with our migration organization.admin_role.members.remove(u) - assert not organization.accessible_by(u, permissions['admin']) + assert u not in organization.admin_role rbac.migrate_organization(apps, None) - assert organization.accessible_by(u, permissions['admin']) + assert u in organization.admin_role @pytest.mark.django_db def test_organization_migration_user(organization, permissions, user): @@ -29,11 +29,11 @@ def test_organization_migration_user(organization, permissions, user): # Undo some automatic work that we're supposed to be testing with our migration organization.member_role.members.remove(u) - assert not organization.accessible_by(u, permissions['auditor']) + assert u not in organization.read_role rbac.migrate_organization(apps, None) - assert organization.accessible_by(u, permissions['auditor']) + assert u in organization.read_role @mock.patch.object(BaseAccess, 'check_license', return_value=None) diff --git a/awx/main/tests/functional/test_rbac_project.py b/awx/main/tests/functional/test_rbac_project.py index 6fae236667..d2e504645a 100644 --- a/awx/main/tests/functional/test_rbac_project.py +++ b/awx/main/tests/functional/test_rbac_project.py @@ -138,11 +138,11 @@ def test_project_user_project(user_project, project, user): assert old_access.check_user_access(u, user_project.__class__, 'read', user_project) assert old_access.check_user_access(u, project.__class__, 'read', project) is False - assert user_project.accessible_by(u, {'read': True}) is False - assert project.accessible_by(u, {'read': True}) is False + assert u not in user_project.read_role + assert u not in project.read_role rbac.migrate_projects(apps, None) - assert user_project.accessible_by(u, {'read': True}) is True - assert project.accessible_by(u, {'read': True}) is False + assert u in user_project.read_role + assert u not in project.read_role @pytest.mark.django_db def test_project_accessible_by_sa(user, project): @@ -150,21 +150,21 @@ def test_project_accessible_by_sa(user, project): # This gets setup by a signal, but we want to test the migration which will set this up too, so remove it Role.singleton('System Administrator').members.remove(u) - assert project.accessible_by(u, {'read': True}) is False + assert u not in project.read_role rbac.migrate_organization(apps, None) rbac.migrate_users(apps, None) rbac.migrate_projects(apps, None) print(project.admin_role.ancestors.all()) print(project.admin_role.ancestors.all()) - assert project.accessible_by(u, {'read': True, 'write': True}) is True + assert u in project.admin_role @pytest.mark.django_db def test_project_org_members(user, organization, project): admin = user('orgadmin') member = user('orgmember') - assert project.accessible_by(admin, {'read': True}) is False - assert project.accessible_by(member, {'read': True}) is False + assert admin not in project.read_role + assert member not in project.read_role organization.deprecated_admins.add(admin) organization.deprecated_users.add(member) @@ -172,8 +172,8 @@ def test_project_org_members(user, organization, project): rbac.migrate_organization(apps, None) rbac.migrate_projects(apps, None) - assert project.accessible_by(admin, {'read': True, 'write': True}) is True - assert project.accessible_by(member, {'read': True}) + assert admin in project.admin_role + assert member in project.read_role @pytest.mark.django_db def test_project_team(user, team, project): @@ -183,15 +183,15 @@ def test_project_team(user, team, project): team.deprecated_users.add(member) project.deprecated_teams.add(team) - assert project.accessible_by(nonmember, {'read': True}) is False - assert project.accessible_by(member, {'read': True}) is False + assert nonmember not in project.read_role + assert member not in project.read_role rbac.migrate_team(apps, None) rbac.migrate_organization(apps, None) rbac.migrate_projects(apps, None) - assert project.accessible_by(member, {'read': True}) is True - assert project.accessible_by(nonmember, {'read': True}) is False + assert member in project.read_role + assert nonmember not in project.read_role @pytest.mark.django_db def test_project_explicit_permission(user, team, project, organization): @@ -203,9 +203,9 @@ def test_project_explicit_permission(user, team, project, organization): p = Permission(user=u, project=project, permission_type='create', name='Perm name') p.save() - assert project.accessible_by(u, {'read': True}) is False + assert u not in project.read_role rbac.migrate_organization(apps, None) rbac.migrate_projects(apps, None) - assert project.accessible_by(u, {'read': True}) is True + assert u in project.read_role diff --git a/awx/main/tests/functional/test_rbac_team.py b/awx/main/tests/functional/test_rbac_team.py index a6ad507e22..3961cb837a 100644 --- a/awx/main/tests/functional/test_rbac_team.py +++ b/awx/main/tests/functional/test_rbac_team.py @@ -54,20 +54,20 @@ def test_team_accessible_by(team, user, project): u = user('team_member', False) team.member_role.children.add(project.member_role) - assert project.accessible_by(team, {'read':True}) - assert not project.accessible_by(u, {'read':True}) + assert team in project.read_role + assert u not in project.read_role team.member_role.members.add(u) - assert project.accessible_by(u, {'read':True}) + assert u in project.read_role @pytest.mark.django_db def test_team_accessible_objects(team, user, project): u = user('team_member', False) team.member_role.children.add(project.member_role) - assert len(Project.accessible_objects(team, {'read':True})) == 1 - assert not Project.accessible_objects(u, {'read':True}) + assert len(Project.accessible_objects(team, 'read_role')) == 1 + assert not Project.accessible_objects(u, 'read_role') team.member_role.members.add(u) - assert len(Project.accessible_objects(u, {'read':True})) == 1 + assert len(Project.accessible_objects(u, 'read_role')) == 1 diff --git a/awx/main/tests/functional/test_rbac_user.py b/awx/main/tests/functional/test_rbac_user.py index 346413b6f6..8e620771f5 100644 --- a/awx/main/tests/functional/test_rbac_user.py +++ b/awx/main/tests/functional/test_rbac_user.py @@ -40,14 +40,14 @@ def test_user_queryset(user): def test_user_accessible_objects(user, organization): admin = user('admin', False) u = user('john', False) - assert User.accessible_objects(admin, {'read':True}).count() == 1 + assert User.accessible_objects(admin, 'admin_role').count() == 1 organization.member_role.members.add(u) organization.admin_role.members.add(admin) - assert User.accessible_objects(admin, {'read':True}).count() == 2 + assert User.accessible_objects(admin, 'admin_role').count() == 2 organization.member_role.members.remove(u) - assert User.accessible_objects(admin, {'read':True}).count() == 1 + assert User.accessible_objects(admin, 'admin_role').count() == 1 @pytest.mark.django_db def test_org_user_admin(user, organization): @@ -55,13 +55,13 @@ def test_org_user_admin(user, organization): member = user('orgmember') organization.member_role.members.add(member) - assert not member.accessible_by(admin, {'write':True}) + assert admin not in member.admin_role organization.admin_role.members.add(admin) - assert member.accessible_by(admin, {'write':True}) + assert admin in member.admin_role organization.admin_role.members.remove(admin) - assert not member.accessible_by(admin, {'write':True}) + assert admin not in member.admin_role @pytest.mark.django_db def test_org_user_removed(user, organization): @@ -71,7 +71,7 @@ def test_org_user_removed(user, organization): organization.admin_role.members.add(admin) organization.member_role.members.add(member) - assert member.accessible_by(admin, {'write':True}) + assert admin in member.admin_role organization.member_role.members.remove(member) - assert not member.accessible_by(admin, {'write':True}) + assert admin not in member.admin_role diff --git a/awx/main/tests/functional/test_teams.py b/awx/main/tests/functional/test_teams.py new file mode 100644 index 0000000000..f1037f0462 --- /dev/null +++ b/awx/main/tests/functional/test_teams.py @@ -0,0 +1,10 @@ +import pytest + + +@pytest.mark.django_db() +def test_admin_not_member(team): + "Test to ensure we don't add admin_role as a parent to team.member_role, as " + "this creates a cycle with organization administration, which we've decided " + "to remove support for" + + assert team.admin_role.is_ancestor_of(team.member_role) is False diff --git a/awx/main/tests/job_base.py b/awx/main/tests/job_base.py index fb88134f61..c7f21a40a6 100644 --- a/awx/main/tests/job_base.py +++ b/awx/main/tests/job_base.py @@ -295,14 +295,14 @@ class BaseJobTestMixin(BaseTestMixin): password='ASK', created_by=self.user_sue, ) - self.cred_bob.usage_role.members.add(self.user_bob) + self.cred_bob.use_role.members.add(self.user_bob) self.cred_chuck = Credential.objects.create( username='chuck', ssh_key_data=TEST_SSH_KEY_DATA, created_by=self.user_sue, ) - self.cred_chuck.usage_role.members.add(self.user_chuck) + self.cred_chuck.use_role.members.add(self.user_chuck) self.cred_doug = Credential.objects.create( username='doug', @@ -310,7 +310,7 @@ class BaseJobTestMixin(BaseTestMixin): 'is why we dont\'t let doug actually run jobs.', created_by=self.user_sue, ) - self.cred_doug.usage_role.members.add(self.user_doug) + self.cred_doug.use_role.members.add(self.user_doug) self.cred_eve = Credential.objects.create( username='eve', @@ -320,14 +320,14 @@ class BaseJobTestMixin(BaseTestMixin): become_password='ASK', created_by=self.user_sue, ) - self.cred_eve.usage_role.members.add(self.user_eve) + self.cred_eve.use_role.members.add(self.user_eve) self.cred_frank = Credential.objects.create( username='frank', password='fr@nk the t@nk', created_by=self.user_sue, ) - self.cred_frank.usage_role.members.add(self.user_frank) + self.cred_frank.use_role.members.add(self.user_frank) self.cred_greg = Credential.objects.create( username='greg', @@ -335,21 +335,21 @@ class BaseJobTestMixin(BaseTestMixin): ssh_key_unlock='ASK', created_by=self.user_sue, ) - self.cred_greg.usage_role.members.add(self.user_greg) + self.cred_greg.use_role.members.add(self.user_greg) self.cred_holly = Credential.objects.create( username='holly', password='holly rocks', created_by=self.user_sue, ) - self.cred_holly.usage_role.members.add(self.user_holly) + self.cred_holly.use_role.members.add(self.user_holly) self.cred_iris = Credential.objects.create( username='iris', password='ASK', created_by=self.user_sue, ) - self.cred_iris.usage_role.members.add(self.user_iris) + self.cred_iris.use_role.members.add(self.user_iris) # Each operations team also has shared credentials they can use. self.cred_ops_east = Credential.objects.create( @@ -358,14 +358,14 @@ class BaseJobTestMixin(BaseTestMixin): ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK, created_by = self.user_sue, ) - self.team_ops_east.member_role.children.add(self.cred_ops_east.usage_role) + self.team_ops_east.member_role.children.add(self.cred_ops_east.use_role) self.cred_ops_west = Credential.objects.create( username='west', password='Heading270', created_by = self.user_sue, ) - self.team_ops_west.member_role.children.add(self.cred_ops_west.usage_role) + self.team_ops_west.member_role.children.add(self.cred_ops_west.use_role) # FIXME: This code can be removed (probably) @@ -391,7 +391,7 @@ class BaseJobTestMixin(BaseTestMixin): password='HeadingNone', created_by = self.user_sue, ) - self.team_ops_testers.member_role.children.add(self.cred_ops_test.usage_role) + self.team_ops_testers.member_role.children.add(self.cred_ops_test.use_role) self.ops_east_permission = Permission.objects.create( inventory = self.inv_ops_east, diff --git a/awx/main/tests/old/ad_hoc.py b/awx/main/tests/old/ad_hoc.py index 3b3f24391e..279b98f3ae 100644 --- a/awx/main/tests/old/ad_hoc.py +++ b/awx/main/tests/old/ad_hoc.py @@ -463,7 +463,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # not allowed to run ad hoc commands). user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,)) with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.updater_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204) with self.current_user('other'): self.run_test_ad_hoc_command(expect=403) self.check_get_list(url, 'other', qs) @@ -471,7 +471,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # Add executor role permissions to other. Fails # when other user can't read credential. with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) with self.current_user('other'): self.run_test_ad_hoc_command(expect=403) @@ -504,7 +504,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # Give the nobody user the run_ad_hoc_commands flag, and can now see # the one ad hoc command previously run. with self.current_user('admin'): - response = self.post(nobody_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) qs = AdHocCommand.objects.filter(credential_id=nobody_cred.pk) self.assertEqual(qs.count(), 1) self.check_get_list(url, 'nobody', qs) @@ -1006,7 +1006,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # can_run_ad_hoc_commands = True when we shouldn't. nobody_roles_list_url = reverse('api:user_roles_list', args=(self.nobody_django_user.pk,)) with self.current_user('admin'): - response = self.post(nobody_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(nobody_roles_list_url, {"id": self.inventory.execute_role.id}, expect=204) # Create a credential for the other user and explicitly give other # user admin permission on the inventory (still not allowed to run ad @@ -1014,7 +1014,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): other_cred = self.create_test_credential(user=self.other_django_user) user_roles_list_url = reverse('api:user_roles_list', args=(self.other_django_user.pk,)) with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.updater_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.update_role.id}, expect=204) with self.current_user('other'): response = self.get(url, expect=200) self.assertEqual(response['count'], 0) @@ -1025,7 +1025,7 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): # Update permission to allow other user to run ad hoc commands. Can # only see his own ad hoc commands (because of credential permission). with self.current_user('admin'): - response = self.post(user_roles_list_url, {"id": self.inventory.executor_role.id}, expect=204) + response = self.post(user_roles_list_url, {"id": self.inventory.adhoc_role.id}, expect=204) with self.current_user('other'): response = self.get(url, expect=200) self.assertEqual(response['count'], 0) diff --git a/awx/main/tests/old/commands/commands_monolithic.py b/awx/main/tests/old/commands/commands_monolithic.py index ba65385139..c0d409c5e0 100644 --- a/awx/main/tests/old/commands/commands_monolithic.py +++ b/awx/main/tests/old/commands/commands_monolithic.py @@ -952,7 +952,8 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertNotEqual(new_inv.groups.count(), 0) self.assertNotEqual(new_inv.total_hosts, 0) self.assertNotEqual(new_inv.total_groups, 0) - self.assertElapsedLessThan(600) + self.assertElapsedLessThan(1800) # TODO: We need to revisit this again to see if we can optimize this back to the sub-600 second range - anoek 2016-04-18 + def _get_ngroups_for_nhosts(self, n): if n > 0: @@ -977,7 +978,7 @@ class InventoryImportTest(BaseCommandMixin, BaseLiveServerTest): self.assertEqual(new_inv.groups.count(), ngroups) self.assertEqual(new_inv.total_hosts, nhosts) self.assertEqual(new_inv.total_groups, ngroups) - self.assertElapsedLessThan(1200) # FIXME: This should be < 120, will drop back down next sprint during our performance tuning work - anoek 2016-03-22 + self.assertElapsedLessThan(180) # TODO: We need to revisit this again to see if we can optimize this back to the sub-120 second range - anoek 2016-04-18 @unittest.skipIf(getattr(settings, 'LOCAL_DEVELOPMENT', False), 'Skip this test in local development environments, ' diff --git a/awx/main/tests/old/inventory.py b/awx/main/tests/old/inventory.py index 6d167ad10a..150eb45a31 100644 --- a/awx/main/tests/old/inventory.py +++ b/awx/main/tests/old/inventory.py @@ -77,7 +77,7 @@ class InventoryTest(BaseTest): self.check_get_list(url, self.normal_django_user, normal_qs) # a user who is on a team who has a read permissions on an inventory can see filtered inventories - other_qs = Inventory.accessible_objects(self.other_django_user, {'read': True}).distinct() + other_qs = Inventory.accessible_objects(self.other_django_user, 'read_role').distinct() self.check_get_list(url, self.other_django_user, other_qs) # a regular user not part of anything cannot see any inventories @@ -401,7 +401,7 @@ class InventoryTest(BaseTest): del_children_url = reverse('api:group_children_list', args=(del_group.pk,)) nondel_url = reverse('api:group_detail', args=(Group.objects.get(name='nondel').pk,)) - assert(inv.accessible_by(self.normal_django_user, {'read': True})) + assert self.normal_django_user in inv.read_role del_group.delete() nondel_detail = self.get(nondel_url, expect=200, auth=self.get_normal_credentials()) self.post(del_children_url, data=nondel_detail, expect=400, auth=self.get_normal_credentials()) diff --git a/awx/main/tests/old/organizations.py b/awx/main/tests/old/organizations.py index 1ace31a340..136e2603cc 100644 --- a/awx/main/tests/old/organizations.py +++ b/awx/main/tests/old/organizations.py @@ -289,7 +289,7 @@ class OrganizationsTest(BaseTest): # post a completely new user to verify we can add users to the subcollection directly new_user = dict(username='NewUser9000', password='NewPassword9000') - which_org = Organization.accessible_objects(self.normal_django_user, {'read': True, 'write': True})[0] + which_org = Organization.accessible_objects(self.normal_django_user, 'admin_role')[0] url = reverse('api:organization_users_list', args=(which_org.pk,)) self.post(url, new_user, expect=201, auth=self.get_normal_credentials()) diff --git a/awx/main/tests/old/users.py b/awx/main/tests/old/users.py index e6e5b1ddba..de364ff161 100644 --- a/awx/main/tests/old/users.py +++ b/awx/main/tests/old/users.py @@ -420,10 +420,10 @@ class UsersTest(BaseTest): self.assertEquals(data1['count'], 3) # 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. + # in that org, so he only sees himself and the system admin. self.organizations[0].delete() data3 = self.get(url, expect=200, auth=self.get_normal_credentials()) - self.assertEquals(data3['count'], 1) + self.assertEquals(data3['count'], 2) # Test no longer relevant since we've moved away from active / inactive. # However there was talk about keeping is_active for users, so this test will diff --git a/config/awx-munin.conf b/config/awx-munin.conf index 833a6f36bf..ca4f4c39ad 100644 --- a/config/awx-munin.conf +++ b/config/awx-munin.conf @@ -9,4 +9,3 @@ Alias /munin /var/www/html/munin/ require valid-user -ScriptAlias /munin-cgi/munin-cgi-graph /var/www/cgi-bin/munin-cgi-graph \ No newline at end of file diff --git a/tools/data_generators/rbac_dummy_data_generator.py b/tools/data_generators/rbac_dummy_data_generator.py new file mode 100755 index 0000000000..5a0df82da5 --- /dev/null +++ b/tools/data_generators/rbac_dummy_data_generator.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved +import os +import sys +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings.development") # noqa + +import django +django.setup() # noqa + + +# Python +from collections import defaultdict +from optparse import make_option, OptionParser + + +# Django + +from django.utils.timezone import now +from django.contrib.auth.models import User +from django.db import transaction + +# awx +from awx.main.models import * # noqa + + + + +option_list = [ + make_option('--organizations', action='store', type='int', default=3, + help='Number of organizations to create'), + make_option('--users', action='store', type='int', default=10, + help='Number of users to create'), + make_option('--teams', action='store', type='int', default=5, + help='Number of teams to create'), + make_option('--projects', action='store', type='int', default=10, + help='Number of projects to create'), + make_option('--job-templates', action='store', type='int', default=20, + help='Number of job templates to create'), + make_option('--credentials', action='store', type='int', default=5, + help='Number of credentials to create'), + make_option('--inventories', action='store', type='int', default=5, + help='Number of credentials to create'), + make_option('--inventory-groups', action='store', type='int', default=10, + help='Number of credentials to create'), + make_option('--inventory-hosts', action='store', type='int', default=40, + help='number of credentials to create'), + make_option('--jobs', action='store', type='int', default=200, + help='number of job entries to create'), + make_option('--job-events', action='store', type='int', default=500, + help='number of job event entries to create'), + make_option('--pretend', action='store_true', + help="Don't commit the data to the database"), + make_option('--prefix', action='store', type='string', default='', + help="Prefix generated names with this string"), + #make_option('--spread-bias', action='store', type='string', default='exponential', + # help='"exponential" to bias associations exponentially front loaded for - for ex'), +] +parser = OptionParser(option_list=option_list) +options, remainder = parser.parse_args() +options = vars(options) + + +n_organizations = int(options['organizations']) +n_users = int(options['users']) +n_teams = int(options['teams']) +n_projects = int(options['projects']) +n_job_templates = int(options['job_templates']) +n_credentials = int(options['credentials']) +n_inventories = int(options['inventories']) +n_inventory_groups = int(options['inventory_groups']) +n_inventory_hosts = int(options['inventory_hosts']) +n_jobs = int(options['jobs']) +n_job_events = int(options['job_events']) +prefix = options['prefix'] + +organizations = [] +users = [] +teams = [] +projects = [] +job_templates = [] +credentials = [] +inventories = [] +inventory_groups = [] +inventory_hosts = [] +jobs = [] +#job_events = [] + +def spread(n, m): + ret = [] + # At least one in each slot, split up the rest exponentially so the first + # buckets contain a lot of entries + for i in xrange(m): + if n > 0: + ret.append(1) + n -= 1 + else: + ret.append(0) + + for i in xrange(m): + n_in_this_slot = n // 2 + n-= n_in_this_slot + ret[i] += n_in_this_slot + if n > 0 and len(ret): + ret[0] += n + return ret + +ids = defaultdict(lambda: 0) + + +class Rollback(Exception): + pass + + +try: + + with batch_role_ancestor_rebuilding(): + with transaction.atomic(): + admin, _ = User.objects.get_or_create(username = 'admin', is_superuser=True) + org_admin, _ = User.objects.get_or_create(username = 'org_admin') + org_member, _ = User.objects.get_or_create(username = 'org_member') + prj_admin, _ = User.objects.get_or_create(username = 'prj_admin') + jt_admin, _ = User.objects.get_or_create(username = 'jt_admin') + inv_admin, _ = User.objects.get_or_create(username = 'inv_admin') + + admin.is_superuser = True + admin.save() + admin.set_password('test') + admin.save() + org_admin.set_password('test') + org_admin.save() + org_member.set_password('test') + org_member.save() + prj_admin.set_password('test') + prj_admin.save() + jt_admin.set_password('test') + jt_admin.save() + inv_admin.set_password('test') + inv_admin.save() + + + + print('# Creating %d organizations' % n_organizations) + for i in xrange(n_organizations): + sys.stdout.write('\r%d ' % (i + 1)) + sys.stdout.flush() + org = Organization.objects.create(name='%s Organization %d' % (prefix, i)) + organizations.append(org) + if i == 0: + org.admin_role.members.add(org_admin) + org.member_role.members.add(org_admin) + org.member_role.members.add(org_member) + org.member_role.members.add(prj_admin) + org.member_role.members.add(jt_admin) + org.member_role.members.add(inv_admin) + + print('') + + print('# Creating %d users' % n_users) + org_idx = 0 + for n in spread(n_users, n_organizations): + for i in range(n): + ids['user'] += 1 + user_id = ids['user'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, organizations[org_idx].name, i+ 1)) + sys.stdout.flush() + user = User.objects.create(username='%suser-%d' % (prefix, user_id)) + organizations[org_idx].member_role.members.add(user) + users.append(user) + org_idx += 1 + print('') + + print('# Creating %d teams' % n_teams) + org_idx = 0 + for n in spread(n_teams, n_organizations): + org = organizations[org_idx] + for i in range(n): + ids['team'] += 1 + team_id = ids['team'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) + sys.stdout.flush() + team = Team.objects.create(name='%s Team %d Org %d' % (prefix, team_id, org_idx), organization=org) + teams.append(team) + org_idx += 1 + print('') + + print('# Adding users to teams') + for org in organizations: + org_teams = [t for t in org.teams.all()] + org_users = [u for u in org.member_role.members.all()] + print(' Spreading %d users accross %d teams for %s' % (len(org_users), len(org_teams), org.name)) + # Our normal spread for most users + cur_user_idx = 0 + cur_team_idx = 0 + for n in spread(len(org_users), len(org_teams)): + team = org_teams[cur_team_idx] + for i in range(n): + if cur_user_idx < len(org_users): + user = org_users[cur_user_idx] + team.member_role.members.add(user) + cur_user_idx += 1 + cur_team_idx += 1 + + # First user gets added to all teams + for team in org_teams: + team.member_role.members.add(org_users[0]) + + + print('# Creating %d credentials for users' % (n_credentials - n_credentials // 2)) + user_idx = 0 + for n in spread(n_credentials - n_credentials // 2, n_users): + user = users[user_idx] + for i in range(n): + ids['credential'] += 1 + sys.stdout.write('\r %d ' % (ids['credential'])) + sys.stdout.flush() + credential_id = ids['credential'] + credential = Credential.objects.create(name='%s Credential %d User %d' % (prefix, credential_id, user_idx)) + credential.owner_role.members.add(user) + credentials.append(credential) + user_idx += 1 + print('') + + print('# Creating %d credentials for teams' % (n_credentials // 2)) + team_idx = 0 + starting_credential_id = ids['credential'] + for n in spread(n_credentials - n_credentials // 2, n_teams): + team = teams[team_idx] + for i in range(n): + ids['credential'] += 1 + sys.stdout.write('\r %d ' % (ids['credential'] - starting_credential_id)) + sys.stdout.flush() + credential_id = ids['credential'] + credential = Credential.objects.create(name='%s Credential %d team %d' % (prefix, credential_id, team_idx)) + credential.owner_role.parents.add(team.member_role) + credentials.append(credential) + team_idx += 1 + print('') + + print('# Creating %d projects' % n_projects) + org_idx = 0 + for n in spread(n_projects, n_organizations): + org = organizations[org_idx] + for i in range(n): + ids['project'] += 1 + project_id = ids['project'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) + sys.stdout.flush() + project = Project.objects.create(name='%s Project %d Org %d' % (prefix, project_id, org_idx), organization=org) + projects.append(project) + if org_idx == 0 and i == 0: + project.admin_role.members.add(prj_admin) + + org_idx += 1 + print('') + + + print('# Creating %d inventories' % n_inventories) + org_idx = 0 + for n in spread(n_inventories, min(n_inventories // 4 + 1, n_organizations)): + org = organizations[org_idx] + for i in range(n): + ids['inventory'] += 1 + inventory_id = ids['inventory'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, org.name, i+ 1)) + sys.stdout.flush() + inventory = Inventory.objects.create(name='%s Inventory %d Org %d' % (prefix, inventory_id, org_idx), organization=org) + inventories.append(inventory) + if org_idx == 0 and i == 0: + inventory.admin_role.members.add(inv_admin) + + org_idx += 1 + print('') + + + print('# Creating %d inventory_groups' % n_inventory_groups) + inv_idx = 0 + for n in spread(n_inventory_groups, n_inventories): + inventory = inventories[inv_idx] + parent_list = [None] * 3 + for i in range(n): + ids['group'] += 1 + group_id = ids['group'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, inventory.name, i+ 1)) + sys.stdout.flush() + group = Group.objects.create( + name='%s Group %d Inventory %d' % (prefix, group_id, inv_idx), + inventory=inventory, + ) + # Have each group have up to 3 parent groups + for parent_n in range(3): + if i // 4 + parent_n < len(parent_list) and parent_list[i // 4 + parent_n]: + group.parents.add(parent_list[i // 4 + parent_n]) + if parent_list[i // 4] is None: + parent_list[i // 4] = group + else: + parent_list.append(group) + inventory_groups.append(group) + + inv_idx += 1 + print('') + + + print('# Creating %d inventory_hosts' % n_inventory_hosts) + group_idx = 0 + for n in spread(n_inventory_hosts, n_inventory_groups): + group = inventory_groups[group_idx] + for i in range(n): + ids['host'] += 1 + host_id = ids['host'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, group.name, i+ 1)) + sys.stdout.flush() + host = Host.objects.create(name='%s.host-%06d.group-%05d.dummy' % (prefix, host_id, group_idx), inventory=group.inventory) + # Add the host to up to 3 groups + host.groups.add(group) + for m in range(2): + if group_idx + m < len(inventory_groups) and group.inventory.id == inventory_groups[group_idx + m].inventory.id: + host.groups.add(inventory_groups[group_idx + m]) + + inventory_hosts.append(host) + + group_idx += 1 + print('') + + print('# Creating %d job_templates' % n_job_templates) + project_idx = 0 + inv_idx = 0 + for n in spread(n_job_templates, n_projects): + project = projects[project_idx] + for i in range(n): + ids['job_template'] += 1 + job_template_id = ids['job_template'] + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, project.name, i+ 1)) + sys.stdout.flush() + + inventory = None + org_inv_count = project.organization.inventories.count() + if org_inv_count > 0: + inventory = project.organization.inventories.all()[inv_idx % org_inv_count] + + job_template = JobTemplate.objects.create( + name='%s Job Template %d Project %d' % (prefix, job_template_id, project_idx), + inventory=inventory, + project=project, + ) + job_templates.append(job_template) + inv_idx += 1 + if project_idx == 0 and i == 0: + job_template.admin_role.members.add(jt_admin) + project_idx += 1 + print('') + + print('# Creating %d jobs' % n_jobs) + group_idx = 0 + job_template_idx = 0 + for n in spread(n_jobs, n_job_templates): + job_template = job_templates[job_template_idx] + for i in range(n): + sys.stdout.write('\r Assigning %d to %s: %d ' % (n, job_template.name, i+ 1)) + sys.stdout.flush() + job = Job.objects.create(job_template=job_template) + jobs.append(job) + + if job_template.inventory: + inv_groups = [g for g in job_template.inventory.groups.all()] + if len(inv_groups): + JobHostSummary.objects.bulk_create([ + JobHostSummary( + job=job, host=h, host_name=h.name, processed=1, + created=now(), modified=now() + ) + for h in inv_groups[group_idx % len(inv_groups)].hosts.all()[:100] + ]) + group_idx += 1 + job_template_idx += 1 + if n: + print('') + + print('# Creating %d job events' % n_job_events) + job_idx = 0 + for n in spread(n_job_events, n_jobs): + job = jobs[job_idx] + sys.stdout.write('\r Creating %d job events for job %d' % (n, job.id)) + sys.stdout.flush() + JobEvent.objects.bulk_create([ + JobEvent( + created=now(), + modified=now(), + job=job, + event='runner_on_ok' + ) + for i in range(n) + ]) + job_idx += 1 + if n: + print('') + + if options['pretend']: + raise Rollback() +except Rollback: + print('Rolled back changes') + pass