ask_for_inventory permissions and runtime tests finished

cleanup prompt-for additions

update migration after rebase
This commit is contained in:
AlanCoding
2016-04-06 14:19:22 -04:00
parent 19b855a4d3
commit bea15021b3
6 changed files with 175 additions and 87 deletions

View File

@@ -2109,7 +2109,8 @@ class JobLaunchSerializer(BaseSerializer):
class Meta: class Meta:
model = JobTemplate model = JobTemplate
fields = ('can_start_without_user_input', 'passwords_needed_to_start', 'extra_vars', fields = ('can_start_without_user_input', 'passwords_needed_to_start',
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
'ask_variables_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_variables_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch',
'ask_inventory_on_launch', 'ask_limit_on_launch', 'ask_inventory_on_launch', 'ask_limit_on_launch',
'survey_enabled', 'variables_needed_to_start', 'survey_enabled', 'variables_needed_to_start',
@@ -2121,11 +2122,6 @@ class JobLaunchSerializer(BaseSerializer):
'credential': { 'credential': {
'write_only': True, 'write_only': True,
}, },
'limit': {'write_only': True},
'job_tags': {'write_only': True},
'skip_tags': {'write_only': True},
'job_type': {'write_only': True},
'inventory': {'write_only': True},
} }
def get_credential_needed_to_start(self, obj): def get_credential_needed_to_start(self, obj):
@@ -2182,8 +2178,18 @@ class JobLaunchSerializer(BaseSerializer):
raise serializers.ValidationError(errors) raise serializers.ValidationError(errors)
JT_extra_vars = obj.extra_vars JT_extra_vars = obj.extra_vars
JT_limit = obj.limit
JT_job_type = obj.job_type
JT_job_tags = obj.job_tags
JT_skip_tags = obj.skip_tags
JT_inventory = obj.inventory
attrs = super(JobLaunchSerializer, self).validate(attrs) attrs = super(JobLaunchSerializer, self).validate(attrs)
obj.extra_vars = JT_extra_vars obj.extra_vars = JT_extra_vars
obj.limit = JT_limit
obj.job_type = JT_job_type
obj.skip_tags = JT_skip_tags
obj.job_tags = JT_job_tags
obj.inventory = JT_inventory
return attrs return attrs
class NotifierSerializer(BaseSerializer): class NotifierSerializer(BaseSerializer):

View File

@@ -2097,11 +2097,11 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
obj = self.get_object() obj = self.get_object()
if not request.user.can_access(self.model, 'start', obj):
raise PermissionDenied()
if 'credential' not in request.data and 'credential_id' in request.data: if 'credential' not in request.data and 'credential_id' in request.data:
request.data['credential'] = request.data['credential_id'] request.data['credential'] = request.data['credential_id']
if 'inventory' not in request.data and 'inventory_id' in request.data:
request.data['inventory'] = request.data['inventory_id']
passwords = {} passwords = {}
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords}) serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
@@ -2116,12 +2116,22 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
'credential': serializer.instance.credential.pk, 'credential': serializer.instance.credential.pk,
} }
prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(user=self.request.user, **request.data) prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(**request.data)
if 'inventory' in prompted_fields:
new_inventory = Inventory.objects.get(pk=prompted_fields['inventory'])
if not request.user.can_access(Inventory, 'read', new_inventory):
raise PermissionDenied()
kv.update(prompted_fields) kv.update(prompted_fields)
kv.update(passwords) kv.update(passwords)
new_job = obj.create_unified_job(**kv) new_job = obj.create_unified_job(**kv)
if not request.user.can_access(Job, 'start', new_job):
new_job.delete()
raise PermissionDenied()
result = new_job.signal_start(**kv) result = new_job.signal_start(**kv)
if not result: if not result:
data = dict(passwords_needed_to_start=new_job.passwords_needed_to_start) data = dict(passwords_needed_to_start=new_job.passwords_needed_to_start)

View File

