mirror of
https://github.com/ansible/awx.git
synced 2026-02-24 22:46:01 -03:30
Working on surfacing credentials via REST.
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
11
lib/urls.py
11
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<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
|
||||
|
||||
Reference in New Issue
Block a user