replace all Job/JT relations with a single M2M credentials relation

Includes backwards compatibility for now-deprecated .credential,
.vault_credential, and .extra_credentials

This is a building block for multi-vault implementation and Alan's saved
launch configurations (both coming soon)

see: https://github.com/ansible/awx/issues/352
see: https://github.com/ansible/awx/issues/169
This commit is contained in:
Ryan Petrello 2017-11-02 11:35:20 -04:00
parent f887aaa71f
commit 28ce9b700e
No known key found for this signature in database
GPG Key ID: F2AA5F2122351777
36 changed files with 1082 additions and 959 deletions

View File

@ -269,8 +269,10 @@ class FieldLookupBackend(BaseFilterBackend):
# Make legacy v1 Job/Template fields work for backwards compatability
# TODO: remove after API v1 deprecation period
if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in ('cloud_credential', 'network_credential'):
key = 'extra_credentials'
if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in (
'credential', 'vault_credential', 'cloud_credential', 'network_credential'
):
key = 'credentials'
# Make legacy v1 Credential fields work for backwards compatability
# TODO: remove after API v1 deprecation period

View File

@ -189,6 +189,7 @@ class APIView(views.APIView):
'new_in_300': getattr(self, 'new_in_300', False),
'new_in_310': getattr(self, 'new_in_310', False),
'new_in_320': getattr(self, 'new_in_320', False),
'new_in_330': getattr(self, 'new_in_330', False),
'new_in_api_v2': getattr(self, 'new_in_api_v2', False),
'deprecated': getattr(self, 'deprecated', False),
}

View File

@ -54,6 +54,8 @@ from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, Ver
logger = logging.getLogger('awx.api.serializers')
DEPRECATED = 'This resource has been deprecated and will be removed in a future release'
# Fields that should be summarized regardless of object type.
DEFAULT_SUMMARY_FIELDS = ('id', 'name', 'description')# , 'created_by', 'modified_by')#, 'type')
@ -538,6 +540,13 @@ class BaseSerializer(serializers.ModelSerializer):
kwargs['request'] = self.context.get('request')
return reverse(*args, **kwargs)
@property
def is_detail_view(self):
if 'view' in self.context:
if 'pk' in self.context['view'].kwargs:
return True
return False
class EmptySerializer(serializers.Serializer):
pass
@ -2286,8 +2295,8 @@ class V1JobOptionsSerializer(BaseSerializer):
fields = ('*', 'cloud_credential', 'network_credential')
V1_FIELDS = {
'cloud_credential': models.PositiveIntegerField(blank=True, null=True, default=None),
'network_credential': models.PositiveIntegerField(blank=True, null=True, default=None)
'cloud_credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
'network_credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
}
def build_field(self, field_name, info, model_class, nested_depth):
@ -2297,20 +2306,41 @@ class V1JobOptionsSerializer(BaseSerializer):
return super(V1JobOptionsSerializer, self).build_field(field_name, info, model_class, nested_depth)
@six.add_metaclass(BaseSerializerMetaclass)
class LegacyCredentialFields(BaseSerializer):
class Meta:
model = Credential
fields = ('*', 'credential', 'vault_credential')
LEGACY_FIELDS = {
'credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
'vault_credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
}
def build_field(self, field_name, info, model_class, nested_depth):
if field_name in self.LEGACY_FIELDS:
return self.build_standard_field(field_name,
self.LEGACY_FIELDS[field_name])
return super(LegacyCredentialFields, self).build_field(field_name, info, model_class, nested_depth)
class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
class Meta:
fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
'credential', 'vault_credential', 'forks', 'limit',
'verbosity', 'extra_vars', 'job_tags', 'force_handlers',
'skip_tags', 'start_at_task', 'timeout', 'use_fact_cache',)
'forks', 'limit', 'verbosity', 'extra_vars', 'job_tags',
'force_handlers', 'skip_tags', 'start_at_task', 'timeout',
'use_fact_cache',)
def get_fields(self):
fields = super(JobOptionsSerializer, self).get_fields()
# TODO: remove when API v1 is removed
if self.version == 1 and 'credential' in self.Meta.fields:
if self.version == 1:
fields.update(V1JobOptionsSerializer().get_fields())
fields.update(LegacyCredentialFields().get_fields())
return fields
def get_related(self, obj):
@ -2321,17 +2351,22 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
if obj.project:
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
if obj.credential:
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential.pk})
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential})
if obj.vault_credential:
res['vault_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.vault_credential.pk})
res['vault_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.vault_credential})
if self.version > 1:
if isinstance(obj, UnifiedJobTemplate):
res['extra_credentials'] = self.reverse(
'api:job_template_extra_credentials_list',
kwargs={'pk': obj.pk}
)
res['credentials'] = self.reverse(
'api:job_template_credentials_list',
kwargs={'pk': obj.pk}
)
elif isinstance(obj, UnifiedJob):
res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk})
res['credentials'] = self.reverse('api:job_credentials_list', kwargs={'pk': obj.pk})
else:
cloud_cred = obj.cloud_credential
if cloud_cred:
@ -2352,64 +2387,67 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
ret['project'] = None
if 'playbook' in ret:
ret['playbook'] = ''
if 'credential' in ret and not obj.credential:
ret['credential'] = None
if 'vault_credential' in ret and not obj.vault_credential:
ret['vault_credential'] = None
if self.version == 1 and 'credential' in self.Meta.fields:
ret['credential'] = obj.credential
ret['vault_credential'] = obj.vault_credential
if self.version == 1:
ret['cloud_credential'] = obj.cloud_credential
ret['network_credential'] = obj.network_credential
return ret
def create(self, validated_data):
deprecated_fields = {}
for key in ('cloud_credential', 'network_credential'):
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
if key in validated_data:
deprecated_fields[key] = validated_data.pop(key)
obj = super(JobOptionsSerializer, self).create(validated_data)
if self.version == 1 and deprecated_fields: # TODO: remove in 3.3
if deprecated_fields: # TODO: remove in 3.3
self._update_deprecated_fields(deprecated_fields, obj)
return obj
def update(self, obj, validated_data):
deprecated_fields = {}
for key in ('cloud_credential', 'network_credential'):
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
if key in validated_data:
deprecated_fields[key] = validated_data.pop(key)
obj = super(JobOptionsSerializer, self).update(obj, validated_data)
if self.version == 1 and deprecated_fields: # TODO: remove in 3.3
if deprecated_fields: # TODO: remove in 3.3
self._update_deprecated_fields(deprecated_fields, obj)
return obj
def _update_deprecated_fields(self, fields, obj):
for key, existing in (
('credential', obj.credentials.filter(credential_type__kind='ssh')),
('vault_credential', obj.credentials.filter(credential_type__kind='vault')),
('cloud_credential', obj.cloud_credentials),
('network_credential', obj.network_credentials),
):
if key in fields:
for cred in existing:
obj.extra_credentials.remove(cred)
obj.credentials.remove(cred)
if fields[key]:
obj.extra_credentials.add(fields[key])
obj.credentials.add(fields[key])
obj.save()
def validate(self, attrs):
v1_credentials = {}
view = self.context.get('view', None)
if self.version == 1: # TODO: remove in 3.3
for attr, kind, error in (
('cloud_credential', 'cloud', _('You must provide a cloud credential.')),
('network_credential', 'net', _('You must provide a network credential.'))
):
if attr in attrs:
v1_credentials[attr] = None
pk = attrs.pop(attr)
if pk:
cred = v1_credentials[attr] = Credential.objects.get(pk=pk)
if cred.credential_type.kind != kind:
raise serializers.ValidationError({attr: error})
if (not view) or (not view.request) or (view.request.user not in cred.use_role):
raise PermissionDenied()
for attr, kind, error in (
('cloud_credential', 'cloud', _('You must provide a cloud credential.')),
('network_credential', 'net', _('You must provide a network credential.')),
('credential', 'ssh', _('You must provide an SSH credential.')),
('vault_credential', 'vault', _('You must provide a vault credential.')),
):
if kind in ('cloud', 'net') and self.version > 1:
continue # cloud and net deprecated creds are v1 only
if attr in attrs:
v1_credentials[attr] = None
pk = attrs.pop(attr)
if pk:
cred = v1_credentials[attr] = Credential.objects.get(pk=pk)
if cred.credential_type.kind != kind:
raise serializers.ValidationError({attr: error})
if view and view.request and view.request.user not in cred.use_role:
raise PermissionDenied()
if 'project' in self.fields and 'playbook' in self.fields:
project = attrs.get('project', self.instance and self.instance.project or None)
@ -2492,19 +2530,11 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)
inventory = get_field_from_model_or_attrs('inventory')
credential = get_field_from_model_or_attrs('credential')
vault_credential = get_field_from_model_or_attrs('vault_credential')
project = get_field_from_model_or_attrs('project')
prompting_error_message = _("Must either set a default value or ask to prompt on launch.")
if project is None:
raise serializers.ValidationError({'project': _("Job types 'run' and 'check' must have assigned a project.")})
elif all([
credential is None,
vault_credential is None,
not get_field_from_model_or_attrs('ask_credential_on_launch'),
]):
raise serializers.ValidationError({'credential': prompting_error_message})
elif inventory is None and not get_field_from_model_or_attrs('ask_inventory_on_launch'):
raise serializers.ValidationError({'inventory': prompting_error_message})
@ -2515,17 +2545,27 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
def get_summary_fields(self, obj):
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
if 'pk' in self.context['view'].kwargs and self.version > 1: # TODO: remove version check in 3.3
if self.is_detail_view:
all_creds = []
extra_creds = []
for cred in obj.extra_credentials.all():
extra_creds.append({
for cred in obj.credentials.all():
summarized_cred = {
'id': cred.pk,
'name': cred.name,
'description': cred.description,
'kind': cred.kind,
'credential_type_id': cred.credential_type_id
})
summary_fields['extra_credentials'] = extra_creds
}
all_creds.append(summarized_cred)
if cred.credential_type.kind in ('cloud', 'net'):
extra_creds.append(summarized_cred)
elif cred.credential_type.kind == 'ssh':
summary_fields['credential'] = summarized_cred
elif cred.credential_type.kind == 'vault':
summary_fields['vault_credential'] = summarized_cred
if self.version > 1:
summary_fields['extra_credentials'] = extra_creds
summary_fields['credentials'] = all_creds
return summary_fields
@ -2618,17 +2658,27 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
def get_summary_fields(self, obj):
summary_fields = super(JobSerializer, self).get_summary_fields(obj)
if 'pk' in self.context['view'].kwargs and self.version > 1: # TODO: remove version check in 3.3
if self.is_detail_view: # TODO: remove version check in 3.3
all_creds = []
extra_creds = []
for cred in obj.extra_credentials.all():
extra_creds.append({
for cred in obj.credentials.all():
summarized_cred = {
'id': cred.pk,
'name': cred.name,
'description': cred.description,
'kind': cred.kind,
'credential_type_id': cred.credential_type_id
})
summary_fields['extra_credentials'] = extra_creds
}
all_creds.append(summarized_cred)
if cred.credential_type.kind in ('cloud', 'net'):
extra_creds.append(summarized_cred)
elif cred.credential_type.kind == 'ssh':
summary_fields['credential'] = summarized_cred
elif cred.credential_type.kind == 'vault':
summary_fields['vault_credential'] = summarized_cred
if self.version > 1:
summary_fields['extra_credentials'] = extra_creds
summary_fields['credentials'] = all_creds
return summary_fields
@ -3250,7 +3300,7 @@ class JobLaunchSerializer(BaseSerializer):
model = JobTemplate
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
'credential', 'extra_credentials', 'ask_variables_on_launch', 'ask_tags_on_launch',
'credentials', 'ask_variables_on_launch', 'ask_tags_on_launch',
'ask_diff_mode_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_limit_on_launch',
'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch',
'survey_enabled', 'variables_needed_to_start', 'credential_needed_to_start',
@ -3260,8 +3310,7 @@ class JobLaunchSerializer(BaseSerializer):
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch',
'ask_inventory_on_launch', 'ask_credential_on_launch',)
extra_kwargs = {
'credential': {'write_only': True,},
'extra_credentials': {'write_only': True, 'default': [], 'allow_empty': True},
'credentials': {'write_only': True, 'default': [], 'allow_empty': True},
'limit': {'write_only': True,},
'job_tags': {'write_only': True,},
'skip_tags': {'write_only': True,},
@ -3270,15 +3319,8 @@ class JobLaunchSerializer(BaseSerializer):
'verbosity': {'write_only': True,}
}
# TODO: remove in 3.3
def get_fields(self):
ret = super(JobLaunchSerializer, self).get_fields()
if self.version == 1:
ret.pop('extra_credentials')
return ret
def get_credential_needed_to_start(self, obj):
return not (obj and obj.credential)
return False
def get_inventory_needed_to_start(self, obj):
return not (obj and obj.inventory)
@ -3293,11 +3335,15 @@ class JobLaunchSerializer(BaseSerializer):
ask_for_vars_dict['vault_credential'] = False
defaults_dict = {}
for field in ask_for_vars_dict:
if field in ('inventory', 'credential', 'vault_credential'):
if field == 'inventory':
defaults_dict[field] = dict(
name=getattrd(obj, '%s.name' % field, None),
id=getattrd(obj, '%s.pk' % field, None))
elif field == 'extra_credentials':
elif field in ('credential', 'vault_credential', 'extra_credentials'):
# don't prefill legacy defaults; encourage API users to specify
# credentials at launch time using the new `credentials` key
pass
elif field == 'credentials':
if self.version > 1:
defaults_dict[field] = [
dict(
@ -3305,7 +3351,7 @@ class JobLaunchSerializer(BaseSerializer):
name=cred.name,
credential_type=cred.credential_type.pk
)
for cred in obj.extra_credentials.all()
for cred in obj.credentials.all()
]
else:
defaults_dict[field] = getattr(obj, field)
@ -3326,23 +3372,7 @@ class JobLaunchSerializer(BaseSerializer):
if obj.inventory and obj.inventory.pending_deletion is True:
errors['inventory'] = _("The inventory associated with this Job Template is being deleted.")
if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)):
credential = obj.credential
else:
credential = attrs.get('credential', None)
# fill passwords dict with request data passwords
for cred in (credential, obj.vault_credential):
if cred and cred.passwords_needed:
passwords = self.context.get('passwords')
try:
for p in cred.passwords_needed:
passwords[p] = data[p]
except KeyError:
errors.setdefault('passwords_needed_to_start', []).extend(cred.passwords_needed)
extra_vars = attrs.get('extra_vars', {})
try:
extra_vars = parse_yaml_or_json(extra_vars, silent_failure=False)
except ParseError as e:
@ -3354,14 +3384,15 @@ class JobLaunchSerializer(BaseSerializer):
if validation_errors:
errors['variables_needed_to_start'] = validation_errors
extra_cred_kinds = []
for cred in data.get('extra_credentials', []):
# Prohibit credential assign of the same CredentialType.kind
# Note: when multi-vault is supported, we'll have to carve out an
# exception to this logic
distinct_cred_kinds = []
for cred in data.get('credentials', []):
cred = Credential.objects.get(id=cred)
if cred.credential_type.pk in extra_cred_kinds:
errors['extra_credentials'] = _('Cannot assign multiple %s credentials.' % cred.credential_type.name)
if cred.credential_type.kind not in ('net', 'cloud'):
errors['extra_credentials'] = _('Extra credentials must be network or cloud.')
extra_cred_kinds.append(cred.credential_type.pk)
if cred.credential_type.pk in distinct_cred_kinds:
errors['credentials'] = _('Cannot assign multiple %s credentials.' % cred.credential_type.name)
distinct_cred_kinds.append(cred.credential_type.pk)
# Special prohibited cases for scan jobs
errors.update(obj._extra_job_type_errors(data))
@ -3375,9 +3406,8 @@ class JobLaunchSerializer(BaseSerializer):
JT_job_tags = obj.job_tags
JT_skip_tags = obj.skip_tags
JT_inventory = obj.inventory
JT_credential = obj.credential
JT_verbosity = obj.verbosity
extra_credentials = attrs.pop('extra_credentials', None)
credentials = attrs.pop('credentials', None)
attrs = super(JobLaunchSerializer, self).validate(attrs)
obj.extra_vars = JT_extra_vars
obj.limit = JT_limit
@ -3385,10 +3415,29 @@ class JobLaunchSerializer(BaseSerializer):
obj.skip_tags = JT_skip_tags
obj.job_tags = JT_job_tags
obj.inventory = JT_inventory
obj.credential = JT_credential
obj.verbosity = JT_verbosity
if extra_credentials is not None:
attrs['extra_credentials'] = extra_credentials
if credentials is not None:
attrs['credentials'] = credentials
# if the POST includes a list of credentials, verify that they don't
# require launch-time passwords
# if the POST *does not* include a list of credentials, fall back to
# checking the credentials on the JobTemplate
credentials = attrs['credentials'] if 'credentials' in data else obj.credentials.all()
passwords_needed = []
for cred in credentials:
if cred.passwords_needed:
passwords = self.context.get('passwords')
try:
for p in cred.passwords_needed:
passwords[p] = data[p]
except KeyError:
passwords_needed.extend(cred.passwords_needed)
if len(passwords_needed):
raise serializers.ValidationError({
'passwords_needed_to_start': passwords_needed
})
return attrs