@@ -8,7 +8,7 @@ from django.conf import settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('main', '0012_v300_create_labels'), ('main', '0013_v300_label_changes'),
] ]
operations = [ operations = [

View File

@@ -378,10 +378,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
kwargs['extra_vars'] = json.dumps(extra_vars) kwargs['extra_vars'] = json.dumps(extra_vars)
return kwargs return kwargs
def _accept_or_ignore_job_kwargs(self, user, **kwargs): def _accept_or_ignore_job_kwargs(self, **kwargs):
# Sort the runtime fields allowed and disallowed by job template # Sort the runtime fields allowed and disallowed by job template
ignored_fields = {} ignored_fields = {}
prompted_fields = {} prompted_fields = {}
if 'extra_vars' in kwargs: if 'extra_vars' in kwargs:
prompted_fields['extra_vars'] = {} prompted_fields['extra_vars'] = {}
ignored_fields['extra_vars'] = {} ignored_fields['extra_vars'] = {}
@@ -401,40 +402,26 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
# No survey & prompt flag is false - ignore all # No survey & prompt flag is false - ignore all
ignored_fields['extra_vars'] = kwargs['extra_vars'] ignored_fields['extra_vars'] = kwargs['extra_vars']
if 'limit' in kwargs: # Fields which all follow the same pattern
if self.ask_limit_on_launch: ask_for_field_dict = dict(
prompted_fields['limit'] = kwargs['limit'] limit=self.ask_limit_on_launch,
else: job_tags=self.ask_tags_on_launch,
ignored_fields['limit'] = kwargs['limit'] skip_tags=self.ask_tags_on_launch,
job_type=self.ask_job_type_on_launch,
inventory=self.ask_inventory_on_launch
)
if 'job_tags' or 'skip_tags' in kwargs: for field in ask_for_field_dict:
if self.ask_tags_on_launch: if field in kwargs:
if 'job_tags' in kwargs: if ask_for_field_dict[field]:
prompted_fields['job_tags'] = kwargs['job_tags'] prompted_fields[field] = kwargs[field]
if 'skip_tags' in kwargs:
prompted_fields['skip_tags'] = kwargs['skip_tags']
else:
if 'job_tags' in kwargs:
ignored_fields['job_tags'] = kwargs['job_tags']
if 'skip_tags' in kwargs:
ignored_fields['skip_tags'] = kwargs['skip_tags']
if 'job_type' in kwargs:
if self.ask_job_type_on_launch:
prompted_fields['job_type'] = kwargs['job_type']
else:
ignored_fields['job_type'] = kwargs['job_type']
if 'inventory' in kwargs:
inv_id = kwargs['inventory']
if self.ask_inventory_on_launch:
from awx.main.models.inventory import Inventory
if Inventory.objects.get(pk=inv_id).accessible_by(user, {'write': True}):
prompted_fields['inventory'] = inv_id
else: else:
ignored_fields['inventory'] = inv_id ignored_fields[field] = kwargs[field]
else:
ignored_fields['inventory'] = inv_id if prompted_fields.get('job_type', None) == 'scan' or self.job_type == 'scan':
if 'inventory' in prompted_fields:
ignored_fields['inventory'] = prompted_fields.pop('inventory')
return prompted_fields, ignored_fields return prompted_fields, ignored_fields
@property @property

View File

@@ -3,29 +3,35 @@ import yaml
from awx.api.serializers import JobLaunchSerializer from awx.api.serializers import JobLaunchSerializer
from awx.main.models.credential import Credential from awx.main.models.credential import Credential
from awx.main.models.inventory import Inventory
from awx.main.models.jobs import Job, JobTemplate from awx.main.models.jobs import Job, JobTemplate
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from copy import copy
@pytest.fixture @pytest.fixture
def runtime_data(): def runtime_data(organization):
cred_obj = Credential.objects.create(name='runtime-cred', kind='ssh', username='test_user2', password='pas4word2') cred_obj = Credential.objects.create(name='runtime-cred', kind='ssh', username='test_user2', password='pas4word2')
inv_obj = organization.inventories.create(name="runtime-inv")
return dict( return dict(
extra_vars='{"job_launch_var": 4}',
limit='test-servers', limit='test-servers',
job_type='check', job_type='check',
inventory=cred_obj.pk, job_tags='provision',
job_tags='["provision"]', skip_tags='restart',
skip_tags='["restart"]', inventory=inv_obj.pk,
extra_vars='{"job_launch_var": 4}' credential=cred_obj.pk,
) )
@pytest.fixture @pytest.fixture
def job_template_prompts(project, inventory, machine_credential): def job_template_prompts(project, inventory, machine_credential):
def rf(on_off): def rf(on_off):
return JobTemplate.objects.create( return JobTemplate.objects.create(
job_type='run', project=project, inventory=inventory, job_type='run',
credential=machine_credential, name='deploy-job-template', project=project,
inventory=inventory,
credential=machine_credential,
name='deploy-job-template',
ask_variables_on_launch=on_off, ask_variables_on_launch=on_off,
ask_tags_on_launch=on_off, ask_tags_on_launch=on_off,
ask_job_type_on_launch=on_off, ask_job_type_on_launch=on_off,
@@ -34,42 +40,25 @@ def job_template_prompts(project, inventory, machine_credential):
) )
return rf return rf
# Probably remove this test after development is finished
@pytest.mark.django_db
def test_job_launch_prompts_echo(job_template_prompts, get, user):
job_template = job_template_prompts(True)
assert job_template.ask_variables_on_launch
url = reverse('api:job_template_launch', args=[job_template.pk])
response = get(
reverse('api:job_template_launch', args=[job_template.pk]),
user('admin', True))
# Just checking that the GET response has what we expect
assert response.data['ask_variables_on_launch']
assert response.data['ask_tags_on_launch']
assert response.data['ask_job_type_on_launch']
assert response.data['ask_inventory_on_launch']
assert response.data['ask_limit_on_launch']
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, user): def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(False) job_template = job_template_prompts(False)
job_template_saved = copy(job_template)
response = post( response = post(reverse('api:job_template_launch', args=[job_template.pk]),
reverse('api:job_template_launch', args=[job_template.pk]), runtime_data, user('admin', True))
runtime_data, user('admin', True))
assert response.status_code == 202
job_id = response.data['job'] job_id = response.data['job']
job_obj = Job.objects.get(pk=job_id) job_obj = Job.objects.get(pk=job_id)
# Check that job data matches job_template data # Check that job data matches job_template data
assert len(yaml.load(job_obj.extra_vars)) == 0 assert len(yaml.load(job_obj.extra_vars)) == 0
assert job_obj.limit == job_template.limit assert job_obj.limit == job_template_saved.limit
assert job_obj.job_type == job_template.job_type assert job_obj.job_type == job_template_saved.job_type
assert job_obj.inventory.pk == job_template.inventory.pk assert job_obj.inventory.pk == job_template_saved.inventory.pk
assert job_obj.job_tags == job_template.job_tags assert job_obj.job_tags == job_template_saved.job_tags
# Check that response tells us what things were ignored # Check that response tells us what things were ignored
assert 'job_launch_var' in response.data['ignored_fields']['extra_vars'] assert 'job_launch_var' in response.data['ignored_fields']['extra_vars']
@@ -80,13 +69,18 @@ def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, us
assert 'skip_tags' in response.data['ignored_fields'] assert 'skip_tags' in response.data['ignored_fields']
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user): def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(True) job_template = job_template_prompts(True)
admin_user = user('admin', True)
response = post( job_template.inventory.executor_role.members.add(admin_user)
reverse('api:job_template_launch', args=[job_template.pk]), job_template.inventory.save()
runtime_data, user('admin', True))
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
runtime_data, user('admin', True))
assert response.status_code == 202
job_id = response.data['job'] job_id = response.data['job']
job_obj = Job.objects.get(pk=job_id) job_obj = Job.objects.get(pk=job_id)
@@ -97,6 +91,101 @@ def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user
assert job_obj.inventory.pk == runtime_data['inventory'] assert job_obj.inventory.pk == runtime_data['inventory']
assert job_obj.job_tags == runtime_data['job_tags'] assert job_obj.job_tags == runtime_data['job_tags']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_reject_invalid_prompted_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(True)
response = post(
reverse('api:job_template_launch', args=[job_template.pk]),
dict(job_type='foobicate', # foobicate is not a valid job type
inventory=87865), user('admin', True))
assert response.status_code == 400
assert response.data['job_type'] == [u'"foobicate" is not a valid choice.']
assert response.data['inventory'] == [u'Invalid pk "87865" - object does not exist.']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_reject_invalid_prompted_extra_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(True)
response = post(
reverse('api:job_template_launch', args=[job_template.pk]),
dict(extra_vars='{"unbalanced brackets":'), user('admin', True))
assert response.status_code == 400
assert response.data['extra_vars'] == ['Must be valid JSON or YAML']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, user):
deploy_jobtemplate.inventory = None
deploy_jobtemplate.save()
response = post(reverse('api:job_template_launch',
args=[deploy_jobtemplate.pk]), {}, user('admin', True))
assert response.status_code == 400
assert response.data['inventory'] == ['Job Template Inventory is missing or undefined']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_launch_fails_without_inventory_access(deploy_jobtemplate, machine_credential, post, user):
deploy_jobtemplate.ask_inventory_on_launch = True
deploy_jobtemplate.credential = machine_credential
common_user = user('test-user', False)
# TODO: Change admin_role to executor_role once issue #1422 is resolved
deploy_jobtemplate.admin_role.members.add(common_user)
deploy_jobtemplate.save()
deploy_jobtemplate.inventory.executor_role.members.add(common_user)
deploy_jobtemplate.inventory.save()
deploy_jobtemplate.project.member_role.members.add(common_user)
deploy_jobtemplate.project.save()
# TODO: change owner_role to usage_role after fix
deploy_jobtemplate.credential.owner_role.members.add(common_user)
deploy_jobtemplate.credential.save()
# Assure that the base job template can be launched to begin with
response = post(reverse('api:job_template_launch',
args=[deploy_jobtemplate.pk]), {}, common_user)
assert response.status_code == 202
# Assure that giving an inventory without access to the inventory blocks the launch
new_inv = deploy_jobtemplate.project.organization.inventories.create(name="user-can-not-use")
response = post(reverse('api:job_template_launch', args=[deploy_jobtemplate.pk]),
dict(inventory=new_inv.pk), common_user)
assert response.status_code == 403
assert response.data['detail'] == u'You do not have permission to perform this action.'
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_relaunch_prompted_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(True)
admin_user = user('admin', True)
# Launch job, overwriting several JT fields
first_response = post(reverse('api:job_template_launch', args=[job_template.pk]),
runtime_data, admin_user)
assert first_response.status_code == 202
original_job = Job.objects.get(pk=first_response.data['job'])
# Launch a second job as a relaunch of the first
second_response = post(reverse('api:job_relaunch', args=[original_job.pk]),
{}, admin_user)
relaunched_job = Job.objects.get(pk=second_response.data['job'])
# Check that job data matches the original runtime variables
assert first_response.status_code == 202
assert 'job_launch_var' in yaml.load(relaunched_job.extra_vars)
assert relaunched_job.limit == runtime_data['limit']
assert relaunched_job.job_type == runtime_data['job_type']
assert relaunched_job.inventory.pk == runtime_data['inventory']
assert relaunched_job.job_tags == runtime_data['job_tags']
@pytest.mark.django_db @pytest.mark.django_db
def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate): def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
deploy_jobtemplate.extra_vars = '{"job_template_var": 3}' deploy_jobtemplate.extra_vars = '{"job_template_var": 3}'
@@ -118,6 +207,7 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
assert 'job_launch_var' in final_job_extra_vars assert 'job_launch_var' in final_job_extra_vars
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_launch_unprompted_vars_with_survey(job_template_prompts, post, user): def test_job_launch_unprompted_vars_with_survey(job_template_prompts, post, user):
job_template = job_template_prompts(False) job_template = job_template_prompts(False)
job_template.survey_enabled = True job_template.survey_enabled = True
@@ -149,11 +239,7 @@ def test_job_launch_unprompted_vars_with_survey(job_template_prompts, post, user
job_id = response.data['job'] job_id = response.data['job']
job_obj = Job.objects.get(pk=job_id) job_obj = Job.objects.get(pk=job_id)
# Check that the survey variable is accept and the job variable isn't # Check that the survey variable is accepted and the job variable isn't
job_extra_vars = yaml.load(job_obj.extra_vars) job_extra_vars = yaml.load(job_obj.extra_vars)
assert 'job_launch_var' not in job_extra_vars assert 'job_launch_var' not in job_extra_vars
assert 'survey_var' in job_extra_vars assert 'survey_var' in job_extra_vars
# To add:
# permissions testing (can't provide inventory you can't run against)
# credentials/password test if they will be included in response format

View File

@@ -19,8 +19,7 @@ else
echo "Failed to find tower source tree, map your development tree volume" echo "Failed to find tower source tree, map your development tree volume"
fi fi
# will remove before PR lands cp -nR /tmp/ansible_tower.egg-info /tower_devel/ || true
cp -fR /tmp/ansible_tower.egg-info /tower_devel/ || true
# Check if we need to build dependencies # Check if we need to build dependencies
#if [ -f "awx/lib/.deps_built" ]; then #if [ -f "awx/lib/.deps_built" ]; then