From ec0e0f60dc3ac72cfe73993137f98d91ff6d0442 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 15 Apr 2013 19:19:54 -0400 Subject: [PATCH] Some TODO updates, a model revision, and getting association/disassociation working on user/team creds. --- TODO.md | 49 ++++++++++++++++++++++++++++--------- lib/main/base_views.py | 27 ++++++++++++++++---- lib/main/models/__init__.py | 12 ++++++++- lib/main/tests/projects.py | 18 +++++++++++--- 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/TODO.md b/TODO.md index 23dd3cb471..71ac687d58 100644 --- a/TODO.md +++ b/TODO.md @@ -3,25 +3,52 @@ TODO items for ansible commander 4/2 NOTES ========= -* supervisord to start celery, modify ansible playbook to set up supervisord <- Chris -* host relationships in DB, last launch job status per host, etc (self.play.inventory) <- Chris -* stats attributes on launch job (??) +* supervisord to start celery, modify ansible playbook to set up supervisord <- ChrisC +* host relationships in DB, last launch job status per host, etc (self.play.inventory) <- ChrisC +* stats attributes on launch job status (??) -- which hosts succeeded, failed, etc (through relationship) <-- Chris C * make launch job rest triggerable & launch job statuses readable. launch_job.start() <-- MPD -* do we need something other than default playbook (ProjectOptions) <-- BOTH, TBD -* way to send cntrl-c to kill job (method on job?) <-- Chris, low priority -* documentation on how to run with callbacks from NOT a launchjob <-- Chris -* interactive SSH agent support for launch jobs/creds +* do we need something other than default playbook (ProjectOptions) <-- me, later +* way to send cntrl-c to kill job (method on job?) <-- ChrisC, low priority +* documentation on how to run with callbacks from NOT a launchjob <-- ChrisC +* interactive SSH agent support for launch jobs/creds <-- ChrisC * michael to modify ansible to accept ssh password and sudo password from env vars +CH notes +======== +need to add api/v1/group/N/all_hosts +need to add api/v1/host/N/all_groups or make sure it's in the resource +figure out how to mark each object generically w/ whether it can be edited/etc +enable generic filtering +sorting? +if you delete a subgroup, hosts go up to parent +verify unique_together on inventory and host name + +make a way to start a launch job via the API. + +future feature of being able to create SavedLaunchJob templates. + REST TODO --------- * credentials objects & permissions -* tags -* audit trails -* launch jobs triggering -* related resources on everything that makes sense +* tags (later) +* audit trails (later) +* launch jobs triggering <-- important +* related resources on everything that makes sense <--- important * expose log data from callback (decide on structure) + +api/v1/ <-- discoverable +api/v1/me + +{ + id: 2 + foo: 3 + related: [ + 'bar' : 'http://api/v1/users/5/bar', + 'baz' : 'http://api/v1/users/10/baz', + } +} + LATER ----- diff --git a/lib/main/base_views.py b/lib/main/base_views.py index 1791c56e5d..7f9ef92142 100644 --- a/lib/main/base_views.py +++ b/lib/main/base_views.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible Commander. If not, see . -from django.http import HttpResponse +from django.http import HttpResponse, Http404 from django.views.decorators.csrf import csrf_exempt from lib.main.models import * from django.contrib.auth.models import User @@ -147,14 +147,27 @@ class BaseSubList(BaseList): 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() + if not request.user.is_superuser: + if type(main) != User: + if not self.__class__.parent_model.can_user_attach(request.user, main, sub, self.__class__.relationship): + raise PermissionDenied() + else: + if not UserHelper.can_user_attach(request.user, main, sub, self.__class__.relationship): + raise PermissionDenied() + if sub in relationship.all(): return Response(status=status.HTTP_409_CONFLICT) relationship.add(sub) else: - if not request.user.is_superuser and not self.__class__.parent_model.can_user_unattach(request.user, main, sub, self.__class__.relationship): - raise PermissionDenied() + if not request.user.is_superuser: + if type(main) != User: + if not self.__class__.parent_model.can_user_unattach(request.user, main, sub, self.__class__.relationship): + raise PermissionDenied() + else: + if not UserHelper.can_user_unattach(request.user, main, sub, self.__class__.relationship): + raise PermissionDenied() + + if severable: relationship.remove(sub) else: @@ -174,6 +187,10 @@ class BaseDetail(generics.RetrieveUpdateDestroyAPIView): def destroy(self, request, *args, **kwargs): # somewhat lame that delete has to call it's own permissions check obj = self.model.objects.get(pk=kwargs['pk']) + if getattr(obj, 'active', True) == False: + raise Http404() + if getattr(obj, 'is_active', True) == False: + raise Http404() if not request.user.is_superuser and not self.delete_permissions_check(request, obj): raise PermissionDenied() if isinstance(obj, PrimordialModel): diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index f5a6e8446b..085b26fad8 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -131,11 +131,14 @@ class UserHelper(object): 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): - print "N1" return False rc = cls.can_user_administrate(user, obj) return rc + @classmethod + def can_user_unattach(cls, user, obj, sub_obj, relationship_type): + return cls.can_user_administrate(user, obj) + class PrimordialModel(models.Model): ''' common model for all object types that have these standard fields @@ -548,6 +551,13 @@ class Credential(CommonModelNameNotUnique): return True return False + @classmethod + def can_user_delete(cls, user, obj): + if obj.user is None and obj.team is None: + # unassociated credentials may be marked deleted by anyone + return True + return cls.can_user_administrate(user,obj) + @classmethod def can_user_read(cls, user, obj): ''' a user can be read if they are on the same team or can be administrated ''' diff --git a/lib/main/tests/projects.py b/lib/main/tests/projects.py index 1c661ce32f..4503720362 100644 --- a/lib/main/tests/projects.py +++ b/lib/main/tests/projects.py @@ -342,19 +342,29 @@ class ProjectsTest(BaseTest): # editing a credential to edit the user record is not legal, this is a test of the .validate # method on the serializer to allow 'write once' fields self.put(edit_creds1, data=d_cred_user2, expect=400, auth=self.get_normal_credentials()) - self.put(edit_creds1, data=d_cred_user, expect=200, auth=self.get_other_credentials()) + cred_put_u = self.put(edit_creds1, data=d_cred_user, expect=200, auth=self.get_other_credentials()) self.put(edit_creds2, data=d_cred_team, expect=401) self.put(edit_creds2, data=d_cred_team, expect=401, auth=self.get_invalid_credentials()) cred_team = Credential.objects.get(pk=cred_team.pk) self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_super_credentials()) cred_team = Credential.objects.get(pk=cred_team.pk) - self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_normal_credentials()) + cred_put_t = self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_normal_credentials()) self.put(edit_creds2, data=d_cred_team, expect=403, auth=self.get_other_credentials()) + cred_put_t['disassociate'] = 1 + team_url = "/api/v1/teams/%s/credentials/" % cred_put_t['team'] + self.post(team_url, data=cred_put_t, expect=204, auth=self.get_normal_credentials()) + # can remove credentials from a user (via disassociate) - # can remove credentials from a team (via disassociate) - # can delete a credential directly + cred_put_u['disassociate'] = 1 + url = cred_put_u['url'] + user_url = "/api/v1/users/%s/credentials/" % cred_put_u['user'] + self.post(user_url, data=cred_put_u, expect=204, auth=self.get_normal_credentials()) + + # can delete a credential directly -- probably won't be used too often + data = self.delete(url, expect=204, auth=self.get_other_credentials()) + data = self.delete(url, expect=404, auth=self.get_other_credentials()) # ===================================================================== # PERMISSIONS