View File

@ -10,4 +10,5 @@
{% if new_in_300 %}> _Added in Ansible Tower 3.0.0_{% endif %}
{% if new_in_310 %}> _New in Ansible Tower 3.1.0_{% endif %}
{% if new_in_320 %}> _New in Ansible Tower 3.2.0_{% endif %}
{% if new_in_330 %}> _New in Ansible Tower 3.3.0_{% endif %}
{% endif %}

View File

@ -26,9 +26,6 @@ The response will include the following fields:
job_template (array, read-only)
* `survey_enabled`: Flag indicating whether the job_template has an enabled
survey (boolean, read-only)
* `credential_needed_to_start`: Flag indicating the presence of a credential
associated with the job template. If not then one should be supplied when
launching the job (boolean, read-only)
* `inventory_needed_to_start`: Flag indicating the presence of an inventory
associated with the job template. If not then one should be supplied when
launching the job (boolean, read-only)
@ -36,9 +33,8 @@ The response will include the following fields:
Make a POST request to this resource to launch the job_template. If any
passwords, inventory, or extra variables (extra_vars) are required, they must
be passed via POST data, with extra_vars given as a YAML or JSON string and
escaped parentheses. If `credential_needed_to_start` is `True` then the
`credential` field is required and if the `inventory_needed_to_start` is
`True` then the `inventory` is required as well.
escaped parentheses. If the `inventory_needed_to_start` is `True` then the
`inventory` is required.
If successful, the response status code will be 201. If any required passwords
are not provided, a 400 status code will be returned. If the job cannot be

View File

@ -18,7 +18,9 @@ from awx.api.views import (
UnifiedJobTemplateList,
UnifiedJobList,
HostAnsibleFactsDetail,
JobCredentialsList,
JobExtraCredentialsList,
JobTemplateCredentialsList,
JobTemplateExtraCredentialsList,
)
@ -108,7 +110,9 @@ v2_urls = [
url(r'^credential_types/', include(credential_type_urls)),
url(r'^hosts/(?P<pk>[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'),
url(r'^jobs/(?P<pk>[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'),
url(r'^jobs/(?P<pk>[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'),
url(r'^job_templates/(?P<pk>[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'),
url(r'^job_templates/(?P<pk>[0-9]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'),
]
app_name = 'api'

View File

@ -13,7 +13,7 @@ import sys
import logging
import requests
from base64 import b64encode
from collections import OrderedDict
from collections import OrderedDict, Iterable
# Django
from django.conf import settings
@ -2669,7 +2669,7 @@ class JobTemplateList(ListCreateAPIView):
always_allow_superuser = False
capabilities_prefetch = [
'admin', 'execute',
{'copy': ['project.use', 'inventory.use', 'credential.use', 'vault_credential.use']}
{'copy': ['project.use', 'inventory.use']}
]
def post(self, request, *args, **kwargs):
@ -2711,15 +2711,13 @@ class JobTemplateLaunch(RetrieveAPIView):
data['extra_vars'] = extra_vars
ask_for_vars_dict = obj._ask_for_vars_dict()
ask_for_vars_dict.pop('extra_vars')
if get_request_version(self.request) == 1: # TODO: remove in 3.3
ask_for_vars_dict.pop('extra_credentials')
for field in ask_for_vars_dict:
if not ask_for_vars_dict[field]:
data.pop(field, None)
elif field == 'inventory' or field == 'credential':
elif field == 'inventory':
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
elif field == 'extra_credentials':
data[field] = [cred.id for cred in obj.extra_credentials.all()]
elif field == 'credentials':
data[field] = [cred.id for cred in obj.credentials.all()]
else:
data[field] = getattr(obj, field)
return data
@ -2733,13 +2731,56 @@ class JobTemplateLaunch(RetrieveAPIView):
if fd not in request.data and id_fd in request.data:
request.data[fd] = request.data[id_fd]
if get_request_version(self.request) == 1 and 'extra_credentials' in request.data: # TODO: remove in 3.3
# This block causes `extra_credentials` to _always_ be ignored for
# the launch endpoint if we're accessing `/api/v1/`
if get_request_version(self.request) == 1 and 'extra_credentials' in request.data:
if hasattr(request.data, '_mutable') and not request.data._mutable:
request.data._mutable = True
extra_creds = request.data.pop('extra_credentials', None)
if extra_creds is not None:
ignored_fields['extra_credentials'] = extra_creds
# Automatically convert legacy launch credential arguments into a list of `.credentials`
if 'credentials' in request.data and (
'credential' in request.data or
'vault_credential' in request.data or
'extra_credentials' in request.data
):
return Response(dict(
error=_("'credentials' cannot be used in combination with 'credential', 'vault_credential', or 'extra_credentials'.")), # noqa
status=status.HTTP_400_BAD_REQUEST
)
if (
'credential' in request.data or
'vault_credential' in request.data or
'extra_credentials' in request.data
):
# make a list of the current credentials
existing_credentials = obj.credentials.all()
new_credentials = []
for key, conditional in (
('credential', lambda cred: cred.credential_type.kind != 'ssh'),
('vault_credential', lambda cred: cred.credential_type.kind != 'vault'),
('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'))
):
if key in request.data:
# if a specific deprecated key is specified, remove all
# credentials of _that_ type from the list of current
# credentials
existing_credentials = filter(conditional, existing_credentials)
prompted_value = request.data.pop(key)
# add the deprecated credential specified in the request
if not isinstance(prompted_value, Iterable):
prompted_value = [prompted_value]
new_credentials.extend(prompted_value)
# combine the list of "new" and the filtered list of "old"
new_credentials.extend([cred.pk for cred in existing_credentials])
if new_credentials:
request.data['credentials'] = new_credentials
passwords = {}
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
if not serializer.is_valid():
@ -2749,17 +2790,14 @@ class JobTemplateLaunch(RetrieveAPIView):
prompted_fields = _accepted_or_ignored[0]
ignored_fields.update(_accepted_or_ignored[1])
for fd, model in (
('credential', Credential),
('vault_credential', Credential),
('inventory', Inventory)):
if fd in prompted_fields and prompted_fields[fd] != getattrd(obj, '{}.pk'.format(fd), None):
new_res = get_object_or_400(model, pk=get_pk_from_dict(prompted_fields, fd))
use_role = getattr(new_res, 'use_role')
if request.user not in use_role:
raise PermissionDenied()
fd = 'inventory'
if fd in prompted_fields and prompted_fields[fd] != getattrd(obj, '{}.pk'.format(fd), None):
new_res = get_object_or_400(Inventory, pk=get_pk_from_dict(prompted_fields, fd))
use_role = getattr(new_res, 'use_role')
if request.user not in use_role:
raise PermissionDenied()
for cred in prompted_fields.get('extra_credentials', []):
for cred in prompted_fields.get('credentials', []):
new_credential = get_object_or_400(Credential, pk=cred)
if request.user not in new_credential.use_role:
raise PermissionDenied()
@ -2920,17 +2958,17 @@ class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIVi
new_in_300 = True
class JobTemplateExtraCredentialsList(SubListCreateAttachDetachAPIView):
class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
model = Credential
serializer_class = CredentialSerializer
parent_model = JobTemplate
relationship = 'extra_credentials'
new_in_320 = True
relationship = 'credentials'
new_in_330 = True
new_in_api_v2 = True
def get_queryset(self):
# Return the full list of extra_credentials
# Return the full list of credentials
parent = self.get_parent_object()
self.check_parent_access(parent)
sublist_qs = getattrd(parent, self.relationship)
@ -2941,15 +2979,29 @@ class JobTemplateExtraCredentialsList(SubListCreateAttachDetachAPIView):
return sublist_qs
def is_valid_relation(self, parent, sub, created=False):
current_extra_types = [
cred.credential_type.pk for cred in parent.extra_credentials.all()
]
current_extra_types = [cred.credential_type.pk for cred in parent.credentials.all()]
if sub.credential_type.pk in current_extra_types:
return {'error': _('Cannot assign multiple %s credentials.' % sub.credential_type.name)}
if sub.credential_type.kind not in ('net', 'cloud'):
return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created)
class JobTemplateExtraCredentialsList(JobTemplateCredentialsList):
deprecated = True
new_in_320 = True
new_in_330 = False
def get_queryset(self):
sublist_qs = super(JobTemplateExtraCredentialsList, self).get_queryset()
sublist_qs = sublist_qs.filter(**{'credential_type__kind__in': ['cloud', 'net']})
return sublist_qs
def is_valid_relation(self, parent, sub, created=False):
valid = super(JobTemplateExtraCredentialsList, self).is_valid_relation(parent, sub, created)
if sub.credential_type.kind not in ('cloud', 'net'):
return {'error': _('Extra credentials must be network or cloud.')}
return super(JobTemplateExtraCredentialsList, self).is_valid_relation(parent, sub, created)
return valid
class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
@ -3720,14 +3772,26 @@ class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView):
return super(JobDetail, self).update(request, *args, **kwargs)
class JobExtraCredentialsList(SubListAPIView):
class JobCredentialsList(SubListAPIView):
model = Credential
serializer_class = CredentialSerializer
parent_model = Job
relationship = 'extra_credentials'
new_in_320 = True
relationship = 'credentials'
new_in_api_v2 = True
new_in_330 = True
class JobExtraCredentialsList(JobCredentialsList):
deprecated = True
new_in_320 = True
new_in_330 = False
def get_queryset(self):
sublist_qs = super(JobExtraCredentialsList, self).get_queryset()
sublist_qs = sublist_qs.filter(**{'credential_type__kind__in': ['cloud', 'net']})
return sublist_qs
class JobLabelList(SubListAPIView):
@ -4252,7 +4316,6 @@ class UnifiedJobTemplateList(ListAPIView):
capabilities_prefetch = [
'admin', 'execute',
{'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use',
'jobtemplate.credential.use', 'jobtemplate.vault_credential.use',
'workflowjobtemplate.organization.admin']}
]

View File

