diff --git a/lib/main/base_views.py b/lib/main/base_views.py index 4928d0c575..5209696602 100644 --- a/lib/main/base_views.py +++ b/lib/main/base_views.py @@ -34,7 +34,7 @@ import json as python_json # FIXME: machinery for auto-adding audit trail logs to all CREATE/EDITS class BaseList(generics.ListCreateAPIView): - + def list_permissions_check(self, request, obj=None): ''' determines some early yes/no access decisions, pre-filtering ''' if request.method == 'GET': @@ -50,7 +50,7 @@ class BaseList(generics.ListCreateAPIView): raise PermissionDenied() return True raise exceptions.NotImplementedError - + def get_queryset(self): base = self._get_queryset() model = self.__class__.model @@ -121,11 +121,16 @@ class BaseSubList(BaseList): # save the object through the serializer, reload and returned the saved object deserialized obj = ser.save() ser = self.__class__.serializer_class(obj) - + # now make sure we could have already attached the two together. If we could not have, raise an exception # such that the transaction does not commit. - if not self.__class__.parent_model.can_user_attach(request.user, main, obj, self.__class__.relationship): - raise PermissionDenied() + if self.__class__.parent_model != User: + if not self.__class__.parent_model.can_user_attach(request.user, main, obj, self.__class__.relationship): + raise PermissionDenied() + else: + # FIXME: should generalize this + if not UserHelper.can_user_attach(request.user, main, obj, self.__class__.relationship): + raise PermissionDenied() return Response(status=status.HTTP_201_CREATED, data=ser.data) @@ -141,7 +146,7 @@ class BaseSubList(BaseList): return Response(status=status.HTTP_400_BAD_REQUEST) sub = subs[0] relationship = getattr(main, self.__class__.relationship) - + if not 'disassociate' in request.DATA: if not request.user.is_superuser and not self.__class__.parent_model.can_user_attach(request.user, main, sub, self.__class__.relationship): raise PermissionDenied() @@ -215,14 +220,14 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView): pass class VariableBaseDetail(BaseDetail): - ''' - an object that is always 1 to 1 with the foreign key of another object - and does not have it's own key, such as HostVariableDetail + ''' + an object that is always 1 to 1 with the foreign key of another object + and does not have it's own key, such as HostVariableDetail ''' def destroy(self, request, *args, **kwargs): raise PermissionDenied() - + def delete_permissions_check(self, request, obj): raise PermissionDenied() @@ -285,4 +290,4 @@ class VariableBaseDetail(BaseDetail): if not has_permission: raise PermissionDenied() return Response(status=status.HTTP_200_OK, data=python_json.loads(this_object.data)) - + diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 927fc431cf..36da1b85be 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -126,6 +126,13 @@ class UserHelper(object): matching_orgs = obj.organizations.filter(admins__in = [user]).count() return matching_orgs + @classmethod + def can_user_attach(cls, user, obj, sub_obj, relationship_type): + if type(sub_obj) != User: + if not sub_obj.can_user_read(user, sub_obj): + return False + rc = cls.can_user_administrate(user, obj) + return rc class PrimordialModel(models.Model): ''' @@ -186,11 +193,11 @@ class CommonModel(PrimordialModel): name = models.CharField(max_length=512, unique=True) class CommonModelNameNotUnique(PrimordialModel): - ''' a base model where the name is not unique ''' + ''' a base model where the name is not unique ''' class Meta: abstract = True - + name = models.CharField(max_length=512, unique=False) class Tag(models.Model): @@ -290,7 +297,7 @@ class Inventory(CommonModel): unique_together = (("name", "organization"),) organization = models.ForeignKey(Organization, null=False, related_name='inventories') - + def get_absolute_url(self): import lib.urls return reverse(lib.urls.views_InventoryDetail, args=(self.pk,)) @@ -308,16 +315,16 @@ class Inventory(CommonModel): user = user, permission_type__in = allowed ).count() - + result = (by_org_admin + by_team_permission + by_user_permission) return result > 0 @classmethod def _has_any_inventory_permission_types(cls, user, allowed): - ''' - rather than checking for a permission on a specific inventory, return whether we have + ''' + rather than checking for a permission on a specific inventory, return whether we have permissions on any inventory. This is primarily used to decide if the user can create - host or group objects + host or group objects ''' if user.is_superuser: @@ -332,7 +339,7 @@ class Inventory(CommonModel): by_user_permission = user.permissions.filter( permission_type__in = allowed ).count() - + result = (by_org_admin + by_team_permission + by_user_permission) return result > 0 @@ -380,7 +387,7 @@ class Host(CommonModelNameNotUnique): class Meta: app_label = 'main' unique_together = (("name", "inventory"),) - + variable_data = models.OneToOneField('VariableData', null=True, default=None, blank=True, on_delete=SET_NULL, related_name='host') inventory = models.ForeignKey('Inventory', null=False, related_name='hosts') @@ -390,7 +397,7 @@ class Host(CommonModelNameNotUnique): @classmethod def can_user_read(cls, user, obj): return Inventory.can_user_read(user, obj.inventory) - + @classmethod def can_user_add(cls, user, data): if not 'inventory' in data: @@ -432,7 +439,7 @@ class Group(CommonModelNameNotUnique): inventory = Inventory.objects.get(pk=data['inventory']) return Inventory._has_permission_types(user, inventory, PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE) - + @classmethod def can_user_administrate(cls, user, obj): # here this controls whether the user can attach subgroups @@ -511,7 +518,7 @@ class Credential(CommonModel): # # STAGE 3: # - # MICHAEL: modify ansible/ansible-playbook such that + # MICHAEL: modify ansible/ansible-playbook such that # if ANSIBLE_PASSWORD or ANSIBLE_SUDO_PASSWORD is set # you do not have to use --ask-pass and --ask-sudo-pass, so we don't have to do interactive # stuff with that. @@ -520,10 +527,24 @@ class Credential(CommonModel): ssh_key_data = models.TextField(blank=True, default='') ssh_key_unlock = models.CharField(blank=True, default='', max_length=1024) - default_username = models.CharField(blank=True, default='', max_length=1024) + default_username = models.CharField(blank=True, default='', max_length=1024) ssh_password = models.CharField(blank=True, default='', max_length=1024) sudo_password = models.CharField(blank=True, default='', max_length=1024) + @classmethod + def can_user_read(cls, user, obj): + ''' a user can be read if they are on the same team or can be administrated ''' + if user.is_superuser: + return True + if user == obj.user: + return True + if Organizations.filter(admins__in = [ user ], users__in = [ obj.user ]).count(): + return True + return False + + def get_absolute_url(self): + import lib.urls + return reverse(lib.urls.views_CredentialsDetail, args=(self.pk,)) class Team(CommonModel): ''' @@ -571,7 +592,7 @@ class Team(CommonModel): @classmethod def can_user_delete(cls, user, obj): return cls.can_user_administrate(user, obj) - + class Project(CommonModel): ''' A project represents a playbook git repo that can access a set of inventories @@ -632,7 +653,7 @@ class Permission(CommonModelNameNotUnique): # for example, user A on inventory X has write permissions (PERM_INVENTORY_WRITE) # team C on inventory X has read permissions (PERM_INVENTORY_READ) # team C on inventory X and project Y has launch permissions (PERM_INVENTORY_DEPLOY) - # team C on inventory X and project Z has dry run permissions (PERM_INVENTORY_CHECK) + # team C on inventory X and project Z has dry run permissions (PERM_INVENTORY_CHECK) # # basically for launching, permissions can be awarded to the whole inventory source or just the inventory source # in context of a given project. @@ -640,14 +661,14 @@ class Permission(CommonModelNameNotUnique): # the project parameter is not used when dealing with READ, WRITE, or ADMIN permissions. permission_type = models.CharField(max_length=64, choices=PERMISSION_TYPE_CHOICES) - + def __unicode__(self): return unicode("Permission(name=%s,ON(user=%s,team=%s),FOR(project=%s,inventory=%s,type=%s))" % ( self.name, - self.user, - self.team, - self.project, - self.inventory, + self.user, + self.team, + self.project, + self.inventory, self.permission_type )) diff --git a/lib/main/serializers.py b/lib/main/serializers.py index 1a6b26e773..bcab1b9208 100644 --- a/lib/main/serializers.py +++ b/lib/main/serializers.py @@ -146,7 +146,7 @@ class CredentialSerializer(BaseSerializer): model = Credential fields = ( 'url', 'id', 'related', 'name', 'description', 'creation_date', - 'ssh_key_path', 'ssh_key_data', 'ssh_key_unlock', 'ssh_password', 'sudo_password', + 'default_username', 'ssh_key_data', 'ssh_key_unlock', 'ssh_password', 'sudo_password', 'user', 'team' ) diff --git a/lib/main/tests/projects.py b/lib/main/tests/projects.py index 517ab66de6..5212d66222 100644 --- a/lib/main/tests/projects.py +++ b/lib/main/tests/projects.py @@ -285,7 +285,7 @@ class ProjectsTest(BaseTest): new_credentials = dict( name = 'credential', project = Project.objects.all()[0].pk, - ssh_key_path = 'foo', + default_username = 'foo', ssh_key_data = 'bar', ssh_key_unlock = 'baz', ssh_password = 'narf', diff --git a/lib/main/tests/tasks.py b/lib/main/tests/tasks.py index c8f5a90765..d62e0c8d7d 100644 --- a/lib/main/tests/tasks.py +++ b/lib/main/tests/tasks.py @@ -88,8 +88,12 @@ class RunLaunchJobTest(BaseCeleryTest): launch_job_status = self.launch_job.start() self.assertEqual(launch_job_status.status, 'pending') launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status.pk) - #print 'stdout:', launch_job_status.result_stdout - #print 'stderr:', launch_job_status.result_stderr + print 'stdout:', launch_job_status.result_stdout + print 'stderr:', launch_job_status.result_stderr + print launch_job_status.status + + print settings.DATABASES + self.assertEqual(launch_job_status.status, 'successful') self.assertTrue(launch_job_status.result_stdout) launch_job_status_events = launch_job_status.launch_job_status_events.all() diff --git a/lib/main/views.py b/lib/main/views.py index e33fff1a29..a8ccb79c62 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -39,9 +39,9 @@ class OrganizationsList(BaseList): # I can see the organizations if: # I am a superuser - # I am an admin of the organization + # I am an admin of the organization # I am a member of the organization - + def _get_queryset(self): ''' I can see organizations when I am a superuser, or I am an admin or user in that organization ''' base = Organization.objects @@ -78,7 +78,7 @@ class OrganizationsAuditTrailList(BaseSubList): class OrganizationsUsersList(BaseSubList): - + model = User serializer_class = UserSerializer permission_classes = (CustomRbac,) @@ -95,7 +95,7 @@ class OrganizationsUsersList(BaseSubList): return User.objects.filter(organizations__in = [ organization ]) class OrganizationsAdminsList(BaseSubList): - + model = User serializer_class = UserSerializer permission_classes = (CustomRbac,) @@ -112,7 +112,7 @@ class OrganizationsAdminsList(BaseSubList): return User.objects.filter(admin_of_organizations__in = [ organization ]) class OrganizationsProjectsList(BaseSubList): - + model = Project serializer_class = ProjectSerializer permission_classes = (CustomRbac,) @@ -120,7 +120,7 @@ class OrganizationsProjectsList(BaseSubList): relationship = 'projects' # " " postable = True inject_primary_key_on_post_as = 'organization' - + def _get_queryset(self): ''' to list projects in the organization, I must be a superuser or org admin ''' organization = Organization.objects.get(pk=self.kwargs['pk']) @@ -129,7 +129,7 @@ class OrganizationsProjectsList(BaseSubList): return Project.objects.filter(organizations__in = [ organization ]) class OrganizationsTagsList(BaseSubList): - + model = Tag serializer_class = TagSerializer permission_classes = (CustomRbac,) @@ -147,7 +147,7 @@ class OrganizationsTagsList(BaseSubList): return Tag.objects.filter(organization_by_tag__in = [ organization ]) class OrganizationsTeamsList(BaseSubList): - + model = Team serializer_class = TeamSerializer permission_classes = (CustomRbac,) @@ -174,7 +174,7 @@ class TeamsList(BaseList): # I am a superuser # I am an admin of the organization that the team is # I am on that team - + def _get_queryset(self): ''' I can see organizations when I am a superuser, or I am an admin or user in that organization ''' base = Team.objects @@ -193,7 +193,7 @@ class TeamsDetail(BaseDetail): permission_classes = (CustomRbac,) class TeamsUsersList(BaseSubList): - + model = User serializer_class = UserSerializer permission_classes = (CustomRbac,) @@ -224,7 +224,7 @@ class ProjectsList(BaseList): # I am a superuser # I am an admin of the organization that contains the project # I am a member of a team that also contains the project - + def _get_queryset(self): ''' I can see organizations when I am a superuser, or I am an admin or user in that organization ''' base = Project.objects @@ -279,15 +279,15 @@ class UsersList(BaseList): user = User.objects.get(pk=pk) user.set_password(password) user.save() - return result + return result def _get_queryset(self): ''' I can see user records when I'm a superuser, I'm that user, I'm their org admin, or I'm on a team with that user ''' base = User.objects if self.request.user.is_superuser: return base.all() - mine = base.filter(pk = self.request.user.pk).distinct() - admin_of = base.filter(organizations__in = self.request.user.admin_of_organizations.all()).distinct() + mine = base.filter(pk = self.request.user.pk).distinct() + admin_of = base.filter(organizations__in = self.request.user.admin_of_organizations.all()).distinct() same_team = base.filter(teams__in = self.request.user.teams.all()).distinct() return mine | admin_of | same_team @@ -362,7 +362,7 @@ class UsersOrganizationsList(BaseSubList): parent_model = User relationship = 'organizations' postable = False - + def _get_queryset(self): user = User.objects.get(pk=self.kwargs['pk']) if not UserHelper.can_user_administrate(self.request.user, user): @@ -391,7 +391,7 @@ class UsersDetail(BaseDetail): permission_classes = (CustomRbac,) def put_filter(self, request, *args, **kwargs): - ''' make sure non-read-only fields that can only be edited by admins, are only edited by admins ''' + ''' make sure non-read-only fields that can only be edited by admins, are only edited by admins ''' obj = User.objects.get(pk=kwargs['pk']) if EditHelper.illegal_changes(request, obj, UserHelper): raise PermissionDenied() @@ -400,6 +400,13 @@ class UsersDetail(BaseDetail): obj.save() request.DATA.pop('password') +class CredentialsDetail(BaseDetail): + + model = Credential + serializer_class = CredentialSerializer + permission_classes = (CustomRbac,) + + class InventoryList(BaseList): model = Inventory @@ -438,9 +445,9 @@ class HostsList(BaseList): permission_classes = (CustomRbac,) def _get_queryset(self): - ''' + ''' I can see hosts when: - I'm a superuser, + I'm a superuser, or an organization admin of an inventory they are in or when I have allowing read permissions via a user or team on an inventory they are in ''' @@ -524,7 +531,7 @@ class GroupsChildrenList(BaseSubList): def _get_queryset(self): # FIXME: this is the mostly the same as GroupsList, share code similar to how done with Host and Group objects. - + parent = Group.objects.get(pk=self.kwargs['pk']) # FIXME: verify read permissions on this object are still required at a higher level diff --git a/lib/urls.py b/lib/urls.py index 73f4a577fe..fcfe9c8b6e 100644 --- a/lib/urls.py +++ b/lib/urls.py @@ -80,6 +80,9 @@ views_VariableDetail = views.VariableDetail.as_view() # tags service views_TagsDetail = views.TagsDetail.as_view() +# credentials service +views_CredentialsDetail = views.CredentialsDetail.as_view() + urlpatterns = patterns('', @@ -107,7 +110,7 @@ urlpatterns = patterns('', url(r'^api/v1/projects/$', views_ProjectsList), url(r'^api/v1/projects/(?P[0-9]+)/$', views_ProjectsDetail), url(r'^api/v1/projects/(?P[0-9]+)/organizations/$', views_ProjectsOrganizationsList), - + # audit trail service # api/v1/audit_trails/ # api/v1/audit_trails/N/ @@ -119,7 +122,7 @@ urlpatterns = patterns('', url(r'^api/v1/teams/(?P[0-9]+)/$', views_TeamsDetail), url(r'^api/v1/teams/(?P[0-9]+)/users/$', views_TeamsUsersList), - # api/v1/teams/N/ + # api/v1/teams/N/ # api/v1/teams/N/users/ # inventory service @@ -155,9 +158,7 @@ urlpatterns = patterns('', # ... and tag relations on all resources # credentials services - # ... users - # ... teams - # ... projects (?) + url(r'^api/v1/credentials/(?P[0-9]+)/$', views_CredentialsDetail), # permissions services # ... users