Fix credential leak when copying Job Templates.

Signed-off-by: Yunfan Zhang <yz322@duke.edu>
This commit is contained in:
Yunfan Zhang
2018-07-23 16:35:03 -04:00
parent a04d3f817a
commit cb6d7dfe69
4 changed files with 35 additions and 4 deletions

View File

@@ -93,7 +93,7 @@ class LoggedLoginView(auth_views.LoginView):
current_user = JSONRenderer().render(current_user.data) current_user = JSONRenderer().render(current_user.data)
current_user = urllib.quote('%s' % current_user, '') current_user = urllib.quote('%s' % current_user, '')
ret.set_cookie('current_user', current_user) ret.set_cookie('current_user', current_user)
return ret return ret
else: else:
ret.status_code = 401 ret.status_code = 401
@@ -755,6 +755,7 @@ class DeleteLastUnattachLabelMixin(object):
when the last disassociate is called should inherit from this class. Further, when the last disassociate is called should inherit from this class. Further,
the model should implement is_detached() the model should implement is_detached()
''' '''
def unattach(self, request, *args, **kwargs): def unattach(self, request, *args, **kwargs):
(sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request) (sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request)
if res: if res:
@@ -945,7 +946,9 @@ class CopyAPIView(GenericAPIView):
create_kwargs = self._build_create_dict(obj) create_kwargs = self._build_create_dict(obj)
for key in create_kwargs: for key in create_kwargs:
create_kwargs[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key] create_kwargs[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key]
return Response({'can_copy': request.user.can_access(self.model, 'add', create_kwargs)}) can_copy = request.user.can_access(self.model, 'add', create_kwargs) and \
request.user.can_access(self.model, 'copy_related', obj)
return Response({'can_copy': can_copy})
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if get_request_version(request) < 2: if get_request_version(request) < 2:
@@ -957,6 +960,8 @@ class CopyAPIView(GenericAPIView):
create_kwargs_check[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key] create_kwargs_check[key] = getattr(create_kwargs[key], 'pk', None) or create_kwargs[key]
if not request.user.can_access(self.model, 'add', create_kwargs_check): if not request.user.can_access(self.model, 'add', create_kwargs_check):
raise PermissionDenied() raise PermissionDenied()
if not request.user.can_access(self.model, 'copy_related', obj):
raise PermissionDenied()
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -6,6 +6,7 @@ import os
import sys import sys
import logging import logging
import six import six
from functools import reduce
# Django # Django
from django.conf import settings from django.conf import settings
@@ -218,6 +219,15 @@ class BaseAccess(object):
def can_copy(self, obj): def can_copy(self, obj):
return self.can_add({'reference_obj': obj}) return self.can_add({'reference_obj': obj})
def can_copy_related(self, obj):
'''
can_copy_related() should only be used to check if the user have access to related
many to many credentials in when copying the object. It does not check if the user
has permission for any other related objects. Therefore, when checking if the user
can copy an object, it should always be used in conjunction with can_add()
'''
return True
def can_attach(self, obj, sub_obj, relationship, data, def can_attach(self, obj, sub_obj, relationship, data,
skip_sub_obj_read_check=False): skip_sub_obj_read_check=False):
if skip_sub_obj_read_check: if skip_sub_obj_read_check:
@@ -1328,6 +1338,17 @@ class JobTemplateAccess(BaseAccess):
return self.user in project.use_role return self.user in project.use_role
else: else:
return False return False
@check_superuser
def can_copy_related(self, obj):
'''
Check if we have access to all the credentials related to Job Templates.
Does not verify the user's permission for any other related fields (projects, inventories, etc).
'''
# obj.credentials.all() is accessible ONLY when object is saved (has valid id)
credential_manager = getattr(obj, 'credentials', None) if getattr(obj, 'id', False) else Credentials.objects.none()
return reduce(lambda prev, cred: prev and self.user in cred.use_role, credential_manager.all(), True)
def can_start(self, obj, validate_license=True): def can_start(self, obj, validate_license=True):
# Check license. # Check license.

View File

@@ -18,6 +18,8 @@ def test_job_template_copy(post, get, project, inventory, machine_credential, va
job_template_with_survey_passwords.credentials.add(machine_credential) job_template_with_survey_passwords.credentials.add(machine_credential)
job_template_with_survey_passwords.credentials.add(vault_credential) job_template_with_survey_passwords.credentials.add(vault_credential)
job_template_with_survey_passwords.admin_role.members.add(alice) job_template_with_survey_passwords.admin_role.members.add(alice)
project.admin_role.members.add(alice)
inventory.admin_role.members.add(alice)
assert get( assert get(
reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}), reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}),
alice, expect=200 alice, expect=200
@@ -26,6 +28,10 @@ def test_job_template_copy(post, get, project, inventory, machine_credential, va
reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}), reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}),
admin, expect=200 admin, expect=200
).data['can_copy'] is True ).data['can_copy'] is True
post(
reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}),
{'name': 'new jt name'}, alice, expect=403
)
jt_copy_pk = post( jt_copy_pk = post(
reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}), reverse('api:job_template_copy', kwargs={'pk': job_template_with_survey_passwords.pk}),
{'name': 'new jt name'}, admin, expect=201 {'name': 'new jt name'}, admin, expect=201

View File

@@ -97,7 +97,6 @@ class TestJobTemplateSerializerGetSummaryFields():
are put into the serializer user_capabilities""" are put into the serializer user_capabilities"""
jt_obj = job_template_factory('testJT', project='proj1', persisted=False).job_template jt_obj = job_template_factory('testJT', project='proj1', persisted=False).job_template
jt_obj.id = 5
jt_obj.admin_role = Role(id=9, role_field='admin_role') jt_obj.admin_role = Role(id=9, role_field='admin_role')
jt_obj.execute_role = Role(id=8, role_field='execute_role') jt_obj.execute_role = Role(id=8, role_field='execute_role')
jt_obj.read_role = Role(id=7, role_field='execute_role') jt_obj.read_role = Role(id=7, role_field='execute_role')
@@ -115,7 +114,7 @@ class TestJobTemplateSerializerGetSummaryFields():
with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'): with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'):
with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'): with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'):
with mocker.patch("awx.main.access.JobTemplateAccess.can_add", return_value='foo'): with mocker.patch("awx.main.access.JobTemplateAccess.can_copy", return_value='foo'):
with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None): with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None):
response = serializer.get_summary_fields(jt_obj) response = serializer.get_summary_fields(jt_obj)