@ -1149,7 +1149,7 @@ class JobTemplateAccess(BaseAccess):
model = JobTemplate
select_related = ('created_by', 'modified_by', 'inventory', 'project',
'credential', 'next_schedule',)
'next_schedule',)
def filtered_queryset(self):
return self.model.accessible_objects(self.user, 'read_role')
@ -1187,13 +1187,10 @@ class JobTemplateAccess(BaseAccess):
else:
return None
# If a credential is provided, the user should have use access to it.
if not self.check_related('credential', Credential, data, role_field='use_role'):
return False
# If a vault credential is provided, the user should have use access to it.
if not self.check_related('vault_credential', Credential, data, role_field='use_role'):
return False
# If credentials is provided, the user should have use access to them.
for pk in data.get('credentials', []):
if self.user not in get_object_or_400(Credential, pk=pk).use_role:
return False
# If an inventory is provided, the user should have use access.
inventory = get_value(Inventory, 'inventory')
@ -1239,7 +1236,7 @@ class JobTemplateAccess(BaseAccess):
self.check_license(feature='surveys')
return True
for required_field in ('credential', 'inventory', 'project', 'vault_credential'):
for required_field in ('inventory', 'project'):
required_obj = getattr(obj, required_field, None)
if required_field not in data_for_change and required_obj is not None:
data_for_change[required_field] = required_obj.pk
@ -1288,7 +1285,7 @@ class JobTemplateAccess(BaseAccess):
if not obj.project.organization:
return False
return self.user.can_access(type(sub_obj), "read", sub_obj) and self.user in obj.project.organization.admin_role
if relationship == 'extra_credentials' and isinstance(sub_obj, Credential):
if relationship == 'credentials' and isinstance(sub_obj, Credential):
return self.user in obj.admin_role and self.user in sub_obj.use_role
return super(JobTemplateAccess, self).can_attach(
obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
@ -1297,7 +1294,7 @@ class JobTemplateAccess(BaseAccess):
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
if relationship == "instance_groups":
return self.can_attach(obj, sub_obj, relationship, *args, **kwargs)
if relationship == 'extra_credentials' and isinstance(sub_obj, Credential):
if relationship == 'credentials' and isinstance(sub_obj, Credential):
return self.user in obj.admin_role
return super(JobTemplateAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs)
@ -1316,7 +1313,7 @@ class JobAccess(BaseAccess):
model = Job
select_related = ('created_by', 'modified_by', 'job_template', 'inventory',
'project', 'credential', 'job_template',)
'project', 'job_template',)
prefetch_related = (
'unified_job_template',
'instance_group',
@ -1399,29 +1396,27 @@ class JobAccess(BaseAccess):
if self.user.is_superuser:
return True
credential_access = all([self.user in cred.use_role for cred in obj.credentials.all()])
inventory_access = obj.inventory and self.user in obj.inventory.use_role
credential_access = obj.credential and self.user in obj.credential.use_role
job_extra_credentials = set(obj.extra_credentials.all())
if job_extra_credentials:
credential_access = False
job_credentials = set(obj.credentials.all())
# Check if JT execute access (and related prompts) is sufficient
if obj.job_template is not None:
prompts_access = True
job_fields = {}
jt_extra_credentials = set(obj.job_template.extra_credentials.all())
jt_credentials = set(obj.job_template.credentials.all())
for fd in obj.job_template._ask_for_vars_dict():
if fd == 'extra_credentials':
job_fields[fd] = job_extra_credentials
if fd == 'credentials':
job_fields[fd] = job_credentials
job_fields[fd] = getattr(obj, fd)
accepted_fields, ignored_fields = obj.job_template._accept_or_ignore_job_kwargs(**job_fields)
# Check if job fields are not allowed by current _on_launch settings
for fd in ignored_fields:
if fd == 'extra_vars':
continue # we cannot yet validate validity of prompted extra_vars
elif fd == 'extra_credentials':
if job_extra_credentials != jt_extra_credentials:
# Job has extra_credentials that are not promptable
elif fd == 'credentials':
if job_credentials != jt_credentials:
# Job has credentials that are not promptable
prompts_access = False
break
elif job_fields[fd] != getattr(obj.job_template, fd):
@ -1431,19 +1426,16 @@ class JobAccess(BaseAccess):
# For those fields that are allowed by prompting, but differ
# from JT, assure that user has explicit access to them
if prompts_access:
if obj.credential != obj.job_template.credential and not credential_access:
prompts_access = False
if obj.inventory != obj.job_template.inventory and not inventory_access:
prompts_access = False
if prompts_access and job_extra_credentials != jt_extra_credentials:
for cred in job_extra_credentials:
if prompts_access and job_credentials != jt_credentials:
for cred in job_credentials:
if self.user not in cred.use_role:
prompts_access = False
break
if prompts_access and self.user in obj.job_template.execute_role:
return True
org_access = obj.inventory and self.user in obj.inventory.organization.admin_role
project_access = obj.project is None or self.user in obj.project.admin_role

View File

@ -45,10 +45,10 @@ class Command(BaseCommand):
inventory=i,
variables="ansible_connection: local",
created_by=superuser)
JobTemplate.objects.create(name='Demo Job Template',
playbook='hello_world.yml',
project=p,
inventory=i,
credential=c)
jt = JobTemplate.objects.create(name='Demo Job Template',
playbook='hello_world.yml',
project=p,
inventory=i)
jt.credentials.add(c)
print('Default organization added.')
print('Demo Credential, Inventory, and Job Template added.')

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from awx.main.migrations import _migration_utils as migration_utils
from awx.main.migrations._multi_cred import migrate_to_multi_cred
class Migration(migrations.Migration):
dependencies = [
('main', '0008_v320_drop_v1_credential_fields'),
]
operations = [
migrations.AddField(
model_name='unifiedjob',
name='credentials',
field=models.ManyToManyField(related_name='unifiedjobs', to='main.Credential'),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='credentials',
field=models.ManyToManyField(related_name='unifiedjobtemplates', to='main.Credential'),
),
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
migrations.RunPython(migrate_to_multi_cred),
migrations.RemoveField(
model_name='job',
name='credential',
),
migrations.RemoveField(
model_name='job',
name='extra_credentials',
),
migrations.RemoveField(
model_name='job',
name='vault_credential',
),
migrations.RemoveField(
model_name='jobtemplate',
name='credential',
),
migrations.RemoveField(
model_name='jobtemplate',
name='extra_credentials',
),
migrations.RemoveField(
model_name='jobtemplate',
name='vault_credential',
),
]

View File

@ -0,0 +1,12 @@
def migrate_to_multi_cred(app, schema_editor):
Job = app.get_model('main', 'Job')
JobTemplate = app.get_model('main', 'JobTemplate')
for cls in (Job, JobTemplate):
for j in cls.objects.iterator():
if j.credential:
j.credentials.add(j.credential)
if j.vault_credential:
j.credentials.add(j.vault_credential)
for cred in j.extra_credentials.all():
j.credentials.add(cred)

View File

@ -91,26 +91,6 @@ class JobOptions(BaseModel):
default='',
blank=True,
)
credential = models.ForeignKey(
'Credential',
related_name='%(class)ss',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
vault_credential = models.ForeignKey(
'Credential',
related_name='%(class)ss_as_vault_credential+',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
extra_credentials = models.ManyToManyField(
'Credential',
related_name='%(class)ss_as_extra_credential+',
)
forks = models.PositiveIntegerField(
blank=True,
default=0,
@ -184,22 +164,31 @@ class JobOptions(BaseModel):
)
return cred
@property
def all_credentials(self):
credentials = list(self.extra_credentials.all())
if self.vault_credential:
credentials.insert(0, self.vault_credential)
if self.credential:
credentials.insert(0, self.credential)
return credentials
@property
def network_credentials(self):
return [cred for cred in self.extra_credentials.all() if cred.credential_type.kind == 'net']
return [cred for cred in self.credentials.all() if cred.credential_type.kind == 'net']
@property
def cloud_credentials(self):
return [cred for cred in self.extra_credentials.all() if cred.credential_type.kind == 'cloud']
return [cred for cred in self.credentials.all() if cred.credential_type.kind == 'cloud']
@property
def credential(self):
cred = self.get_deprecated_credential('ssh')
if cred is not None:
return cred.pk
@property
def vault_credential(self):
cred = self.get_deprecated_credential('vault')
if cred is not None:
return cred.pk
def get_deprecated_credential(self, kind):
try:
return [cred for cred in self.credentials.all() if cred.credential_type.kind == kind][0]
except IndexError:
return None
# TODO: remove when API v1 is removed
@property
@ -221,10 +210,8 @@ class JobOptions(BaseModel):
def passwords_needed_to_start(self):
'''Return list of password field names needed to start the job.'''
needed = []
if self.credential:
needed.extend(self.credential.passwords_needed)
if self.vault_credential:
needed.extend(self.vault_credential.passwords_needed)
for cred in self.credentials.all():
needed.extend(cred.passwords_needed)
return needed
@ -234,6 +221,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
playbook) to an inventory source with a given credential.
'''
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name')]
PASSWORD_FIELDS = ('credential', 'vault_credential')
class Meta:
app_label = 'main'
@ -298,12 +286,12 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
@classmethod
def _get_unified_job_field_names(cls):
return ['name', 'description', 'job_type', 'inventory', 'project',
'playbook', 'credential', 'vault_credential',
'extra_credentials', 'forks', 'schedule', 'limit', 'verbosity',
'job_tags', 'extra_vars', 'launch_type', 'force_handlers',
'skip_tags', 'start_at_task', 'become_enabled', 'labels',
'survey_passwords', 'allow_simultaneous', 'timeout',
'use_fact_cache', 'diff_mode',]
'playbook', 'credentials', 'forks', 'schedule', 'limit',
'verbosity', 'job_tags', 'extra_vars', 'launch_type',
'force_handlers', 'skip_tags', 'start_at_task',
'become_enabled', 'labels', 'survey_passwords',
'allow_simultaneous', 'timeout', 'use_fact_cache',
'diff_mode',]
def resource_validation_data(self):
'''
@ -317,10 +305,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
resources_needed_to_start.append('inventory')
if not self.ask_inventory_on_launch:
validation_errors['inventory'] = [_("Job Template must provide 'inventory' or allow prompting for it."),]
if self.credential is None and self.vault_credential is None:
resources_needed_to_start.append('credential')
if not self.ask_credential_on_launch:
validation_errors['credential'] = [_("Job Template must provide 'credential' or allow prompting for it."),]
# Job type dependent checks
if self.project is None:
@ -379,9 +363,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
job_type=self.ask_job_type_on_launch,
verbosity=self.ask_verbosity_on_launch,
inventory=self.ask_inventory_on_launch,
credential=self.ask_credential_on_launch,
vault_credential=self.ask_credential_on_launch,
extra_credentials=self.ask_credential_on_launch,
credentials=self.ask_credential_on_launch,
)
def _accept_or_ignore_job_kwargs(self, **kwargs):
@ -715,17 +697,6 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
return self.global_instance_groups
return selected_groups
# Job Credential required
@property
def can_start(self):
if not super(Job, self).can_start:
return False
if not (self.credential) and not (self.vault_credential):
return False
return True
'''
JobNotificationMixin
'''

View File

@ -149,6 +149,10 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
default='ok',
editable=False,
)
credentials = models.ManyToManyField(
'Credential',
related_name='%(class)ss',
)
labels = models.ManyToManyField(
"Label",
blank=True,
@ -579,6 +583,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
on_delete=models.SET_NULL,
help_text=_('The Rampart/Instance group the job was run under'),
)
credentials = models.ManyToManyField(
'Credential',
related_name='%(class)ss',
)
def get_absolute_url(self, request=None):
real_instance = self.get_real_instance()

View File

