Working on surfacing credentials via REST.

This commit is contained in:
Michael DeHaan
2013-04-02 14:59:58 -04:00
parent 37cdd31b79
commit b20a29b458
7 changed files with 97 additions and 59 deletions

View File

@@ -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))

View File

@@ -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
))

View File

@@ -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'
)

View File

@@ -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',

View File

@@ -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()

View File

@@ -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

View File

@@ -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<pk>[0-9]+)/$', views_ProjectsDetail),
url(r'^api/v1/projects/(?P<pk>[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<pk>[0-9]+)/$', views_TeamsDetail),
url(r'^api/v1/teams/(?P<pk>[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<pk>[0-9]+)/$', views_CredentialsDetail),
# permissions services
# ... users