@ -791,12 +791,15 @@ class BaseTask(LogErrorsTask):
safe_env = self.build_safe_env(env, **kwargs)
# handle custom injectors specified on the CredentialType
if hasattr(instance, 'all_credentials'):
credentials = instance.all_credentials
credentials = []
if isinstance(instance, Job):
credentials = instance.credentials.all()
elif hasattr(instance, 'credential'):
# once other UnifiedJobs (project updates, inventory updates)
# move from a .credential -> .credentials model, we can
# lose this block
credentials = [instance.credential]
else:
credentials = []
for credential in credentials:
if credential:
credential.credential_type.inject_credential(
@ -927,7 +930,7 @@ class RunJob(BaseTask):
}
'''
private_data = {'credentials': {}}
for credential in job.all_credentials:
for credential in job.credentials.all():
# If we were sent SSH credentials, decrypt them and send them
# back (they will be written to a temporary file).
if credential.ssh_key_data not in (None, ''):
@ -957,11 +960,11 @@ class RunJob(BaseTask):
and ansible-vault.
'''
passwords = super(RunJob, self).build_passwords(job, **kwargs)
for cred, fields in {
'credential': ('ssh_key_unlock', 'ssh_password', 'become_password'),
'vault_credential': ('vault_password',)
for kind, fields in {
'ssh': ('ssh_key_unlock', 'ssh_password', 'become_password'),
'vault': ('vault_password',)
}.items():
cred = getattr(job, cred, None)
cred = job.get_deprecated_credential(kind)
if cred:
for field in fields:
if field == 'ssh_password':
@ -1072,7 +1075,8 @@ class RunJob(BaseTask):
Build command line argument list for running ansible-playbook,
optionally using ssh-agent for public/private key authentication.
'''
creds = job.credential
creds = job.get_deprecated_credential('ssh')
ssh_username, become_username, become_method = '', '', ''
if creds:
ssh_username = kwargs.get('username', creds.username)

View File

@ -141,11 +141,11 @@ def mk_job(job_type='run', status='new', job_template=None, inventory=None,
job.job_template = job_template
job.inventory = inventory
job.credential = credential
job.project = project
if persisted:
job.save()
job.credentials.add(credential)
job.project = project
return job
@ -164,9 +164,11 @@ def mk_job_template(name, job_type='run',
if jt.inventory is None:
jt.ask_inventory_on_launch = True
jt.credential = credential
if jt.credential is None:
jt.ask_credential_on_launch = True
if persisted and credential:
jt.save()
jt.credentials.add(credential)
if jt.credential is None:
jt.ask_credential_on_launch = True
jt.project = project
@ -178,10 +180,10 @@ def mk_job_template(name, job_type='run',
jt.save()
if cloud_credential:
cloud_credential.save()
jt.extra_credentials.add(cloud_credential)
jt.credentials.add(cloud_credential)
if network_credential:
network_credential.save()
jt.extra_credentials.add(network_credential)
jt.credentials.add(network_credential)
jt.save()
return jt

View File

@ -0,0 +1,340 @@
import mock
import pytest
from awx.main.models import Credential, Job
from awx.api.versioning import reverse
@pytest.fixture
def job_template(job_template, project, inventory):
job_template.playbook = 'helloworld.yml'
job_template.project = project
job_template.inventory = inventory
job_template.ask_credential_on_launch = True
job_template.save()
return job_template
@pytest.mark.django_db
@pytest.mark.parametrize('key', ('credential', 'vault_credential'))
def test_credential_access_empty(get, job_template, admin, key):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert resp.data[key] is None
assert key not in resp.data['summary_fields']
@pytest.mark.django_db
def test_ssh_credential_access(get, job_template, admin, machine_credential):
job_template.credentials.add(machine_credential)
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert resp.data['credential'] == machine_credential.pk
assert resp.data['summary_fields']['credential']['credential_type_id'] == machine_credential.pk
assert resp.data['summary_fields']['credential']['kind'] == 'ssh'
@pytest.mark.django_db
def test_ssh_credential_update(get, patch, job_template, admin, machine_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
patch(url, {'credential': machine_credential.pk}, admin, expect=200)
resp = get(url, admin)
assert resp.data['credential'] == machine_credential.pk
@pytest.mark.django_db
def test_ssh_credential_update_invalid_kind(get, patch, job_template, admin, vault_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = patch(url, {'credential': vault_credential.pk}, admin, expect=400)
assert 'You must provide an SSH credential.' in resp.content
@pytest.mark.django_db
def test_vault_credential_access(get, job_template, admin, vault_credential):
job_template.credentials.add(vault_credential)
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert resp.data['vault_credential'] == vault_credential.pk
assert resp.data['summary_fields']['vault_credential']['credential_type_id'] == vault_credential.pk # noqa
assert resp.data['summary_fields']['vault_credential']['kind'] == 'vault'
@pytest.mark.django_db
def test_vault_credential_update(get, patch, job_template, admin, vault_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
patch(url, {'vault_credential': vault_credential.pk}, admin, expect=200)
resp = get(url, admin)
assert resp.data['vault_credential'] == vault_credential.pk
@pytest.mark.django_db
def test_vault_credential_update_invalid_kind(get, patch, job_template, admin,
machine_credential):
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk})
resp = patch(url, {'vault_credential': machine_credential.pk}, admin, expect=400)
assert 'You must provide a vault credential.' in resp.content
@pytest.mark.django_db
def test_extra_credentials_filtering(get, job_template, admin,
machine_credential, vault_credential, credential):
job_template.credentials.add(machine_credential)
job_template.credentials.add(vault_credential)
job_template.credentials.add(credential)
url = reverse(
'api:job_template_extra_credentials_list',
kwargs={'version': 'v2', 'pk': job_template.pk}
)
resp = get(url, admin, expect=200)
assert resp.data['count'] == 1
assert resp.data['results'][0]['id'] == credential.pk
@pytest.mark.django_db
def test_extra_credentials_requires_cloud_or_net(get, post, job_template, admin,
machine_credential, vault_credential, credential,
net_credential):
url = reverse(
'api:job_template_extra_credentials_list',
kwargs={'version': 'v2', 'pk': job_template.pk}
)
for cred in (machine_credential, vault_credential):
resp = post(url, {'associate': True, 'id': cred.pk}, admin, expect=400)
assert 'Extra credentials must be network or cloud.' in resp.content
post(url, {'associate': True, 'id': credential.pk}, admin, expect=204)
assert get(url, admin).data['count'] == 1
post(url, {'associate': True, 'id': net_credential.pk}, admin, expect=204)
assert get(url, admin).data['count'] == 2
@pytest.mark.django_db
def test_prevent_multiple_machine_creds(get, post, job_template, admin, machine_credential):
url = reverse(
'api:job_template_credentials_list',
kwargs={'version': 'v2', 'pk': job_template.pk}
)
def _new_cred(name):
return {
'name': name,
'credential_type': machine_credential.credential_type.pk,
'inputs': {
'username': 'bob',
'password': 'secret',
}
}
post(url, _new_cred('First Cred'), admin, expect=201)
assert get(url, admin).data['count'] == 1
resp = post(url, _new_cred('Second Cred'), admin, expect=400)
assert 'Cannot assign multiple Machine credentials.' in resp.content
@pytest.mark.django_db
def test_prevent_multiple_machine_creds_at_launch(get, post, job_template, admin, machine_credential):
other_cred = Credential(credential_type=machine_credential.credential_type, name="Second",
inputs={'username': 'bob'})
other_cred.save()
creds = [machine_credential.pk, other_cred.pk]
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
resp = post(url, {'credentials': creds}, admin)
assert 'Cannot assign multiple Machine credentials.' in resp.content
@pytest.mark.django_db
def test_extra_credentials_unique_by_kind(get, post, job_template, admin,
credentialtype_aws):
url = reverse(
'api:job_template_extra_credentials_list',
kwargs={'version': 'v2', 'pk': job_template.pk}
)
def _new_cred(name):
return {
'name': name,
'credential_type': credentialtype_aws.pk,
'inputs': {
'username': 'bob',
'password': 'secret',
}
}
post(url, _new_cred('First Cred'), admin, expect=201)
assert get(url, admin).data['count'] == 1
resp = post(url, _new_cred('Second Cred'), admin, expect=400)
assert 'Cannot assign multiple Amazon Web Services credentials.' in resp.content
@pytest.mark.django_db
def test_ssh_credential_at_launch(get, post, job_template, admin, machine_credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'credential': machine_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 1
@pytest.mark.django_db
def test_vault_credential_at_launch(get, post, job_template, admin, vault_credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'vault_credential': vault_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 1
@pytest.mark.django_db
def test_extra_credentials_at_launch(get, post, job_template, admin, credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'extra_credentials': [credential.pk]}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 1
@pytest.mark.django_db
def test_modify_ssh_credential_at_launch(get, post, job_template, admin,
machine_credential, vault_credential, credential):
job_template.credentials.add(vault_credential)
job_template.credentials.add(credential)
job_template.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'credential': machine_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 3
@pytest.mark.django_db
def test_modify_vault_credential_at_launch(get, post, job_template, admin,
machine_credential, vault_credential, credential):
job_template.credentials.add(machine_credential)
job_template.credentials.add(credential)
job_template.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'vault_credential': vault_credential.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 3
@pytest.mark.django_db
def test_modify_extra_credentials_at_launch(get, post, job_template, admin,
machine_credential, vault_credential, credential):
job_template.credentials.add(machine_credential)
job_template.credentials.add(vault_credential)
job_template.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'extra_credentials': [credential.pk]}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 3
@pytest.mark.django_db
def test_overwrite_ssh_credential_at_launch(get, post, job_template, admin, machine_credential):
job_template.credentials.add(machine_credential)
job_template.save()
new_cred = machine_credential
new_cred.pk = None
new_cred.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
pk = post(url, {'credential': new_cred.pk}, admin, expect=201).data['job']
summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields']
assert len(summary_fields['credentials']) == 1
assert summary_fields['credentials'][0]['id'] == new_cred.pk
@pytest.mark.django_db
def test_ssh_password_prompted_at_launch(get, post, job_template, admin, machine_credential):
job_template.credentials.add(machine_credential)
job_template.save()
machine_credential.inputs['password'] = 'ASK'
machine_credential.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert 'ssh_password' in resp.data['passwords_needed_to_start']
@pytest.mark.django_db
def test_prompted_credential_removed_on_launch(get, post, job_template, admin, machine_credential):
# If a JT has a credential that needs a password, but the launch POST
# specifies {"credentials": []}, don't require any passwords
job_template.credentials.add(machine_credential)
job_template.save()
machine_credential.inputs['password'] = 'ASK'
machine_credential.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
resp = post(url, {}, admin, expect=400)
resp = post(url, {'credentials': []}, admin, expect=201)
assert 'job' in resp.data
@pytest.mark.django_db
def test_ssh_credential_with_password_at_launch(get, post, job_template, admin, machine_credential):
machine_credential.inputs['password'] = 'ASK'
machine_credential.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
resp = post(url, {'credentials': [machine_credential.pk]}, admin, expect=400)
assert resp.data['passwords_needed_to_start'] == ['ssh_password']
with mock.patch.object(Job, 'signal_start') as signal_start:
resp = post(url, {
'credentials': [machine_credential.pk],
'ssh_password': 'testing123'
}, admin, expect=201)
signal_start.assert_called_with(ssh_password='testing123')
@pytest.mark.django_db
def test_vault_password_prompted_at_launch(get, post, job_template, admin, vault_credential):
job_template.credentials.add(vault_credential)
job_template.save()
vault_credential.inputs['vault_password'] = 'ASK'
vault_credential.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
resp = get(url, admin)
assert 'vault_password' in resp.data['passwords_needed_to_start']
@pytest.mark.django_db
def test_vault_credential_with_password_at_launch(get, post, job_template, admin, vault_credential):
vault_credential.inputs['vault_password'] = 'ASK'
vault_credential.save()
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
resp = post(url, {'credentials': [vault_credential.pk]}, admin, expect=400)
assert resp.data['passwords_needed_to_start'] == ['vault_password']
with mock.patch.object(Job, 'signal_start') as signal_start:
resp = post(url, {
'credentials': [vault_credential.pk],
'vault_password': 'testing123'
}, admin, expect=201)
signal_start.assert_called_with(vault_password='testing123')
@pytest.mark.django_db
def test_extra_creds_prompted_at_launch(get, post, job_template, admin, net_credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
resp = post(url, {'extra_credentials': [net_credential.pk]}, admin, expect=201)
summary_fields = get(
reverse('api:job_detail', kwargs={'pk': resp.data['job']}),
admin
).data['summary_fields']
assert len(summary_fields['credentials']) == 1
@pytest.mark.django_db
def test_invalid_mixed_credentials_specification(get, post, job_template, admin, net_credential):
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
post(url, {'credentials': [net_credential.pk], 'extra_credentials': [net_credential.pk]}, admin, expect=400)

View File

@ -10,7 +10,7 @@ def test_extra_credentials(get, organization_factory, job_template_factory, cred
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.extra_credentials.add(credential)
jt.credentials.add(credential)
jt.save()
job = jt.create_unified_job()
@ -22,8 +22,8 @@ def test_extra_credentials(get, organization_factory, job_template_factory, cred
@pytest.mark.django_db
def test_job_relaunch_permission_denied_response(
post, get, inventory, project, credential, net_credential, machine_credential):
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project,
credential=machine_credential)
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project)
jt.credentials.add(machine_credential)
jt_user = User.objects.create(username='jobtemplateuser')
jt.execute_role.members.add(jt_user)
job = jt.create_unified_job()
@ -33,7 +33,7 @@ def test_job_relaunch_permission_denied_response(
assert r.data['summary_fields']['user_capabilities']['start']
# Job has prompted extra_credential, launch denied w/ message
job.extra_credentials.add(net_credential)
job.credentials.add(net_credential)
r = post(reverse('api:job_relaunch', kwargs={'pk':job.pk}), {}, jt_user, expect=403)
assert 'launched with prompted fields' in r.data['detail']
assert 'do not have permission' in r.data['detail']
@ -50,8 +50,9 @@ def test_job_relaunch_on_failed_hosts(post, inventory, project, machine_credenti
h3 = inventory.hosts.create(name='host3') # failed host
jt = JobTemplate.objects.create(
name='testjt', inventory=inventory,
project=project, credential=machine_credential
project=project
)
jt.credentials.add(machine_credential)
job = jt.create_unified_job(_eager_fields={'status': 'failed', 'limit': 'host1,host2,host3'})
job.job_events.create(event='playbook_on_stats')
job.job_host_summaries.create(host=h1, failed=False, ok=1, changed=0, failures=0, host_name=h1.name)

View File

@ -28,7 +28,7 @@ def runtime_data(organization, credentialtype_ssh):
job_tags='provision',
skip_tags='restart',
inventory=inv_obj.pk,
credential=cred_obj.pk,
credentials=[cred_obj.pk],
)
@ -40,11 +40,10 @@ def job_with_links(machine_credential, inventory):
@pytest.fixture
def job_template_prompts(project, inventory, machine_credential):
def rf(on_off):
return JobTemplate.objects.create(
jt = JobTemplate.objects.create(
job_type='run',
project=project,
inventory=inventory,
credential=machine_credential,
name='deploy-job-template',
ask_variables_on_launch=on_off,
ask_tags_on_launch=on_off,
@ -55,6 +54,8 @@ def job_template_prompts(project, inventory, machine_credential):
ask_credential_on_launch=on_off,
ask_verbosity_on_launch=on_off,
)
jt.credentials.add(machine_credential)
return jt
return rf
@ -64,7 +65,6 @@ def job_template_prompts_null(project):
job_type='run',
project=project,
inventory=None,
credential=None,
name='deploy-job-template',
ask_variables_on_launch=True,
ask_tags_on_launch=True,
@ -87,7 +87,7 @@ def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, ad
with mocker.patch.object(JobTemplate, 'create_unified_job', return_value=mock_job):
with mocker.patch('awx.api.serializers.JobSerializer.to_representation'):
response = post(reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
response = post(reverse('api:job_template_launch', kwargs={'pk': job_template.pk}),
runtime_data, admin_user, expect=201)
assert JobTemplate.create_unified_job.called
assert JobTemplate.create_unified_job.call_args == ({'extra_vars':{}},)
@ -104,7 +104,7 @@ def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, ad
assert 'job_type' in response.data['ignored_fields']
assert 'limit' in response.data['ignored_fields']
assert 'inventory' in response.data['ignored_fields']
assert 'credential' in response.data['ignored_fields']
assert 'credentials' in response.data['ignored_fields']
assert 'job_tags' in response.data['ignored_fields']
assert 'skip_tags' in response.data['ignored_fields']
@ -155,7 +155,7 @@ def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null,
job_template.execute_role.members.add(rando)
# Give user permission to use inventory and credential at runtime
credential = Credential.objects.get(pk=runtime_data['credential'])
credential = Credential.objects.get(pk=runtime_data['credentials'][0])
credential.use_role.members.add(rando)
inventory = Inventory.objects.get(pk=runtime_data['inventory'])
inventory.use_role.members.add(rando)
@ -182,11 +182,11 @@ def test_job_reject_invalid_prompted_vars(runtime_data, job_template_prompts, po
response = post(
reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
dict(job_type='foobicate', # foobicate is not a valid job type
inventory=87865, credential=48474), admin_user, expect=400)
inventory=87865, credentials=[48474]), admin_user, expect=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.']
assert response.data['credential'] == [u'Invalid pk "48474" - object does not exist.']
assert response.data['credentials'] == [u'Invalid pk "48474" - object does not exist.']
@pytest.mark.django_db
@ -235,7 +235,7 @@ def test_job_launch_fails_without_credential_access(job_template_prompts, runtim
# Assure that giving a credential without access blocks the launch
response = post(reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
dict(credential=runtime_data['credential']), rando, expect=403)
dict(credentials=runtime_data['credentials']), rando, expect=403)
assert response.data['detail'] == u'You do not have permission to perform this action.'
@ -258,7 +258,7 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
deploy_jobtemplate.ask_credential_on_launch = True
deploy_jobtemplate.save()
kv = dict(extra_vars={"job_launch_var": 4}, credential=machine_credential.id)
kv = dict(extra_vars={"job_launch_var": 4}, credentials=[machine_credential.id])
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data=kv,
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
@ -270,106 +270,15 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
final_job_extra_vars = yaml.load(job_obj.extra_vars)
assert 'job_template_var' in final_job_extra_vars
assert 'job_launch_var' in final_job_extra_vars
assert job_obj.credential.id == machine_credential.id
assert [cred.pk for cred in job_obj.credentials.all()] == [machine_credential.id]
@pytest.mark.django_db
@pytest.mark.parametrize('pks, error_msg', [
([1], 'must be network or cloud'),
([999], 'object does not exist'),
])
def test_job_launch_JT_with_invalid_extra_credentials(machine_credential, deploy_jobtemplate, pks, error_msg):
def test_job_launch_with_default_creds(machine_credential, vault_credential, deploy_jobtemplate):
deploy_jobtemplate.ask_credential_on_launch = True
deploy_jobtemplate.save()
kv = dict(extra_credentials=pks, credential=machine_credential.id)
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data=kv,
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
validated = serializer.is_valid()
assert validated is False
@pytest.mark.django_db
def test_job_launch_JT_enforces_unique_extra_credential_kinds(machine_credential, credentialtype_aws, deploy_jobtemplate):
"""
JT launching should require that extra_credentials have distinct CredentialTypes
"""
pks = []
for i in range(2):
aws = Credential.objects.create(
name='cred-%d' % i,
credential_type=credentialtype_aws,
inputs={
'username': 'test_user',
'password': 'pas4word'
}
)
aws.save()
pks.append(aws.pk)
kv = dict(extra_credentials=pks, credential=machine_credential.id)
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data=kv,
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
validated = serializer.is_valid()
assert validated is False
@pytest.mark.django_db
@pytest.mark.parametrize('ask_credential_on_launch', [True, False])
def test_job_launch_with_no_credentials(deploy_jobtemplate, ask_credential_on_launch):
deploy_jobtemplate.credential = None
deploy_jobtemplate.vault_credential = None
deploy_jobtemplate.ask_credential_on_launch = ask_credential_on_launch
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data={},
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
validated = serializer.is_valid()
assert validated is False
assert serializer.errors['credential'] == ["Job Template 'credential' is missing or undefined."]
@pytest.mark.django_db
def test_job_launch_with_only_vault_credential(vault_credential, deploy_jobtemplate):
deploy_jobtemplate.credential = None
deploy_jobtemplate.vault_credential = vault_credential
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data={},
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
validated = serializer.is_valid()
assert validated
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**{})
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
assert job_obj.vault_credential.pk == vault_credential.pk
@pytest.mark.django_db
def test_job_launch_with_vault_credential_ask_for_machine(vault_credential, deploy_jobtemplate):
deploy_jobtemplate.credential = None
deploy_jobtemplate.ask_credential_on_launch = True
deploy_jobtemplate.vault_credential = vault_credential
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data={},
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
validated = serializer.is_valid()
assert validated
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**{})
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
assert job_obj.credential is None
assert job_obj.vault_credential.pk == vault_credential.pk
@pytest.mark.django_db
def test_job_launch_with_vault_credential_and_prompted_machine_cred(machine_credential, vault_credential,
deploy_jobtemplate):
deploy_jobtemplate.credential = None
deploy_jobtemplate.ask_credential_on_launch = True
deploy_jobtemplate.vault_credential = vault_credential
kv = dict(credential=machine_credential.id)
deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.credentials.add(vault_credential)
kv = dict()
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data=kv,
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
@ -378,24 +287,26 @@ def test_job_launch_with_vault_credential_and_prompted_machine_cred(machine_cred
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
assert job_obj.credential.pk == machine_credential.pk
assert job_obj.vault_credential.pk == vault_credential.pk
assert job_obj.credential == machine_credential.pk
assert job_obj.vault_credential == vault_credential.pk
@pytest.mark.django_db
def test_job_launch_JT_with_default_vault_credential(machine_credential, vault_credential, deploy_jobtemplate):
deploy_jobtemplate.credential = machine_credential
deploy_jobtemplate.vault_credential = vault_credential
def test_job_launch_with_empty_creds(machine_credential, vault_credential, deploy_jobtemplate):
deploy_jobtemplate.ask_credential_on_launch = True
deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.credentials.add(vault_credential)
kv = dict(credentials=[])
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data={},
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
instance=deploy_jobtemplate, data=kv,
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
validated = serializer.is_valid()
assert validated
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**{})
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
assert job_obj.vault_credential.pk == vault_credential.pk
assert job_obj.credential is None
assert job_obj.vault_credential is None
@pytest.mark.django_db
@ -403,8 +314,7 @@ def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_
deploy_jobtemplate, post, rando):
vault_credential.vault_password = 'ASK'
vault_credential.save()
deploy_jobtemplate.credential = machine_credential
deploy_jobtemplate.vault_credential = vault_credential
deploy_jobtemplate.credentials.add(vault_credential)
deploy_jobtemplate.execute_role.members.add(rando)
deploy_jobtemplate.save()
@ -421,7 +331,7 @@ def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_j
rando):
machine_credential.password = 'ASK'
machine_credential.save()
deploy_jobtemplate.credential = machine_credential
deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.execute_role.members.add(rando)
deploy_jobtemplate.save()
@ -440,8 +350,8 @@ def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential
vault_credential.save()
machine_credential.password = 'ASK'
machine_credential.save()
deploy_jobtemplate.credential = machine_credential
deploy_jobtemplate.vault_credential = vault_credential
deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.credentials.add(vault_credential)
deploy_jobtemplate.execute_role.members.add(rando)
deploy_jobtemplate.save()
@ -458,8 +368,8 @@ def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_
deploy_jobtemplate, post, rando):
vault_credential.vault_password = 'ASK'
vault_credential.save()
deploy_jobtemplate.credential = machine_credential
deploy_jobtemplate.vault_credential = vault_credential
deploy_jobtemplate.credentials.add(machine_credential)
deploy_jobtemplate.credentials.add(vault_credential)
deploy_jobtemplate.execute_role.members.add(rando)
deploy_jobtemplate.save()
@ -473,27 +383,6 @@ def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_
signal_start.assert_called_with(vault_password='vault-me')
@pytest.mark.django_db
def test_job_launch_JT_with_extra_credentials(machine_credential, credential, net_credential, deploy_jobtemplate):
deploy_jobtemplate.ask_credential_on_launch = True
deploy_jobtemplate.save()
kv = dict(extra_credentials=[credential.pk, net_credential.pk], credential=machine_credential.id)
serializer = JobLaunchSerializer(
instance=deploy_jobtemplate, data=kv,
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
validated = serializer.is_valid()
assert validated
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
extra_creds = job_obj.extra_credentials.all()
assert len(extra_creds) == 2
assert credential in extra_creds
assert net_credential in extra_creds
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_launch_unprompted_vars_with_survey(mocker, survey_spec_factory, job_template_prompts, post, admin_user):

View File

@ -30,7 +30,7 @@ def test_create(post, project, machine_credential, inventory, alice, grant_proje
post(reverse('api:job_template_list'), {
'name': 'Some name',
'project': project.id,
'credential': machine_credential.id,
'credentials': [machine_credential.id],
'inventory': inventory.id,
'playbook': 'helloworld.yml',
}, alice, expect=expect)
@ -184,7 +184,7 @@ def test_detach_extra_credential(get, post, organization_factory, job_template_f
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.extra_credentials.add(credential)
jt.credentials.add(credential)
jt.save()
url = reverse('api:job_template_extra_credentials_list', kwargs={'version': 'v2', 'pk': jt.pk})
@ -222,8 +222,8 @@ def test_v1_extra_credentials_detail(get, organization_factory, job_template_fac
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.extra_credentials.add(credential)
jt.extra_credentials.add(net_credential)
jt.credentials.add(credential)
jt.credentials.add(net_credential)
jt.save()
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
@ -272,8 +272,8 @@ def test_filter_by_v1(get, organization_factory, job_template_factory, credentia
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.extra_credentials.add(credential)
jt.extra_credentials.add(net_credential)
jt.credentials.add(credential)
jt.credentials.add(net_credential)
jt.save()
for query in (
@ -312,7 +312,7 @@ def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project
patch(reverse('api:job_template_detail', kwargs={'pk': objs.job_template.id}), {
'name': 'Some name',
'project': objs.project.id,
'credential': objs.credential.id,
'credentials': [objs.credential.id],
'inventory': objs.inventory.id,
'playbook': 'alt-helloworld.yml',
}, alice, expect=expect)
@ -459,8 +459,7 @@ def test_launch_with_extra_credentials(get, post, organization_factory,
resp = post(
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
dict(
credential=machine_credential.pk,
extra_credentials=[credential.pk, net_credential.pk]
credentials=[machine_credential.pk, credential.pk, net_credential.pk]
),
objs.superusers.admin, expect=201
)
@ -480,83 +479,24 @@ def test_launch_with_extra_credentials_not_allowed(get, post, organization_facto
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.credential = machine_credential
jt.credentials.add(machine_credential)
jt.ask_credential_on_launch = False
jt.save()
resp = post(
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
dict(
credential=machine_credential.pk,
extra_credentials=[credential.pk, net_credential.pk]
credentials=[machine_credential.pk, credential.pk, net_credential.pk]
),
objs.superusers.admin
)
assert 'credential' in resp.data['ignored_fields'].keys()
assert 'extra_credentials' in resp.data['ignored_fields'].keys()
assert 'credentials' in resp.data['ignored_fields'].keys()
job_pk = resp.data.get('id')
resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin)
assert resp.data.get('count') == 0
@pytest.mark.django_db
def test_launch_with_extra_credentials_from_jt(get, post, organization_factory,
job_template_factory, machine_credential,
credential, net_credential):
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.ask_credential_on_launch = True
jt.extra_credentials.add(credential)
jt.extra_credentials.add(net_credential)
jt.save()
resp = post(
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
dict(
credential=machine_credential.pk
),
objs.superusers.admin, expect=201
)
job_pk = resp.data.get('id')
resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin)
assert resp.data.get('count') == 2
resp = get(reverse('api:job_template_extra_credentials_list', kwargs={'pk': jt.pk}), objs.superusers.admin)
assert resp.data.get('count') == 2
@pytest.mark.django_db
def test_launch_with_empty_extra_credentials(get, post, organization_factory,
job_template_factory, machine_credential,
credential, net_credential):
objs = organization_factory("org", superusers=['admin'])
jt = job_template_factory("jt", organization=objs.organization,
inventory='test_inv', project='test_proj').job_template
jt.ask_credential_on_launch = True
jt.extra_credentials.add(credential)
jt.extra_credentials.add(net_credential)
jt.save()
resp = post(
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
dict(
credential=machine_credential.pk,
extra_credentials=[],
),
objs.superusers.admin, expect=201
)
job_pk = resp.data.get('id')
resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin)
assert resp.data.get('count') == 0
resp = get(reverse('api:job_template_extra_credentials_list', kwargs={'pk': jt.pk}), objs.superusers.admin)
assert resp.data.get('count') == 2
@pytest.mark.django_db
def test_v1_launch_with_extra_credentials(get, post, organization_factory,
job_template_factory, machine_credential,

View File

@ -89,8 +89,7 @@ class TestJobTemplateCopyEdit:
job_type='run',
project=project,
inventory=None, ask_inventory_on_launch=False, # not allowed
credential=None, ask_credential_on_launch=True,
name='deploy-job-template'
ask_credential_on_launch=True, name='deploy-job-template'
)
serializer = JobTemplateSerializer(jt_res, context=self.fake_context(admin_user))
response = serializer.to_representation(jt_res)
@ -111,22 +110,6 @@ class TestJobTemplateCopyEdit:
assert response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
def test_org_admin_foreign_cred_no_copy_edit(self, jt_copy_edit, org_admin, machine_credential):
"""
Organization admins without access to the 3 related resources:
SHOULD NOT be able to copy JT
SHOULD be able to edit that job template, for nonsensitive changes
"""
# Attach credential to JT that org admin cannot use
jt_copy_edit.credential = machine_credential
jt_copy_edit.save()
serializer = JobTemplateSerializer(jt_copy_edit, context=self.fake_context(org_admin))
response = serializer.to_representation(jt_copy_edit)
assert not response['summary_fields']['user_capabilities']['copy']
assert response['summary_fields']['user_capabilities']['edit']
def test_jt_admin_copy_edit(self, jt_copy_edit, rando):
"""
JT admins wihout access to associated resources SHOULD NOT be able to copy
@ -302,27 +285,22 @@ def test_prefetch_group_capabilities(group, rando):
@pytest.mark.django_db
def test_prefetch_jt_copy_capability(job_template, project, inventory,
machine_credential, vault_credential, rando):
def test_prefetch_jt_copy_capability(job_template, project, inventory, rando):
job_template.project = project
job_template.inventory = inventory
job_template.credential = machine_credential
job_template.vault_credential = vault_credential
job_template.save()
qs = JobTemplate.objects.all()
cache_list_capabilities(qs, [{'copy': [
'project.use', 'inventory.use', 'credential.use', 'vault_credential.use'
'project.use', 'inventory.use',
]}], JobTemplate, rando)
assert qs[0].capabilities_cache == {'copy': False}
project.use_role.members.add(rando)
inventory.use_role.members.add(rando)
machine_credential.use_role.members.add(rando)
vault_credential.use_role.members.add(rando)
cache_list_capabilities(qs, [{'copy': [
'project.use', 'inventory.use', 'credential.use', 'vault_credential.use'
'project.use', 'inventory.use',
]}], JobTemplate, rando)
assert qs[0].capabilities_cache == {'copy': True}

View File

@ -81,26 +81,26 @@ def user():
@pytest.fixture
def check_jobtemplate(project, inventory, credential):
return \
JobTemplate.objects.create(
job_type='check',
project=project,
inventory=inventory,
credential=credential,
name='check-job-template'
)
jt = JobTemplate.objects.create(
job_type='check',
project=project,
inventory=inventory,
name='check-job-template'
)
jt.credentials.add(credential)
return jt
@pytest.fixture
def deploy_jobtemplate(project, inventory, credential):
return \
JobTemplate.objects.create(
job_type='run',
project=project,
inventory=inventory,
credential=credential,
name='deploy-job-template'
)
jt = JobTemplate.objects.create(
job_type='run',
project=project,
inventory=inventory,
name='deploy-job-template'
)
jt.credentials.add(credential)
return jt
@pytest.fixture

View File

@ -54,12 +54,11 @@ class TestImplicitRolesOmitted:
assert qs[1].operation == 'delete'
@pytest.mark.django_db
def test_activity_stream_create_JT(self, project, inventory, credential):
def test_activity_stream_create_JT(self, project, inventory):
JobTemplate.objects.create(
name='test-jt',
project=project,
inventory=inventory,
credential=credential
)
qs = ActivityStream.objects.filter(job_template__isnull=False)
assert qs.count() == 1

View File

@ -1,6 +1,5 @@
import pytest
from django.core.exceptions import ValidationError
from awx.main.models import Credential
@ -12,23 +11,10 @@ def test_clean_credential_with_ssh_type(credentialtype_ssh, job_template):
)
credential.save()
job_template.credential = credential
job_template.credentials.add(credential)
job_template.full_clean()
@pytest.mark.django_db
def test_clean_credential_with_invalid_type_xfail(credentialtype_aws, job_template):
credential = Credential(
name='My Credential',
credential_type=credentialtype_aws
)
credential.save()
with pytest.raises(ValidationError):
job_template.credential = credential
job_template.full_clean()
@pytest.mark.django_db
def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_net, job_template):
aws = Credential(
@ -42,6 +28,6 @@ def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_n
)
net.save()
job_template.extra_credentials.add(aws)
job_template.extra_credentials.add(net)
job_template.credentials.add(aws)
job_template.credentials.add(net)
job_template.full_clean()

View File

@ -50,14 +50,15 @@ class TestCreateUnifiedJob:
mocked.all.assert_called_with()
'''
Ensure that extra_credentials m2m field is copied to new relaunched job
Ensure that credentials m2m field is copied to new relaunched job
'''
def test_job_relaunch_copy_vars(self, machine_credential, inventory,
deploy_jobtemplate, post, mocker, net_credential):
job_with_links = Job.objects.create(name='existing-job', credential=machine_credential, inventory=inventory)
job_with_links = Job.objects.create(name='existing-job', inventory=inventory)
job_with_links.job_template = deploy_jobtemplate
job_with_links.limit = "my_server"
job_with_links.extra_credentials.add(net_credential)
job_with_links.credentials.add(machine_credential)
job_with_links.credentials.add(net_credential)
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate._get_unified_job_field_names',
return_value=['inventory', 'credential', 'limit']):
second_job = job_with_links.copy_unified_job()
@ -66,7 +67,7 @@ class TestCreateUnifiedJob:
assert second_job.credential == job_with_links.credential
assert second_job.inventory == job_with_links.inventory
assert second_job.limit == 'my_server'
assert net_credential in second_job.extra_credentials.all()
assert net_credential in second_job.credentials.all()
@pytest.mark.django_db

View File

@ -1,331 +0,0 @@
import mock
import pytest
from contextlib import contextmanager
from copy import deepcopy
from django.apps import apps
from awx.main.models import Credential, CredentialType
from awx.main.migrations._credentialtypes import migrate_to_v2_credentials
from awx.main.utils import decrypt_field
from awx.main.migrations._credentialtypes import _disassociate_non_insights_projects
EXAMPLE_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxyz==\n-----END PRIVATE KEY-----'
# TODO: remove this set of tests when API v1 is removed
@contextmanager
def migrate(credential, kind, is_insights=False):
with mock.patch.object(Credential, 'kind', kind), \
mock.patch.object(Credential, 'objects', mock.Mock(
get=lambda **kw: deepcopy(credential),
all=lambda: [credential],
)), mock.patch('awx.main.migrations._credentialtypes._is_insights_scm', return_value=is_insights):
class Apps(apps.__class__):
def get_model(self, app, model):
if model == 'Credential':
return Credential
return apps.get_model(app, model)
yield
migrate_to_v2_credentials(Apps(), None)
@pytest.mark.django_db
def test_ssh_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'ssh'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
'ssh_key_unlock': 'keypass',
'become_method': 'sudo',
'become_username': 'superuser',
'become_password': 'superpassword',
})
assert cred.credential_type.name == 'Machine'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
assert cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
assert decrypt_field(cred, 'ssh_key_unlock') == 'keypass'
assert cred.inputs['become_method'] == 'sudo'
assert cred.inputs['become_username'] == 'superuser'
assert cred.inputs['become_password'].startswith('$encrypted$')
assert decrypt_field(cred, 'become_password') == 'superpassword'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_scm_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'scm'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
'ssh_key_unlock': 'keypass',
})
assert cred.credential_type.name == 'Source Control'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
assert cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
assert decrypt_field(cred, 'ssh_key_unlock') == 'keypass'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_vault_only_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'ssh'):
cred.__dict__.update({
'vault_password': 'vault',
})
assert cred.credential_type.name == 'Vault'
assert cred.inputs['vault_password'].startswith('$encrypted$')
assert decrypt_field(cred, 'vault_password') == 'vault'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_vault_with_ssh_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'ssh'):
cred.__dict__.update({
'vault_password': 'vault',
'username': 'bob',
'password': 'secret',
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
'ssh_key_unlock': 'keypass',
'become_method': 'sudo',
'become_username': 'superuser',
'become_password': 'superpassword',
})
assert Credential.objects.count() == 2
assert Credential.objects.filter(credential_type__name='Vault').get() == cred
assert cred.inputs.keys() == ['vault_password']
assert cred.inputs['vault_password'].startswith('$encrypted$')
assert decrypt_field(cred, 'vault_password') == 'vault'
ssh_cred = Credential.objects.filter(credential_type__name='Machine').get()
assert sorted(ssh_cred.inputs.keys()) == sorted(CredentialType.from_v1_kind('ssh').defined_fields)
assert ssh_cred.credential_type.name == 'Machine'
assert ssh_cred.inputs['username'] == 'bob'
assert ssh_cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(ssh_cred, 'password') == 'secret'
assert ssh_cred.inputs['ssh_key_data'].startswith('$encrypted$')
assert decrypt_field(ssh_cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
assert ssh_cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
assert decrypt_field(ssh_cred, 'ssh_key_unlock') == 'keypass'
assert ssh_cred.inputs['become_method'] == 'sudo'
assert ssh_cred.inputs['become_username'] == 'superuser'
assert ssh_cred.inputs['become_password'].startswith('$encrypted$')
assert decrypt_field(ssh_cred, 'become_password') == 'superpassword'
@pytest.mark.django_db
def test_net_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'net'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
'ssh_key_unlock': 'keypass',
'authorize_password': 'authorize-secret',
})
assert cred.credential_type.name == 'Network'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
assert cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
assert decrypt_field(cred, 'ssh_key_unlock') == 'keypass'
assert cred.inputs['authorize_password'].startswith('$encrypted$')
assert decrypt_field(cred, 'authorize_password') == 'authorize-secret'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_aws_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'aws'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'security_token': 'secret-token'
})
assert cred.credential_type.name == 'Amazon Web Services'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['security_token'].startswith('$encrypted$')
assert decrypt_field(cred, 'security_token') == 'secret-token'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_openstack_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'openstack'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'host': 'https://keystone.example.org/',
'project': 'TENANT_ID',
})
assert cred.credential_type.name == 'OpenStack'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['host'] == 'https://keystone.example.org/'
assert cred.inputs['project'] == 'TENANT_ID'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_vmware_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'vmware'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'host': 'https://example.org/',
})
assert cred.credential_type.name == 'VMware vCenter'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['host'] == 'https://example.org/'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_satellite6_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'satellite6'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'host': 'https://example.org/',
})
assert cred.credential_type.name == 'Red Hat Satellite 6'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['host'] == 'https://example.org/'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_cloudforms_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'cloudforms'):
cred.__dict__.update({
'username': 'bob',
'password': 'secret',
'host': 'https://example.org/',
})
assert cred.credential_type.name == 'Red Hat CloudForms'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'secret'
assert cred.inputs['host'] == 'https://example.org/'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_gce_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'gce'):
cred.__dict__.update({
'username': 'bob',
'project': 'PROJECT-123',
'ssh_key_data': EXAMPLE_PRIVATE_KEY
})
assert cred.credential_type.name == 'Google Compute Engine'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['project'] == 'PROJECT-123'
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_azure_rm_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'azure_rm'):
cred.__dict__.update({
'subscription': 'some-subscription',
'username': 'bob',
'password': 'some-password',
'client': 'some-client',
'secret': 'some-secret',
'tenant': 'some-tenant',
})
assert cred.credential_type.name == 'Microsoft Azure Resource Manager'
assert cred.inputs['subscription'] == 'some-subscription'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
assert decrypt_field(cred, 'password') == 'some-password'
assert cred.inputs['client'] == 'some-client'
assert cred.inputs['secret'].startswith('$encrypted$')
assert decrypt_field(cred, 'secret') == 'some-secret'
assert cred.inputs['tenant'] == 'some-tenant'
assert Credential.objects.count() == 1
@pytest.mark.django_db
def test_insights_migration():
cred = Credential(name='My Credential')
with migrate(cred, 'scm', is_insights=True):
cred.__dict__.update({
'username': 'bob',
'password': 'some-password',
})
assert cred.credential_type.name == 'Insights'
assert cred.inputs['username'] == 'bob'
assert cred.inputs['password'].startswith('$encrypted$')
@pytest.mark.skip(reason="Need some more mocking here or something.")
@pytest.mark.django_db
def test_insights_project_migration():
cred1 = apps.get_model('main', 'Credential').objects.create(name='My Credential')
cred2 = apps.get_model('main', 'Credential').objects.create(name='My Credential')
projA1 = apps.get_model('main', 'Project').objects.create(name='Insights Project A1', scm_type='insights', credential=cred1)
projB1 = apps.get_model('main', 'Project').objects.create(name='Git Project B1', scm_type='git', credential=cred1)
projB2 = apps.get_model('main', 'Project').objects.create(name='Git Project B2', scm_type='git', credential=cred1)
projC1 = apps.get_model('main', 'Project').objects.create(name='Git Project C1', scm_type='git', credential=cred2)
_disassociate_non_insights_projects(apps, cred1)
_disassociate_non_insights_projects(apps, cred2)
assert apps.get_model('main', 'Project').objects.get(pk=projA1).credential is None
assert apps.get_model('main', 'Project').objects.get(pk=projB1).credential is None
assert apps.get_model('main', 'Project').objects.get(pk=projB2).credential is None
assert apps.get_model('main', 'Project').objects.get(pk=projC1).credential == cred2

View File

@ -138,28 +138,29 @@ def test_project_org_admin_delete_allowed(normal_job, org_admin):
class TestJobRelaunchAccess:
def test_job_relaunch_normal_resource_access(self, user, inventory, machine_credential):
job_with_links = Job.objects.create(name='existing-job', credential=machine_credential, inventory=inventory)
job_with_links = Job.objects.create(name='existing-job', inventory=inventory)
job_with_links.credentials.add(machine_credential)
inventory_user = user('user1', False)
credential_user = user('user2', False)
both_user = user('user3', False)
# Confirm that a user with inventory & credential access can launch
job_with_links.credential.use_role.members.add(both_user)
machine_credential.use_role.members.add(both_user)
job_with_links.inventory.use_role.members.add(both_user)
assert both_user.can_access(Job, 'start', job_with_links, validate_license=False)
# Confirm that a user with credential access alone cannot launch
job_with_links.credential.use_role.members.add(credential_user)
machine_credential.use_role.members.add(credential_user)
assert not credential_user.can_access(Job, 'start', job_with_links, validate_license=False)
# Confirm that a user with inventory access alone cannot launch
job_with_links.inventory.use_role.members.add(inventory_user)
assert not inventory_user.can_access(Job, 'start', job_with_links, validate_license=False)
def test_job_relaunch_extra_credential_access(
def test_job_relaunch_credential_access(
self, inventory, project, credential, net_credential):
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project)
jt.extra_credentials.add(credential)
jt.credentials.add(credential)
job = jt.create_unified_job()
# Job is unchanged from JT, user has ability to launch
@ -168,11 +169,11 @@ class TestJobRelaunchAccess:
assert jt_user in job.job_template.execute_role
assert jt_user.can_access(Job, 'start', job, validate_license=False)
# Job has prompted extra_credential, launch denied w/ message
job.extra_credentials.add(net_credential)
# Job has prompted net credential, launch denied w/ message
job.credentials.add(net_credential)
assert not jt_user.can_access(Job, 'start', job, validate_license=False)
def test_prompted_extra_credential_relaunch_denied(
def test_prompted_credential_relaunch_denied(
self, inventory, project, net_credential, rando):
jt = JobTemplate.objects.create(
name='testjt', inventory=inventory, project=project,
@ -180,11 +181,11 @@ class TestJobRelaunchAccess:
job = jt.create_unified_job()
jt.execute_role.members.add(rando)
# Job has prompted extra_credential, rando lacks permission to use it
job.extra_credentials.add(net_credential)
# Job has prompted net credential, rando lacks permission to use it
job.credentials.add(net_credential)
assert not rando.can_access(Job, 'start', job, validate_license=False)
def test_prompted_extra_credential_relaunch_allowed(
def test_prompted_credential_relaunch_allowed(
self, inventory, project, net_credential, rando):
jt = JobTemplate.objects.create(
name='testjt', inventory=inventory, project=project,
@ -192,23 +193,24 @@ class TestJobRelaunchAccess:
job = jt.create_unified_job()
jt.execute_role.members.add(rando)
# Job has prompted extra_credential, but rando can use it
# Job has prompted net credential, but rando can use it
net_credential.use_role.members.add(rando)
job.extra_credentials.add(net_credential)
job.credentials.add(net_credential)
assert rando.can_access(Job, 'start', job, validate_license=False)
def test_extra_credential_relaunch_recreation_permission(
def test_credential_relaunch_recreation_permission(
self, inventory, project, net_credential, credential, rando):
jt = JobTemplate.objects.create(
name='testjt', inventory=inventory, project=project,
credential=credential, ask_credential_on_launch=True)
ask_credential_on_launch=True)
job = jt.create_unified_job()
project.admin_role.members.add(rando)
inventory.admin_role.members.add(rando)
credential.admin_role.members.add(rando)
# Relaunch blocked by the extra credential
job.extra_credentials.add(net_credential)
# Relaunch blocked by the net credential
job.credentials.add(credential)
job.credentials.add(net_credential)
assert not rando.can_access(Job, 'start', job, validate_license=False)

View File

@ -47,16 +47,18 @@ def test_inventory_use_access(inventory, user):
class TestJobRelaunchAccess:
@pytest.fixture
def job_no_prompts(self, machine_credential, inventory):
jt = JobTemplate.objects.create(name='test-job_template', credential=machine_credential, inventory=inventory)
jt = JobTemplate.objects.create(name='test-job_template', inventory=inventory)
jt.credentials.add(machine_credential)
return jt.create_unified_job()
@pytest.fixture
def job_with_prompts(self, machine_credential, inventory, organization, credentialtype_ssh):
jt = JobTemplate.objects.create(
name='test-job-template-prompts', credential=machine_credential, inventory=inventory,
name='test-job-template-prompts', inventory=inventory,
ask_tags_on_launch=True, ask_variables_on_launch=True, ask_skip_tags_on_launch=True,
ask_limit_on_launch=True, ask_job_type_on_launch=True, ask_verbosity_on_launch=True,
ask_inventory_on_launch=True, ask_credential_on_launch=True)
jt.credentials.add(machine_credential)
new_cred = Credential.objects.create(
name='new-cred',
credential_type=credentialtype_ssh,
@ -65,8 +67,9 @@ class TestJobRelaunchAccess:
'password': 'pas4word'
}
)
new_cred.save()
new_inv = Inventory.objects.create(name='new-inv', organization=organization)
return jt.create_unified_job(credential=new_cred, inventory=new_inv)
return jt.create_unified_job(credentials=[new_cred.pk], inventory=new_inv)
def test_normal_relaunch_via_job_template(self, job_no_prompts, rando):
"Has JT execute_role, job unchanged relative to JT"
@ -81,7 +84,8 @@ class TestJobRelaunchAccess:
def test_can_relaunch_with_prompted_fields_access(self, job_with_prompts, rando):
"Has use_role on the prompted inventory & credential - allow relaunch"
job_with_prompts.job_template.execute_role.members.add(rando)
job_with_prompts.credential.use_role.members.add(rando)
for cred in job_with_prompts.credentials.all():
cred.use_role.members.add(rando)
job_with_prompts.inventory.use_role.members.add(rando)
assert rando.can_access(Job, 'start', job_with_prompts)

View File

@ -21,11 +21,11 @@ def jt_linked(job_template_factory, credential, net_credential, vault_credential
'testJT', organization='org1', project='proj1', inventory='inventory1',
credential='cred1')
jt = objects.job_template
jt.vault_credential = vault_credential
jt.credentials.add(vault_credential)
jt.save()
# Add AWS cloud credential and network credential
jt.extra_credentials.add(credential)
jt.extra_credentials.add(net_credential)
jt.credentials.add(credential)
jt.credentials.add(net_credential)
return jt
@ -47,15 +47,15 @@ def test_job_template_access_read_level(jt_linked, rando):
access = JobTemplateAccess(rando)
jt_linked.project.read_role.members.add(rando)
jt_linked.inventory.read_role.members.add(rando)
jt_linked.credential.read_role.members.add(rando)
jt_linked.get_deprecated_credential('ssh').read_role.members.add(rando)
proj_pk = jt_linked.project.pk
assert not access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
assert not access.can_add(dict(credential=jt_linked.credential.pk, project=proj_pk))
assert not access.can_add(dict(vault_credential=jt_linked.vault_credential.pk, project=proj_pk))
assert not access.can_add(dict(credential=jt_linked.credential, project=proj_pk))
assert not access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk))
for cred in jt_linked.extra_credentials.all():
assert not access.can_unattach(jt_linked, cred, 'extra_credentials', {})
for cred in jt_linked.credentials.all():
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
@pytest.mark.django_db
@ -64,16 +64,16 @@ def test_job_template_access_use_level(jt_linked, rando):
access = JobTemplateAccess(rando)
jt_linked.project.use_role.members.add(rando)
jt_linked.inventory.use_role.members.add(rando)
jt_linked.credential.use_role.members.add(rando)
jt_linked.vault_credential.use_role.members.add(rando)
jt_linked.get_deprecated_credential('ssh').use_role.members.add(rando)
jt_linked.get_deprecated_credential('vault').use_role.members.add(rando)
proj_pk = jt_linked.project.pk
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
assert access.can_add(dict(credential=jt_linked.credential.pk, project=proj_pk))
assert access.can_add(dict(vault_credential=jt_linked.vault_credential.pk, project=proj_pk))
assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk))
assert access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk))
for cred in jt_linked.extra_credentials.all():
assert not access.can_unattach(jt_linked, cred, 'extra_credentials', {})
for cred in jt_linked.credentials.all():
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
@pytest.mark.django_db
@ -83,14 +83,14 @@ def test_job_template_access_org_admin(jt_linked, rando):
jt_linked.inventory.organization.admin_role.members.add(rando)
# Assign organization permission in the same way the create view does
organization = jt_linked.inventory.organization
jt_linked.credential.admin_role.parents.add(organization.admin_role)
jt_linked.get_deprecated_credential('ssh').admin_role.parents.add(organization.admin_role)
proj_pk = jt_linked.project.pk
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
assert access.can_add(dict(credential=jt_linked.credential.pk, project=proj_pk))
assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk))
for cred in jt_linked.extra_credentials.all():
assert access.can_unattach(jt_linked, cred, 'extra_credentials', {})
for cred in jt_linked.credentials.all():
assert access.can_unattach(jt_linked, cred, 'credentials', {})
assert access.can_read(jt_linked)
assert access.can_delete(jt_linked)
@ -104,9 +104,9 @@ def test_job_template_extra_credentials_prompts_access(
project = project,
playbook = 'helloworld.yml',
inventory = inventory,
credential = machine_credential,
ask_credential_on_launch = True
)
jt.credentials.add(machine_credential)
jt.execute_role.members.add(rando)
r = post(
reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}),
@ -123,24 +123,24 @@ class TestJobTemplateCredentials:
credential.read_role.members.add(rando)
# without permission to credential, user can not attach it
assert not JobTemplateAccess(rando).can_attach(
job_template, credential, 'extra_credentials', {})
job_template, credential, 'credentials', {})
def test_job_template_can_add_extra_credentials(self, job_template, credential, rando):
job_template.admin_role.members.add(rando)
credential.use_role.members.add(rando)
# user has permission to apply credential
assert JobTemplateAccess(rando).can_attach(
job_template, credential, 'extra_credentials', {})
job_template, credential, 'credentials', {})
def test_job_template_vault_cred_check(self, job_template, vault_credential, rando):
job_template.admin_role.members.add(rando)
# not allowed to use the vault cred
assert not JobTemplateAccess(rando).can_change(
job_template, {'vault_credential': vault_credential})
job_template, {'credentials': [vault_credential.pk]})
def test_new_jt_with_vault(self, vault_credential, project, rando):
project.admin_role.members.add(rando)
assert not JobTemplateAccess(rando).can_add({'vault_credential': vault_credential, 'project': project.pk})
assert not JobTemplateAccess(rando).can_add({'credentials': [vault_credential.pk], 'project': project.pk})
@pytest.mark.django_db

View File

@ -114,7 +114,8 @@ class TestJobTemplateSerializerGetSummaryFields():
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_add", return_value='foo'):
response = serializer.get_summary_fields(jt_obj)
with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None):
response = serializer.get_summary_fields(jt_obj)
assert response['user_capabilities']['copy'] == 'foo'
assert response['user_capabilities']['edit'] == 'foobar'

View File

@ -272,4 +272,4 @@ class TestResourceAccessList:
def test_related_search_reverse_FK_field():
view = ListAPIView()
view.model = Credential
assert 'jobtemplates__search' in view.related_search_fields
assert 'unifiedjobtemplates__search' in view.related_search_fields

View File

@ -1,13 +1,14 @@
import pytest
import json
import mock
def test_missing_project_error(job_template_factory):
objects = job_template_factory(
'missing-project-jt',
organization='org1',
inventory='inventory1',
credential='cred1',
persisted=False)
obj = objects.job_template
assert 'project' in obj.resources_needed_to_start
@ -15,27 +16,24 @@ def test_missing_project_error(job_template_factory):
assert 'project' in validation_errors
def test_inventory_credential_need_to_start(job_template_factory):
def test_inventory_need_to_start(job_template_factory):
objects = job_template_factory(
'job-template-few-resources',
project='project1',
persisted=False)
obj = objects.job_template
assert 'inventory' in obj.resources_needed_to_start
assert 'credential' in obj.resources_needed_to_start
def test_inventory_credential_contradictions(job_template_factory):
def test_inventory_contradictions(job_template_factory):
objects = job_template_factory(
'job-template-paradox',
project='project1',
persisted=False)
obj = objects.job_template
obj.ask_inventory_on_launch = False
obj.ask_credential_on_launch = False
validation_errors, resources_needed_to_start = obj.resource_validation_data()
assert 'inventory' in validation_errors
assert 'credential' in validation_errors
def test_survey_answers_as_string(job_template_factory):
@ -142,4 +140,5 @@ def test_job_template_can_start_with_callback_extra_vars_provided(job_template_f
)
obj = objects.job_template
obj.ask_variables_on_launch = True
assert obj.can_start_without_user_input(callback_extra_vars='{"foo": "bar"}') is True
with mock.patch.object(obj.__class__, 'passwords_needed_to_start', []):
assert obj.can_start_without_user_input(callback_extra_vars='{"foo": "bar"}') is True

View File

@ -194,18 +194,18 @@ class TestWorkflowJobNodeJobKWARGS:
extra_vars={'a': 84}, **self.kwargs_base)
def test_char_prompts_and_res_node_prompts(self, job_node_with_prompts):
# TBD: properly handle multicred credential assignment
expect_kwargs = dict(
inventory=job_node_with_prompts.inventory.pk,
credential=job_node_with_prompts.credential.pk,
**example_prompts)
expect_kwargs.update(self.kwargs_base)
assert job_node_with_prompts.get_job_kwargs() == expect_kwargs
def test_reject_some_node_prompts(self, job_node_with_prompts):
# TBD: properly handle multicred credential assignment
job_node_with_prompts.unified_job_template.ask_inventory_on_launch = False
job_node_with_prompts.unified_job_template.ask_job_type_on_launch = False
expect_kwargs = dict(inventory=job_node_with_prompts.inventory.pk,
credential=job_node_with_prompts.credential.pk,
**example_prompts)
expect_kwargs.update(self.kwargs_base)
expect_kwargs.pop('inventory')
@ -239,6 +239,5 @@ class TestWorkflowWarnings:
job_node_with_prompts.unified_job_template.ask_job_type_on_launch = False
assert 'ignored' in job_node_with_prompts.get_prompts_warnings()
assert 'job_type' in job_node_with_prompts.get_prompts_warnings()['ignored']
assert 'credential' in job_node_with_prompts.get_prompts_warnings()['ignored']
assert len(job_node_with_prompts.get_prompts_warnings()['ignored']) == 2
assert len(job_node_with_prompts.get_prompts_warnings()['ignored']) == 1

View File

@ -128,9 +128,6 @@ def job_template_with_ids(job_template_factory):
cloud_type = CredentialType(kind='aws')
cloud_cred = Credential(id=3, pk=3, name='testcloudcred', credential_type=cloud_type)
vault_type = CredentialType(kind='vault')
vault_cred = Credential(id=4, pk=4, name='testnetcred', credential_type=vault_type)
inv = Inventory(id=11, pk=11, name='testinv')
proj = Project(id=14, pk=14, name='testproj')
@ -138,7 +135,6 @@ def job_template_with_ids(job_template_factory):
'testJT', project=proj, inventory=inv, credential=credential,
cloud_credential=cloud_cred, network_credential=net_cred,
persisted=False)
jt_objects.job_template.vault_credential = vault_cred
return jt_objects.job_template
@ -184,9 +180,7 @@ def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit):
mock_add.assert_called_once_with({
'inventory': data['inventory'],
'project': job_template_with_ids.project.id,
'credential': job_template_with_ids.credential.id,
'vault_credential': job_template_with_ids.vault_credential.id
'project': job_template_with_ids.project.id
})

View File

@ -243,12 +243,13 @@ class TestJobExecution:
verbosity=3
)
# mock the job.extra_credentials M2M relation so we can avoid DB access
job._extra_credentials = []
patch = mock.patch.object(Job, 'extra_credentials', mock.Mock(
all=lambda: job._extra_credentials,
add=job._extra_credentials.append,
spec_set=['all', 'add']
# mock the job.credentials M2M relation so we can avoid DB access
job._credentials = []
patch = mock.patch.object(UnifiedJob, 'credentials', mock.Mock(
all=lambda: job._credentials,
add=job._credentials.append,
filter=mock.Mock(return_value=job._credentials),
spec_set=['all', 'add', 'filter']
))
self.patches.append(patch)
patch.start()
@ -328,7 +329,7 @@ class TestIsolatedExecution(TestJobExecution):
}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.credential = credential
self.instance.credentials.add(credential)
private_data = tempfile.mkdtemp(prefix='awx_')
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
@ -370,7 +371,7 @@ class TestIsolatedExecution(TestJobExecution):
credential_type=ssh,
inputs = {'username': 'bob',}
)
self.instance.credential = credential
self.instance.credentials.add(credential)
private_data = tempfile.mkdtemp(prefix='awx_')
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
@ -414,7 +415,7 @@ class TestJobCredentials(TestJobExecution):
inputs = {'username': 'bob', field: 'secret'}
)
credential.inputs[field] = encrypt_field(credential, field)
self.instance.credential = credential
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -434,7 +435,7 @@ class TestJobCredentials(TestJobExecution):
inputs={'vault_password': 'vault-me'}
)
credential.inputs['vault_password'] = encrypt_field(credential, 'vault_password')
self.instance.vault_credential = credential
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -457,7 +458,7 @@ class TestJobCredentials(TestJobExecution):
}
)
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.credential = credential
self.instance.credentials.add(credential)
def run_pexpect_side_effect(private_data, *args, **kwargs):
args, cwd, env, stdout = args
@ -485,7 +486,7 @@ class TestJobCredentials(TestJobExecution):
inputs = {'username': 'bob', 'password': 'secret'}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -505,7 +506,7 @@ class TestJobCredentials(TestJobExecution):
)
for key in ('password', 'security_token'):
credential.inputs[key] = encrypt_field(credential, key)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -528,7 +529,7 @@ class TestJobCredentials(TestJobExecution):
}
)
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@ -554,7 +555,7 @@ class TestJobCredentials(TestJobExecution):
}
)
credential.inputs['secret'] = encrypt_field(credential, 'secret')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
@ -579,7 +580,7 @@ class TestJobCredentials(TestJobExecution):
}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
@ -599,7 +600,7 @@ class TestJobCredentials(TestJobExecution):
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -623,7 +624,7 @@ class TestJobCredentials(TestJobExecution):
}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@ -658,7 +659,7 @@ class TestJobCredentials(TestJobExecution):
)
for field in ('password', 'ssh_key_data', 'authorize_password'):
credential.inputs[field] = encrypt_field(credential, field)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@ -695,7 +696,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
with pytest.raises(Exception):
self.task.run(self.pk)
@ -722,7 +723,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -754,7 +755,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs={'turbo_button': True}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -785,7 +786,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -819,7 +820,7 @@ class TestJobCredentials(TestJobExecution):
inputs = {'password': 'SUPER-SECRET-123'}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -852,7 +853,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -884,7 +885,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs={'turbo_button': True}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -915,7 +916,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs={'turbo_button': True}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -951,7 +952,7 @@ class TestJobCredentials(TestJobExecution):
inputs = {'password': 'SUPER-SECRET-123'}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
assert self.run_pexpect.call_count == 1
@ -987,7 +988,7 @@ class TestJobCredentials(TestJobExecution):
credential_type=some_cloud,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.instance.credentials.add(credential)
self.task.run(self.pk)
def run_pexpect_side_effect(*args, **kwargs):
@ -1010,7 +1011,7 @@ class TestJobCredentials(TestJobExecution):
}
)
gce_credential.inputs['ssh_key_data'] = encrypt_field(gce_credential, 'ssh_key_data')
self.instance.extra_credentials.add(gce_credential)
self.instance.credentials.add(gce_credential)
azure_rm = CredentialType.defaults['azure_rm']()
azure_rm_credential = Credential(
@ -1023,7 +1024,7 @@ class TestJobCredentials(TestJobExecution):
}
)
azure_rm_credential.inputs['secret'] = encrypt_field(azure_rm_credential, 'secret')
self.instance.extra_credentials.add(azure_rm_credential)
self.instance.credentials.add(azure_rm_credential)
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args

View File

@ -0,0 +1,163 @@
Multi-Credential Assignment
===========================
awx has added support for assigning zero or more credentials to
a JobTemplate via a singular, unified interface.
Background
----------
Prior to awx (Tower 3.2), Job Templates had a certain set of requirements
surrounding their relation to Credentials:
* All Job Templates (and Jobs) were required to have exactly *one* Machine/SSH
or Vault credential (or one of both).
* All Job Templates (and Jobs) could have zero or more "extra" Credentials.
* These extra Credentials represented "Cloud" and "Network" credentials that
* could be used to provide authentication to external services via environment
* variables (e.g., AWS_ACCESS_KEY_ID).
This model required a variety of disjoint interfaces for specifying Credentials
on a JobTemplate. For example, to modify assignment of Machine/SSH and Vault
credentials, you would change the Credential key itself:
`PATCH /api/v2/job_templates/N/ {'credential': X, 'vault_credential': Y}`
Modifying `extra_credentials` was accomplished on a separate API endpoint
via association/disassociation actions:
```
POST /api/v2/job_templates/N/extra_credentials {'associate': true, 'id': Z}
POST /api/v2/job_templates/N/extra_credentials {'disassociate': true, 'id': Z}
```
This model lacked the ability associate multiple Vault credentials with
a playbook run, a use case supported by Ansible core from Ansible 2.4 onwards.
This model also was a stumbling block for certain playbook execution workflows.
For example, some users wanted to run playbooks with `connection:local` that
only interacted with some cloud service via a cloud Credential. In this
scenario, users often generated a "dummy" Machine/SSH Credential to attach to
the Job Template simply to satisfy the requirement on the model.
Important Changes
-----------------
JobTemplates now have a single interface for Credential assignment:
`GET /api/v2/job_templates/N/credentials/`
Users can associate and disassociate credentials using `POST` requests to this
interface, similar to the behavior in the now-deprecated `extra_credentials`
endpoint:
```
POST /api/v2/job_templates/N/credentials/ {'associate': true, 'id': X'}
POST /api/v2/job_templates/N/credentials/ {'disassociate': true, 'id': Y'}
```
Under this model, a JobTemplate is considered valid even when it has _zero_
Credentials assigned to it.
Launch Time Considerations
--------------------------
Prior to this change, JobTemplates had a configurable attribute,
`ask_credential_on_launch`. This value was used at launch time to determine
which missing credential values were necessary for launch - this was primarily
used as a mechanism for users to specify an SSH (or Vault) credential to satisfy
the minimum Credential requirement.
Under the new unified Credential list model, this attribute still exists, but it
is no longer bound to a notion of "requiring" a Credential. Now when
`ask_credential_on_launch` is `True`, it signifies that users may (if they
wish) specify a list of credentials at launch time to override those defined on
the JobTemplate:
`POST /api/v2/job_templates/N/launch/ {'credentials': [A, B, C]}`
If `ask_credential_on_launch` is `False`, it signifies that custom `credentials`
provided in the payload to `POST /api/v2/job_templates/N/launch/` will be
ignored.
Under this model, the only purpose for `ask_credential_on_launch` is to signal
that API clients should prompt the user for (optional) changes at launch time.
Backwards Compatability Concerns
--------------------------------
A variety of API clients rely on now-deprecated mechanisms for Credential
retrieval and assignment, and those are still supported in a backwards
compatible way under this new API change. Requests to update
`JobTemplate.credential` and `JobTemplate.vault_credential` will still behave
as they did before:
`PATCH /api/v2/job_templates/N/ {'credential': X, 'vault_credential': Y}`
Under this model, when a JobTemplate with multiple vault Credentials is updated
in this way, the new underlying list will _only_ contain the single Vault
Credential specified in the deprecated request.
`GET` requests to `/api/v2/job_templates/N/` and `/api/v2/jobs/N/`
have traditionally included a variety of metadata in the response via
`related_fields`:
```
{
"related": {
...
"credential": "/api/v2/credentials/1/",
"vault_credential": "/api/v2/credentials/3/",
"extra_credentials": "/api/v2/job_templates/5/extra_credentials/",
}
}
```
...and `summary_fields`:
```
{
"summary_fields": {
"credential": {
"description": "",
"credential_type_id": 1,
"id": 1,
"kind": "ssh",
"name": "Demo Credential"
},
"vault_credential": {
"description": "",
"credential_type_id": 3,
"id": 3,
"kind": "vault",
"name": "some-vault"
},
"extra_credentials": [
{
"description": "",
"credential_type_id": 5,
"id": 2,
"kind": "aws",
"name": "some-aws"
},
{
"description": "",
"credential_type_id": 10,
"id": 4,
"kind": "gce",
"name": "some-gce"
}
],
}
}
```
These metadata will continue to exist and function in a backwards-compatible way.
The `/api/v2/job_templates/N/extra_credentials` endpoint has been deprecated, but
will also continue to exist and function in the same manner for multiple releases.
The `/api/v2/job_templates/N/launch/` endpoint also provides
deprecated,backwards compatible support for specifying credentials at launch time
via the `credential`, `vault_credential`, and `extra_credentials` fields:
`POST /api/v2/job_templates/N/launch/ {'credential': A, 'vault_credential': B, 'extra_credentials': [C, D]}`

View File

@ -490,16 +490,16 @@ def make_the_data():
defaults=dict(
inventory=inventory,
project=project,
credential=next(credential_gen),
created_by=next(creator_gen),
modified_by=next(modifier_gen),
playbook="debug.yml",
**extra_kwargs)
)
job_template.credentials.add(next(credential_gen))
if ids['job_template'] % 7 == 0:
job_template.extra_credentials.add(next(credential_gen))
job_template.credentials.add(next(credential_gen))
if ids['job_template'] % 5 == 0: # formerly cloud credential
job_template.extra_credentials.add(next(credential_gen))
job_template.credentials.add(next(credential_gen))
job_template._is_new = _
job_templates.append(job_template)
inv_idx += 1
@ -649,10 +649,9 @@ def make_the_data():
job_template=job_template,
status=job_stat, name="%s-%d" % (job_template.name, job_i),
project=job_template.project, inventory=job_template.inventory,
credential=job_template.credential,
)
for ec in job_template.extra_credentials.all():
job.extra_credentials.add(ec)
for ec in job_template.credentials.all():
job.credentials.add(ec)
job._is_new = _
jobs.append(job)
job_i += 1