mirror of
https://github.com/ansible/awx.git
synced 2026-03-03 09:48:51 -03:30
Merge pull request #3413 from ryanpetrello/bye-bye-v1
remove /api/v1 and deprecated credential fields Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
@@ -24,20 +24,6 @@ from rest_framework.filters import BaseFilterBackend
|
|||||||
# AWX
|
# AWX
|
||||||
from awx.main.utils import get_type_for_model, to_python_boolean
|
from awx.main.utils import get_type_for_model, to_python_boolean
|
||||||
from awx.main.utils.db import get_all_field_names
|
from awx.main.utils.db import get_all_field_names
|
||||||
from awx.main.models.credential import CredentialType
|
|
||||||
|
|
||||||
|
|
||||||
class V1CredentialFilterBackend(BaseFilterBackend):
|
|
||||||
'''
|
|
||||||
For /api/v1/ requests, filter out v2 (custom) credentials
|
|
||||||
'''
|
|
||||||
|
|
||||||
def filter_queryset(self, request, queryset, view):
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
from awx.api.versioning import get_request_version
|
|
||||||
if get_request_version(request) == 1:
|
|
||||||
queryset = queryset.filter(credential_type__managed_by_tower=True)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class TypeFilterBackend(BaseFilterBackend):
|
class TypeFilterBackend(BaseFilterBackend):
|
||||||
@@ -292,39 +278,6 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
key = key[5:]
|
key = key[5:]
|
||||||
q_not = True
|
q_not = True
|
||||||
|
|
||||||
# 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 (
|
|
||||||
'credential', 'vault_credential', 'cloud_credential', 'network_credential'
|
|
||||||
) or queryset.model._meta.object_name in ('InventorySource', 'InventoryUpdate') and key == 'credential':
|
|
||||||
key = 'credentials'
|
|
||||||
|
|
||||||
# Make legacy v1 Credential fields work for backwards compatability
|
|
||||||
# TODO: remove after API v1 deprecation period
|
|
||||||
#
|
|
||||||
# convert v1 `Credential.kind` queries to `Credential.credential_type__pk`
|
|
||||||
if queryset.model._meta.object_name == 'Credential' and key == 'kind':
|
|
||||||
key = key.replace('kind', 'credential_type')
|
|
||||||
|
|
||||||
if 'ssh' in values:
|
|
||||||
# In 3.2, SSH and Vault became separate credential types, but in the v1 API,
|
|
||||||
# they're both still "kind=ssh"
|
|
||||||
# under the hood, convert `/api/v1/credentials/?kind=ssh` to
|
|
||||||
# `/api/v1/credentials/?or__credential_type=<ssh_pk>&or__credential_type=<vault_pk>`
|
|
||||||
values = set(values)
|
|
||||||
values.add('vault')
|
|
||||||
values = list(values)
|
|
||||||
q_or = True
|
|
||||||
|
|
||||||
for i, kind in enumerate(values):
|
|
||||||
if kind == 'vault':
|
|
||||||
type_ = CredentialType.objects.get(kind=kind)
|
|
||||||
else:
|
|
||||||
type_ = CredentialType.from_v1_kind(kind)
|
|
||||||
if type_ is None:
|
|
||||||
raise ParseError(_('cannot filter on kind %s') % kind)
|
|
||||||
values[i] = type_.pk
|
|
||||||
|
|
||||||
# Convert value(s) to python and add to the appropriate list.
|
# Convert value(s) to python and add to the appropriate list.
|
||||||
for value in values:
|
for value in values:
|
||||||
if q_int:
|
if q_int:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from rest_framework.negotiation import DefaultContentNegotiation
|
|||||||
# AWX
|
# AWX
|
||||||
from awx.api.filters import FieldLookupBackend
|
from awx.api.filters import FieldLookupBackend
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
UnifiedJob, UnifiedJobTemplate, User, Role
|
UnifiedJob, UnifiedJobTemplate, User, Role, Credential
|
||||||
)
|
)
|
||||||
from awx.main.access import access_registry
|
from awx.main.access import access_registry
|
||||||
from awx.main.utils import (
|
from awx.main.utils import (
|
||||||
@@ -46,7 +46,7 @@ from awx.main.utils import (
|
|||||||
)
|
)
|
||||||
from awx.main.utils.db import get_all_field_names
|
from awx.main.utils.db import get_all_field_names
|
||||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer
|
||||||
from awx.api.versioning import URLPathVersioning, get_request_version
|
from awx.api.versioning import URLPathVersioning
|
||||||
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
from awx.api.metadata import SublistAttachDetatchMetadata, Metadata
|
||||||
|
|
||||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||||
@@ -288,12 +288,6 @@ class APIView(views.APIView):
|
|||||||
template_list.append('api/%s.md' % template_basename)
|
template_list.append('api/%s.md' % template_basename)
|
||||||
context = self.get_description_context()
|
context = self.get_description_context()
|
||||||
|
|
||||||
# "v2" -> 2
|
|
||||||
default_version = int(settings.REST_FRAMEWORK['DEFAULT_VERSION'].lstrip('v'))
|
|
||||||
request_version = get_request_version(self.request)
|
|
||||||
if request_version is not None and request_version < default_version:
|
|
||||||
context['deprecated'] = True
|
|
||||||
|
|
||||||
description = render_to_string(template_list, context)
|
description = render_to_string(template_list, context)
|
||||||
if context.get('deprecated') and context.get('swagger_method') is None:
|
if context.get('deprecated') and context.get('swagger_method') is None:
|
||||||
# render deprecation messages at the very top
|
# render deprecation messages at the very top
|
||||||
@@ -842,10 +836,6 @@ class CopyAPIView(GenericAPIView):
|
|||||||
new_in_330 = True
|
new_in_330 = True
|
||||||
new_in_api_v2 = True
|
new_in_api_v2 = True
|
||||||
|
|
||||||
def v1_not_allowed(self):
|
|
||||||
return Response({'detail': 'Action only possible starting with v2 API.'},
|
|
||||||
status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
def _get_copy_return_serializer(self, *args, **kwargs):
|
def _get_copy_return_serializer(self, *args, **kwargs):
|
||||||
if not self.copy_return_serializer_class:
|
if not self.copy_return_serializer_class:
|
||||||
return self.get_serializer(*args, **kwargs)
|
return self.get_serializer(*args, **kwargs)
|
||||||
@@ -859,15 +849,15 @@ class CopyAPIView(GenericAPIView):
|
|||||||
def _decrypt_model_field_if_needed(obj, field_name, field_val):
|
def _decrypt_model_field_if_needed(obj, field_name, field_val):
|
||||||
if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []):
|
if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []):
|
||||||
return field_val
|
return field_val
|
||||||
if isinstance(field_val, dict):
|
if isinstance(obj, Credential) and field_name == 'inputs':
|
||||||
|
for secret in obj.credential_type.secret_fields:
|
||||||
|
if secret in field_val:
|
||||||
|
field_val[secret] = decrypt_field(obj, secret)
|
||||||
|
elif isinstance(field_val, dict):
|
||||||
for sub_field in field_val:
|
for sub_field in field_val:
|
||||||
if isinstance(sub_field, str) \
|
if isinstance(sub_field, str) \
|
||||||
and isinstance(field_val[sub_field], str):
|
and isinstance(field_val[sub_field], str):
|
||||||
try:
|
field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
|
||||||
field_val[sub_field] = decrypt_field(obj, field_name, sub_field)
|
|
||||||
except AttributeError:
|
|
||||||
# Catching the corner case with v1 credential fields
|
|
||||||
field_val[sub_field] = decrypt_field(obj, sub_field)
|
|
||||||
elif isinstance(field_val, str):
|
elif isinstance(field_val, str):
|
||||||
try:
|
try:
|
||||||
field_val = decrypt_field(obj, field_name)
|
field_val = decrypt_field(obj, field_name)
|
||||||
@@ -952,8 +942,6 @@ class CopyAPIView(GenericAPIView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if get_request_version(request) < 2:
|
|
||||||
return self.v1_not_allowed()
|
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if not request.user.can_access(obj.__class__, 'read', obj):
|
if not request.user.can_access(obj.__class__, 'read', obj):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
@@ -968,8 +956,6 @@ class CopyAPIView(GenericAPIView):
|
|||||||
return Response({'can_copy': can_copy})
|
return Response({'can_copy': can_copy})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if get_request_version(request) < 2:
|
|
||||||
return self.v1_not_allowed()
|
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
create_kwargs = self._build_create_dict(obj)
|
create_kwargs = self._build_create_dict(obj)
|
||||||
create_kwargs_check = {}
|
create_kwargs_check = {}
|
||||||
|
|||||||
@@ -232,19 +232,6 @@ class RoleMetadata(Metadata):
|
|||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
# TODO: Tower 3.3 remove class and all uses in views.py when API v1 is removed
|
|
||||||
class JobTypeMetadata(Metadata):
|
|
||||||
def get_field_info(self, field):
|
|
||||||
res = super(JobTypeMetadata, self).get_field_info(field)
|
|
||||||
|
|
||||||
if field.field_name == 'job_type':
|
|
||||||
res['choices'] = [
|
|
||||||
choice for choice in res['choices']
|
|
||||||
if choice[0] != 'scan'
|
|
||||||
]
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class SublistAttachDetatchMetadata(Metadata):
|
class SublistAttachDetatchMetadata(Metadata):
|
||||||
|
|
||||||
def determine_actions(self, request, view):
|
def determine_actions(self, request, view):
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ from awx.main.models import (
|
|||||||
OAuth2AccessToken, OAuth2Application, Organization, Project,
|
OAuth2AccessToken, OAuth2Application, Organization, Project,
|
||||||
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
|
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
|
||||||
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
|
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
|
||||||
UnifiedJobTemplate, V1Credential, WorkflowJob, WorkflowJobNode,
|
UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
|
||||||
WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
|
WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
|
||||||
)
|
)
|
||||||
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
|
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
|
||||||
@@ -72,7 +72,7 @@ from awx.main.redact import UriCleaner, REPLACE_STR
|
|||||||
|
|
||||||
from awx.main.validators import vars_validate_or_raise
|
from awx.main.validators import vars_validate_or_raise
|
||||||
|
|
||||||
from awx.api.versioning import reverse, get_request_version
|
from awx.api.versioning import reverse
|
||||||
from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField,
|
from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField,
|
||||||
VerbatimField, DeprecatedCredentialField)
|
VerbatimField, DeprecatedCredentialField)
|
||||||
|
|
||||||
@@ -113,7 +113,6 @@ SUMMARIZABLE_FK_FIELDS = {
|
|||||||
'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
|
'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
|
||||||
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
|
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
|
||||||
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||||
'vault_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
|
||||||
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type'),
|
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type'),
|
||||||
'job_template': DEFAULT_SUMMARY_FIELDS,
|
'job_template': DEFAULT_SUMMARY_FIELDS,
|
||||||
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
|
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
|
||||||
@@ -144,7 +143,7 @@ def reverse_gfk(content_object, request):
|
|||||||
Returns a dictionary of the form
|
Returns a dictionary of the form
|
||||||
{ '<type>': reverse(<type detail>) }
|
{ '<type>': reverse(<type detail>) }
|
||||||
for example
|
for example
|
||||||
{ 'organization': '/api/v1/organizations/1/' }
|
{ 'organization': '/api/v2/organizations/1/' }
|
||||||
'''
|
'''
|
||||||
if content_object is None or not hasattr(content_object, 'get_absolute_url'):
|
if content_object is None or not hasattr(content_object, 'get_absolute_url'):
|
||||||
return {}
|
return {}
|
||||||
@@ -301,10 +300,7 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
"""
|
return 2
|
||||||
The request version component of the URL as an integer i.e., 1 or 2
|
|
||||||
"""
|
|
||||||
return get_request_version(self.context.get('request')) or 1
|
|
||||||
|
|
||||||
def get_type(self, obj):
|
def get_type(self, obj):
|
||||||
return get_type_for_model(self.Meta.model)
|
return get_type_for_model(self.Meta.model)
|
||||||
@@ -359,10 +355,9 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
|||||||
if view and (hasattr(view, 'retrieve') or view.request.method == 'POST') and \
|
if view and (hasattr(view, 'retrieve') or view.request.method == 'POST') and \
|
||||||
type(obj) in settings.NAMED_URL_GRAPH:
|
type(obj) in settings.NAMED_URL_GRAPH:
|
||||||
original_url = self.get_url(obj)
|
original_url = self.get_url(obj)
|
||||||
if not original_url.startswith('/api/v1'):
|
res['named_url'] = self._generate_named_url(
|
||||||
res['named_url'] = self._generate_named_url(
|
original_url, obj, settings.NAMED_URL_GRAPH[type(obj)]
|
||||||
original_url, obj, settings.NAMED_URL_GRAPH[type(obj)]
|
)
|
||||||
)
|
|
||||||
if getattr(obj, 'created_by', None):
|
if getattr(obj, 'created_by', None):
|
||||||
res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk})
|
res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk})
|
||||||
if getattr(obj, 'modified_by', None):
|
if getattr(obj, 'modified_by', None):
|
||||||
@@ -396,8 +391,6 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl
|
|||||||
continue
|
continue
|
||||||
summary_fields[fk] = OrderedDict()
|
summary_fields[fk] = OrderedDict()
|
||||||
for field in related_fields:
|
for field in related_fields:
|
||||||
if self.version < 2 and field == 'credential_type_id': # TODO: remove version check in 3.3
|
|
||||||
continue
|
|
||||||
|
|
||||||
fval = getattr(fkval, field, None)
|
fval = getattr(fkval, field, None)
|
||||||
|
|
||||||
@@ -884,10 +877,10 @@ class UserSerializer(BaseSerializer):
|
|||||||
'username', 'first_name', 'last_name',
|
'username', 'first_name', 'last_name',
|
||||||
'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'last_login', 'external_account')
|
'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'last_login', 'external_account')
|
||||||
|
|
||||||
def to_representation(self, obj): # TODO: Remove in 3.3
|
def to_representation(self, obj):
|
||||||
ret = super(UserSerializer, self).to_representation(obj)
|
ret = super(UserSerializer, self).to_representation(obj)
|
||||||
ret.pop('password', None)
|
ret.pop('password', None)
|
||||||
if obj and type(self) is UserSerializer or self.version == 1:
|
if obj and type(self) is UserSerializer:
|
||||||
ret['auth'] = obj.social_auth.values('provider', 'uid')
|
ret['auth'] = obj.social_auth.values('provider', 'uid')
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -1364,9 +1357,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
|
|||||||
notification_templates_error = self.reverse('api:project_notification_templates_error_list', kwargs={'pk': obj.pk}),
|
notification_templates_error = self.reverse('api:project_notification_templates_error_list', kwargs={'pk': obj.pk}),
|
||||||
access_list = self.reverse('api:project_access_list', kwargs={'pk': obj.pk}),
|
access_list = self.reverse('api:project_access_list', kwargs={'pk': obj.pk}),
|
||||||
object_roles = self.reverse('api:project_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:project_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
|
copy = self.reverse('api:project_copy', kwargs={'pk': obj.pk})
|
||||||
|
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['copy'] = self.reverse('api:project_copy', kwargs={'pk': obj.pk})
|
|
||||||
if obj.organization:
|
if obj.organization:
|
||||||
res['organization'] = self.reverse('api:organization_detail',
|
res['organization'] = self.reverse('api:organization_detail',
|
||||||
kwargs={'pk': obj.organization.pk})
|
kwargs={'pk': obj.organization.pk})
|
||||||
@@ -1561,9 +1554,8 @@ class InventorySerializer(BaseSerializerWithVariables):
|
|||||||
access_list = self.reverse('api:inventory_access_list', kwargs={'pk': obj.pk}),
|
access_list = self.reverse('api:inventory_access_list', kwargs={'pk': obj.pk}),
|
||||||
object_roles = self.reverse('api:inventory_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:inventory_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
instance_groups = self.reverse('api:inventory_instance_groups_list', kwargs={'pk': obj.pk}),
|
instance_groups = self.reverse('api:inventory_instance_groups_list', kwargs={'pk': obj.pk}),
|
||||||
|
copy = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk})
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['copy'] = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk})
|
|
||||||
if obj.insights_credential:
|
if obj.insights_credential:
|
||||||
res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk})
|
res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk})
|
||||||
if obj.organization:
|
if obj.organization:
|
||||||
@@ -1615,20 +1607,6 @@ class InventorySerializer(BaseSerializerWithVariables):
|
|||||||
return super(InventorySerializer, self).validate(attrs)
|
return super(InventorySerializer, self).validate(attrs)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Remove entire serializer in 3.3, replace with normal serializer
|
|
||||||
class InventoryDetailSerializer(InventorySerializer):
|
|
||||||
|
|
||||||
def get_fields(self):
|
|
||||||
fields = super(InventoryDetailSerializer, self).get_fields()
|
|
||||||
if self.version == 1:
|
|
||||||
fields['can_run_ad_hoc_commands'] = serializers.SerializerMethodField()
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def get_can_run_ad_hoc_commands(self, obj):
|
|
||||||
view = self.context.get('view', None)
|
|
||||||
return bool(obj and view and view.request and view.request.user and view.request.user.can_access(Inventory, 'run_ad_hoc_commands', obj))
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryScriptSerializer(InventorySerializer):
|
class InventoryScriptSerializer(InventorySerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1668,19 +1646,15 @@ class HostSerializer(BaseSerializerWithVariables):
|
|||||||
smart_inventories = self.reverse('api:host_smart_inventories_list', kwargs={'pk': obj.pk}),
|
smart_inventories = self.reverse('api:host_smart_inventories_list', kwargs={'pk': obj.pk}),
|
||||||
ad_hoc_commands = self.reverse('api:host_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
ad_hoc_commands = self.reverse('api:host_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
||||||
ad_hoc_command_events = self.reverse('api:host_ad_hoc_command_events_list', kwargs={'pk': obj.pk}),
|
ad_hoc_command_events = self.reverse('api:host_ad_hoc_command_events_list', kwargs={'pk': obj.pk}),
|
||||||
|
insights = self.reverse('api:host_insights', kwargs={'pk': obj.pk}),
|
||||||
|
ansible_facts = self.reverse('api:host_ansible_facts_detail', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['insights'] = self.reverse('api:host_insights', kwargs={'pk': obj.pk})
|
|
||||||
if obj.inventory:
|
if obj.inventory:
|
||||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||||
if obj.last_job:
|
if obj.last_job:
|
||||||
res['last_job'] = self.reverse('api:job_detail', kwargs={'pk': obj.last_job.pk})
|
res['last_job'] = self.reverse('api:job_detail', kwargs={'pk': obj.last_job.pk})
|
||||||
if obj.last_job_host_summary:
|
if obj.last_job_host_summary:
|
||||||
res['last_job_host_summary'] = self.reverse('api:job_host_summary_detail', kwargs={'pk': obj.last_job_host_summary.pk})
|
res['last_job_host_summary'] = self.reverse('api:job_host_summary_detail', kwargs={'pk': obj.last_job_host_summary.pk})
|
||||||
if self.version > 1:
|
|
||||||
res.update(dict(
|
|
||||||
ansible_facts = self.reverse('api:host_ansible_facts_detail', kwargs={'pk': obj.pk}),
|
|
||||||
))
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_summary_fields(self, obj):
|
def get_summary_fields(self, obj):
|
||||||
@@ -1766,6 +1740,7 @@ class AnsibleFactsSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class GroupSerializer(BaseSerializerWithVariables):
|
class GroupSerializer(BaseSerializerWithVariables):
|
||||||
|
show_capabilities = ['copy', 'edit', 'delete']
|
||||||
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc']
|
capabilities_prefetch = ['inventory.admin', 'inventory.adhoc']
|
||||||
groups_with_active_failures = serializers.IntegerField(
|
groups_with_active_failures = serializers.IntegerField(
|
||||||
read_only=True,
|
read_only=True,
|
||||||
@@ -1779,13 +1754,6 @@ class GroupSerializer(BaseSerializerWithVariables):
|
|||||||
'total_hosts', 'hosts_with_active_failures', 'total_groups',
|
'total_hosts', 'hosts_with_active_failures', 'total_groups',
|
||||||
'groups_with_active_failures', 'has_inventory_sources')
|
'groups_with_active_failures', 'has_inventory_sources')
|
||||||
|
|
||||||
@property
|
|
||||||
def show_capabilities(self): # TODO: consolidate in 3.3
|
|
||||||
if self.version == 1:
|
|
||||||
return ['copy', 'edit', 'start', 'schedule', 'delete']
|
|
||||||
else:
|
|
||||||
return ['copy', 'edit', 'delete']
|
|
||||||
|
|
||||||
def build_relational_field(self, field_name, relation_info):
|
def build_relational_field(self, field_name, relation_info):
|
||||||
field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(field_name, relation_info)
|
field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(field_name, relation_info)
|
||||||
# Inventory is read-only unless creating a new group.
|
# Inventory is read-only unless creating a new group.
|
||||||
@@ -1794,20 +1762,6 @@ class GroupSerializer(BaseSerializerWithVariables):
|
|||||||
field_kwargs.pop('queryset', None)
|
field_kwargs.pop('queryset', None)
|
||||||
return field_class, field_kwargs
|
return field_class, field_kwargs
|
||||||
|
|
||||||
def get_summary_fields(self, obj): # TODO: remove in 3.3
|
|
||||||
summary_fields = super(GroupSerializer, self).get_summary_fields(obj)
|
|
||||||
if self.version == 1:
|
|
||||||
try:
|
|
||||||
inv_src = obj.deprecated_inventory_source
|
|
||||||
summary_fields['inventory_source'] = {}
|
|
||||||
for field in SUMMARIZABLE_FK_FIELDS['inventory_source']:
|
|
||||||
fval = getattr(inv_src, field, None)
|
|
||||||
if fval is not None:
|
|
||||||
summary_fields['inventory_source'][field] = fval
|
|
||||||
except Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
return summary_fields
|
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(GroupSerializer, self).get_related(obj)
|
res = super(GroupSerializer, self).get_related(obj)
|
||||||
res.update(dict(
|
res.update(dict(
|
||||||
@@ -1822,24 +1776,10 @@ class GroupSerializer(BaseSerializerWithVariables):
|
|||||||
inventory_sources = self.reverse('api:group_inventory_sources_list', kwargs={'pk': obj.pk}),
|
inventory_sources = self.reverse('api:group_inventory_sources_list', kwargs={'pk': obj.pk}),
|
||||||
ad_hoc_commands = self.reverse('api:group_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
ad_hoc_commands = self.reverse('api:group_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if self.version == 1: # TODO: remove in 3.3
|
|
||||||
try:
|
|
||||||
res['inventory_source'] = self.reverse('api:inventory_source_detail',
|
|
||||||
kwargs={'pk': obj.deprecated_inventory_source.pk})
|
|
||||||
except Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
if obj.inventory:
|
if obj.inventory:
|
||||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def create(self, validated_data): # TODO: remove in 3.3
|
|
||||||
instance = super(GroupSerializer, self).create(validated_data)
|
|
||||||
if self.version == 1: # TODO: remove in 3.3
|
|
||||||
manual_src = InventorySource(deprecated_group=instance, inventory=instance.inventory)
|
|
||||||
manual_src.v1_group_name = instance.name
|
|
||||||
manual_src.save()
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def validate_name(self, value):
|
def validate_name(self, value):
|
||||||
if value in ('all', '_meta'):
|
if value in ('all', '_meta'):
|
||||||
raise serializers.ValidationError(_('Invalid group name.'))
|
raise serializers.ValidationError(_('Invalid group name.'))
|
||||||
@@ -1941,9 +1881,8 @@ class CustomInventoryScriptSerializer(BaseSerializer):
|
|||||||
res = super(CustomInventoryScriptSerializer, self).get_related(obj)
|
res = super(CustomInventoryScriptSerializer, self).get_related(obj)
|
||||||
res.update(dict(
|
res.update(dict(
|
||||||
object_roles = self.reverse('api:inventory_script_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:inventory_script_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
|
copy = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['copy'] = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk})
|
|
||||||
|
|
||||||
if obj.organization:
|
if obj.organization:
|
||||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||||
@@ -2004,27 +1943,6 @@ class InventorySourceOptionsSerializer(BaseSerializer):
|
|||||||
|
|
||||||
return super(InventorySourceOptionsSerializer, self).validate(attrs)
|
return super(InventorySourceOptionsSerializer, self).validate(attrs)
|
||||||
|
|
||||||
# TODO: remove when old 'credential' fields are removed
|
|
||||||
def get_summary_fields(self, obj):
|
|
||||||
summary_fields = super(InventorySourceOptionsSerializer, self).get_summary_fields(obj)
|
|
||||||
all_creds = []
|
|
||||||
if 'credential' in summary_fields:
|
|
||||||
cred = obj.get_cloud_credential()
|
|
||||||
if cred:
|
|
||||||
summarized_cred = {
|
|
||||||
'id': cred.id, 'name': cred.name, 'description': cred.description,
|
|
||||||
'kind': cred.kind, 'cloud': True
|
|
||||||
}
|
|
||||||
summary_fields['credential'] = summarized_cred
|
|
||||||
all_creds.append(summarized_cred)
|
|
||||||
if self.version > 1:
|
|
||||||
summary_fields['credential']['credential_type_id'] = cred.credential_type_id
|
|
||||||
else:
|
|
||||||
summary_fields.pop('credential')
|
|
||||||
if self.version > 1:
|
|
||||||
summary_fields['credentials'] = all_creds
|
|
||||||
return summary_fields
|
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
|
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
|
||||||
|
|
||||||
@@ -2036,14 +1954,12 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
{'admin': 'inventory.admin'},
|
{'admin': 'inventory.admin'},
|
||||||
{'start': 'inventory.update'}
|
{'start': 'inventory.update'}
|
||||||
]
|
]
|
||||||
group = serializers.SerializerMethodField(
|
|
||||||
help_text=_('Automatic group relationship, will be removed in 3.3'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventorySource
|
model = InventorySource
|
||||||
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout',
|
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout',
|
||||||
'source_project', 'update_on_project_update') + \
|
'source_project', 'update_on_project_update') + \
|
||||||
('last_update_failed', 'last_updated', 'group') # Backwards compatibility.
|
('last_update_failed', 'last_updated') # Backwards compatibility.
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(InventorySourceSerializer, self).get_related(obj)
|
res = super(InventorySourceSerializer, self).get_related(obj)
|
||||||
@@ -2069,30 +1985,10 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
if obj.last_update:
|
if obj.last_update:
|
||||||
res['last_update'] = self.reverse('api:inventory_update_detail',
|
res['last_update'] = self.reverse('api:inventory_update_detail',
|
||||||
kwargs={'pk': obj.last_update.pk})
|
kwargs={'pk': obj.last_update.pk})
|
||||||
if self.version == 1: # TODO: remove in 3.3
|
|
||||||
if obj.deprecated_group:
|
|
||||||
res['group'] = self.reverse('api:group_detail', kwargs={'pk': obj.deprecated_group.pk})
|
|
||||||
else:
|
else:
|
||||||
res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk})
|
res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_fields(self): # TODO: remove in 3.3
|
|
||||||
fields = super(InventorySourceSerializer, self).get_fields()
|
|
||||||
if self.version > 1:
|
|
||||||
fields.pop('group', None)
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def get_summary_fields(self, obj): # TODO: remove in 3.3
|
|
||||||
summary_fields = super(InventorySourceSerializer, self).get_summary_fields(obj)
|
|
||||||
if self.version == 1 and obj.deprecated_group_id:
|
|
||||||
g = obj.deprecated_group
|
|
||||||
summary_fields['group'] = {}
|
|
||||||
for field in SUMMARIZABLE_FK_FIELDS['group']:
|
|
||||||
fval = getattr(g, field, None)
|
|
||||||
if fval is not None:
|
|
||||||
summary_fields['group'][field] = fval
|
|
||||||
return summary_fields
|
|
||||||
|
|
||||||
def get_group(self, obj): # TODO: remove in 3.3
|
def get_group(self, obj): # TODO: remove in 3.3
|
||||||
if obj.deprecated_group:
|
if obj.deprecated_group:
|
||||||
return obj.deprecated_group.id
|
return obj.deprecated_group.id
|
||||||
@@ -2127,12 +2023,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
|
|||||||
raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory."))
|
raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory."))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_source(self, value):
|
|
||||||
if value == '':
|
|
||||||
raise serializers.ValidationError(_(
|
|
||||||
"Manual inventory sources are created automatically when a group is created in the v1 API."))
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate_update_on_project_update(self, value):
|
def validate_update_on_project_update(self, value):
|
||||||
if value and self.instance and self.instance.schedules.exists():
|
if value and self.instance and self.instance.schedules.exists():
|
||||||
raise serializers.ValidationError(_("Setting not compatible with existing schedules."))
|
raise serializers.ValidationError(_("Setting not compatible with existing schedules."))
|
||||||
@@ -2253,8 +2143,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
|||||||
if obj.inventory:
|
if obj.inventory:
|
||||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||||
|
|
||||||
if self.version > 1:
|
res['credentials'] = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk})
|
||||||
res['credentials'] = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk})
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -2286,14 +2175,25 @@ class InventoryUpdateDetailSerializer(InventoryUpdateSerializer):
|
|||||||
|
|
||||||
def get_summary_fields(self, obj):
|
def get_summary_fields(self, obj):
|
||||||
summary_fields = super(InventoryUpdateDetailSerializer, self).get_summary_fields(obj)
|
summary_fields = super(InventoryUpdateDetailSerializer, self).get_summary_fields(obj)
|
||||||
summary_obj = self.get_source_project(obj)
|
|
||||||
|
|
||||||
if summary_obj:
|
source_project = self.get_source_project(obj)
|
||||||
|
if source_project:
|
||||||
summary_fields['source_project'] = {}
|
summary_fields['source_project'] = {}
|
||||||
for field in SUMMARIZABLE_FK_FIELDS['project']:
|
for field in SUMMARIZABLE_FK_FIELDS['project']:
|
||||||
value = getattr(summary_obj, field, None)
|
value = getattr(source_project, field, None)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
summary_fields['source_project'][field] = value
|
summary_fields['source_project'][field] = value
|
||||||
|
|
||||||
|
cred = obj.credentials.first()
|
||||||
|
if cred:
|
||||||
|
summary_fields['credential'] = {
|
||||||
|
'id': cred.pk,
|
||||||
|
'name': cred.name,
|
||||||
|
'description': cred.description,
|
||||||
|
'kind': cred.kind,
|
||||||
|
'cloud': cred.credential_type.kind == 'cloud'
|
||||||
|
}
|
||||||
|
|
||||||
return summary_fields
|
return summary_fields
|
||||||
|
|
||||||
|
|
||||||
@@ -2562,67 +2462,22 @@ class CredentialTypeSerializer(BaseSerializer):
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
class V1CredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Credential
|
|
||||||
fields = ('*', 'kind', 'cloud', 'host', 'username',
|
|
||||||
'password', 'security_token', 'project', 'domain',
|
|
||||||
'ssh_key_data', 'ssh_key_unlock', 'become_method',
|
|
||||||
'become_username', 'become_password', 'vault_password',
|
|
||||||
'subscription', 'tenant', 'secret', 'client', 'authorize',
|
|
||||||
'authorize_password')
|
|
||||||
|
|
||||||
def build_field(self, field_name, info, model_class, nested_depth):
|
|
||||||
if field_name in V1Credential.FIELDS:
|
|
||||||
return self.build_standard_field(field_name,
|
|
||||||
V1Credential.FIELDS[field_name])
|
|
||||||
return super(V1CredentialFields, self).build_field(field_name, info, model_class, nested_depth)
|
|
||||||
|
|
||||||
|
|
||||||
class V2CredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Credential
|
|
||||||
fields = ('*', 'credential_type', 'inputs')
|
|
||||||
|
|
||||||
extra_kwargs = {
|
|
||||||
'credential_type': {
|
|
||||||
'label': _('Credential Type'),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CredentialSerializer(BaseSerializer):
|
class CredentialSerializer(BaseSerializer):
|
||||||
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
show_capabilities = ['edit', 'delete', 'copy', 'use']
|
||||||
capabilities_prefetch = ['admin', 'use']
|
capabilities_prefetch = ['admin', 'use']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Credential
|
model = Credential
|
||||||
fields = ('*', 'organization')
|
fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud')
|
||||||
|
extra_kwargs = {
|
||||||
def get_fields(self):
|
'credential_type': {
|
||||||
fields = super(CredentialSerializer, self).get_fields()
|
'label': _('Credential Type'),
|
||||||
|
},
|
||||||
# TODO: remove when API v1 is removed
|
}
|
||||||
if self.version == 1:
|
|
||||||
fields.update(V1CredentialFields().get_fields())
|
|
||||||
else:
|
|
||||||
fields.update(V2CredentialFields().get_fields())
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def to_representation(self, data):
|
def to_representation(self, data):
|
||||||
value = super(CredentialSerializer, self).to_representation(data)
|
value = super(CredentialSerializer, self).to_representation(data)
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
if self.version == 1:
|
|
||||||
if value.get('kind') == 'vault':
|
|
||||||
value['kind'] = 'ssh'
|
|
||||||
for field in V1Credential.PASSWORD_FIELDS:
|
|
||||||
if field in value and force_text(value[field]).startswith('$encrypted$'):
|
|
||||||
value[field] = '$encrypted$'
|
|
||||||
|
|
||||||
if 'inputs' in value:
|
if 'inputs' in value:
|
||||||
value['inputs'] = data.display_inputs()
|
value['inputs'] = data.display_inputs()
|
||||||
return value
|
return value
|
||||||
@@ -2639,16 +2494,10 @@ class CredentialSerializer(BaseSerializer):
|
|||||||
object_roles = self.reverse('api:credential_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:credential_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
owner_users = self.reverse('api:credential_owner_users_list', kwargs={'pk': obj.pk}),
|
owner_users = self.reverse('api:credential_owner_users_list', kwargs={'pk': obj.pk}),
|
||||||
owner_teams = self.reverse('api:credential_owner_teams_list', kwargs={'pk': obj.pk}),
|
owner_teams = self.reverse('api:credential_owner_teams_list', kwargs={'pk': obj.pk}),
|
||||||
|
copy = self.reverse('api:credential_copy', kwargs={'pk': obj.pk}),
|
||||||
|
input_sources = self.reverse('api:credential_input_source_sublist', kwargs={'pk': obj.pk}),
|
||||||
|
credential_type = self.reverse('api:credential_type_detail', kwargs={'pk': obj.credential_type.pk}),
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['copy'] = self.reverse('api:credential_copy', kwargs={'pk': obj.pk})
|
|
||||||
res['input_sources'] = self.reverse('api:credential_input_source_sublist', kwargs={'pk': obj.pk})
|
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
if self.version > 1:
|
|
||||||
res.update(dict(
|
|
||||||
credential_type = self.reverse('api:credential_type_detail', kwargs={'pk': obj.credential_type.pk}),
|
|
||||||
))
|
|
||||||
|
|
||||||
parents = [role for role in obj.admin_role.parents.all() if role.object_id is not None]
|
parents = [role for role in obj.admin_role.parents.all() if role.object_id is not None]
|
||||||
if parents:
|
if parents:
|
||||||
@@ -2684,54 +2533,12 @@ class CredentialSerializer(BaseSerializer):
|
|||||||
return summary_dict
|
return summary_dict
|
||||||
|
|
||||||
def get_validation_exclusions(self, obj=None):
|
def get_validation_exclusions(self, obj=None):
|
||||||
# CredentialType is now part of validation; legacy v1 fields (e.g.,
|
|
||||||
# 'username', 'password') in JSON POST payloads use the
|
|
||||||
# CredentialType's inputs definition to determine their validity
|
|
||||||
ret = super(CredentialSerializer, self).get_validation_exclusions(obj)
|
ret = super(CredentialSerializer, self).get_validation_exclusions(obj)
|
||||||
for field in ('credential_type', 'inputs'):
|
for field in ('credential_type', 'inputs'):
|
||||||
if field in ret:
|
if field in ret:
|
||||||
ret.remove(field)
|
ret.remove(field)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
if 'credential_type' not in data and self.version == 1:
|
|
||||||
# If `credential_type` is not provided, assume the payload is a
|
|
||||||
# v1 credential payload that specifies a `kind` and a flat list
|
|
||||||
# of field values
|
|
||||||
#
|
|
||||||
# In this scenario, we should automatically detect the proper
|
|
||||||
# CredentialType based on the provided values
|
|
||||||
kind = data.get('kind', 'ssh')
|
|
||||||
credential_type = CredentialType.from_v1_kind(kind, data)
|
|
||||||
if credential_type is None:
|
|
||||||
raise serializers.ValidationError({"kind": _('"%s" is not a valid choice' % kind)})
|
|
||||||
data['credential_type'] = credential_type.pk
|
|
||||||
value = OrderedDict(
|
|
||||||
list({'credential_type': credential_type}.items()) +
|
|
||||||
list(super(CredentialSerializer, self).to_internal_value(data).items())
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make a set of the keys in the POST/PUT payload
|
|
||||||
# - Subtract real fields (name, organization, inputs)
|
|
||||||
# - Subtract virtual v1 fields defined on the determined credential
|
|
||||||
# type (username, password, etc...)
|
|
||||||
# - Any leftovers are invalid for the determined credential type
|
|
||||||
valid_fields = set(super(CredentialSerializer, self).get_fields().keys())
|
|
||||||
valid_fields.update(V2CredentialFields().get_fields().keys())
|
|
||||||
valid_fields.update(['kind', 'cloud'])
|
|
||||||
|
|
||||||
for field in set(data.keys()) - valid_fields - set(credential_type.defined_fields):
|
|
||||||
if data.get(field):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"detail": _("'{field_name}' is not a valid field for {credential_type_name}").format(
|
|
||||||
field_name=field, credential_type_name=credential_type.name
|
|
||||||
)}
|
|
||||||
)
|
|
||||||
value.pop('kind', None)
|
|
||||||
return value
|
|
||||||
return super(CredentialSerializer, self).to_internal_value(data)
|
|
||||||
|
|
||||||
def validate_credential_type(self, credential_type):
|
def validate_credential_type(self, credential_type):
|
||||||
if self.instance and credential_type.pk != self.instance.credential_type.pk:
|
if self.instance and credential_type.pk != self.instance.credential_type.pk:
|
||||||
for rel in (
|
for rel in (
|
||||||
@@ -2788,35 +2595,12 @@ class CredentialSerializerCreate(CredentialSerializer):
|
|||||||
if attrs.get('team'):
|
if attrs.get('team'):
|
||||||
attrs['organization'] = attrs['team'].organization
|
attrs['organization'] = attrs['team'].organization
|
||||||
|
|
||||||
try:
|
return super(CredentialSerializerCreate, self).validate(attrs)
|
||||||
return super(CredentialSerializerCreate, self).validate(attrs)
|
|
||||||
except ValidationError as e:
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
# If we have an `inputs` error on `/api/v1/`:
|
|
||||||
# {'inputs': {'username': [...]}}
|
|
||||||
# ...instead, send back:
|
|
||||||
# {'username': [...]}
|
|
||||||
if self.version == 1 and isinstance(e.detail.get('inputs'), dict):
|
|
||||||
e.detail = e.detail['inputs']
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
user = validated_data.pop('user', None)
|
user = validated_data.pop('user', None)
|
||||||
team = validated_data.pop('team', None)
|
team = validated_data.pop('team', None)
|
||||||
|
|
||||||
# If our payload contains v1 credential fields, translate to the new
|
|
||||||
# model
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
if self.version == 1:
|
|
||||||
for attr in (
|
|
||||||
set(V1Credential.FIELDS) & set(validated_data.keys()) # set intersection
|
|
||||||
):
|
|
||||||
validated_data.setdefault('inputs', {})
|
|
||||||
value = validated_data.pop(attr)
|
|
||||||
if value:
|
|
||||||
validated_data['inputs'][attr] = value
|
|
||||||
credential = super(CredentialSerializerCreate, self).create(validated_data)
|
credential = super(CredentialSerializerCreate, self).create(validated_data)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
@@ -2895,35 +2679,6 @@ class LabelsListMixin(object):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
class V1JobOptionsSerializer(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Credential
|
|
||||||
fields = ('*', 'cloud_credential', 'network_credential')
|
|
||||||
|
|
||||||
V1_FIELDS = ('cloud_credential', 'network_credential',)
|
|
||||||
|
|
||||||
def build_field(self, field_name, info, model_class, nested_depth):
|
|
||||||
if field_name in self.V1_FIELDS:
|
|
||||||
return (DeprecatedCredentialField, {})
|
|
||||||
return super(V1JobOptionsSerializer, self).build_field(field_name, info, model_class, nested_depth)
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyCredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Credential
|
|
||||||
fields = ('*', 'credential', 'vault_credential')
|
|
||||||
|
|
||||||
LEGACY_FIELDS = ('credential', 'vault_credential',)
|
|
||||||
|
|
||||||
def build_field(self, field_name, info, model_class, nested_depth):
|
|
||||||
if field_name in self.LEGACY_FIELDS:
|
|
||||||
return (DeprecatedCredentialField, {})
|
|
||||||
return super(LegacyCredentialFields, self).build_field(field_name, info, model_class, nested_depth)
|
|
||||||
|
|
||||||
|
|
||||||
class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -2932,16 +2687,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
|||||||
'force_handlers', 'skip_tags', 'start_at_task', 'timeout',
|
'force_handlers', 'skip_tags', 'start_at_task', 'timeout',
|
||||||
'use_fact_cache',)
|
'use_fact_cache',)
|
||||||
|
|
||||||
def get_fields(self):
|
|
||||||
fields = super(JobOptionsSerializer, self).get_fields()
|
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
if self.version == 1:
|
|
||||||
fields.update(V1JobOptionsSerializer().get_fields())
|
|
||||||
|
|
||||||
fields.update(LegacyCredentialFields().get_fields())
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(JobOptionsSerializer, self).get_related(obj)
|
res = super(JobOptionsSerializer, self).get_related(obj)
|
||||||
res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk})
|
res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk})
|
||||||
@@ -2955,40 +2700,18 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
|||||||
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
setattr(obj, 'project', None)
|
setattr(obj, 'project', None)
|
||||||
try:
|
if isinstance(obj, UnifiedJobTemplate):
|
||||||
if obj.credential:
|
res['extra_credentials'] = self.reverse(
|
||||||
res['credential'] = self.reverse(
|
'api:job_template_extra_credentials_list',
|
||||||
'api:credential_detail', kwargs={'pk': obj.credential}
|
kwargs={'pk': obj.pk}
|
||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
res['credentials'] = self.reverse(
|
||||||
setattr(obj, 'credential', None)
|
'api:job_template_credentials_list',
|
||||||
try:
|
kwargs={'pk': obj.pk}
|
||||||
if obj.vault_credential:
|
)
|
||||||
res['vault_credential'] = self.reverse(
|
elif isinstance(obj, UnifiedJob):
|
||||||
'api:credential_detail', kwargs={'pk': obj.vault_credential}
|
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})
|
||||||
except ObjectDoesNotExist:
|
|
||||||
setattr(obj, 'vault_credential', None)
|
|
||||||
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:
|
|
||||||
res['cloud_credential'] = self.reverse('api:credential_detail', kwargs={'pk': cloud_cred})
|
|
||||||
net_cred = obj.network_credential
|
|
||||||
if net_cred:
|
|
||||||
res['network_credential'] = self.reverse('api:credential_detail', kwargs={'pk': net_cred})
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -3002,70 +2725,9 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
|||||||
ret['project'] = None
|
ret['project'] = None
|
||||||
if 'playbook' in ret:
|
if 'playbook' in ret:
|
||||||
ret['playbook'] = ''
|
ret['playbook'] = ''
|
||||||
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
|
return ret
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
deprecated_fields = {}
|
|
||||||
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 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 ('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 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:
|
|
||||||
new_cred = fields[key]
|
|
||||||
if new_cred not in existing:
|
|
||||||
for cred in existing:
|
|
||||||
obj.credentials.remove(cred)
|
|
||||||
if new_cred:
|
|
||||||
obj.credentials.add(new_cred)
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
v1_credentials = {}
|
|
||||||
view = self.context.get('view', None)
|
|
||||||
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 ((not self.instance or cred.pk != getattr(self.instance, attr)) and
|
|
||||||
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:
|
if 'project' in self.fields and 'playbook' in self.fields:
|
||||||
project = attrs.get('project', self.instance and self.instance.project or None)
|
project = attrs.get('project', self.instance and self.instance.project or None)
|
||||||
playbook = attrs.get('playbook', self.instance and self.instance.playbook or '')
|
playbook = attrs.get('playbook', self.instance and self.instance.playbook or '')
|
||||||
@@ -3079,7 +2741,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
|||||||
raise serializers.ValidationError({'playbook': _('Must select playbook for project.')})
|
raise serializers.ValidationError({'playbook': _('Must select playbook for project.')})
|
||||||
|
|
||||||
ret = super(JobOptionsSerializer, self).validate(attrs)
|
ret = super(JobOptionsSerializer, self).validate(attrs)
|
||||||
ret.update(v1_credentials)
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@@ -3105,12 +2766,6 @@ class JobTemplateMixin(object):
|
|||||||
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
|
if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec):
|
||||||
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
|
d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description'])
|
||||||
d['recent_jobs'] = self._recent_jobs(obj)
|
d['recent_jobs'] = self._recent_jobs(obj)
|
||||||
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
if self.version == 1 and 'vault_credential' in d:
|
|
||||||
if d['vault_credential'].get('kind','') == 'vault':
|
|
||||||
d['vault_credential']['kind'] = 'ssh'
|
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
@@ -3146,9 +2801,8 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
object_roles = self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
instance_groups = self.reverse('api:job_template_instance_groups_list', kwargs={'pk': obj.pk}),
|
instance_groups = self.reverse('api:job_template_instance_groups_list', kwargs={'pk': obj.pk}),
|
||||||
slice_workflow_jobs = self.reverse('api:job_template_slice_workflow_jobs_list', kwargs={'pk': obj.pk}),
|
slice_workflow_jobs = self.reverse('api:job_template_slice_workflow_jobs_list', kwargs={'pk': obj.pk}),
|
||||||
|
copy = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['copy'] = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk})
|
|
||||||
if obj.host_config_key:
|
if obj.host_config_key:
|
||||||
res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk})
|
res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk})
|
||||||
return res
|
return res
|
||||||
@@ -3181,9 +2835,6 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
||||||
all_creds = []
|
all_creds = []
|
||||||
# Organize credential data into multitude of deprecated fields
|
# Organize credential data into multitude of deprecated fields
|
||||||
# TODO: remove most of this as v1 is removed
|
|
||||||
vault_credential = None
|
|
||||||
credential = None
|
|
||||||
extra_creds = []
|
extra_creds = []
|
||||||
if obj.pk:
|
if obj.pk:
|
||||||
for cred in obj.credentials.all():
|
for cred in obj.credentials.all():
|
||||||
@@ -3194,30 +2845,12 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
'kind': cred.kind,
|
'kind': cred.kind,
|
||||||
'cloud': cred.credential_type.kind == 'cloud'
|
'cloud': cred.credential_type.kind == 'cloud'
|
||||||
}
|
}
|
||||||
if self.version > 1:
|
|
||||||
summarized_cred['credential_type_id'] = cred.credential_type_id
|
|
||||||
all_creds.append(summarized_cred)
|
all_creds.append(summarized_cred)
|
||||||
if cred.credential_type.kind in ('cloud', 'net'):
|
if cred.credential_type.kind in ('cloud', 'net'):
|
||||||
extra_creds.append(summarized_cred)
|
extra_creds.append(summarized_cred)
|
||||||
elif summarized_cred['kind'] == 'ssh':
|
if self.is_detail_view:
|
||||||
credential = summarized_cred
|
summary_fields['extra_credentials'] = extra_creds
|
||||||
elif summarized_cred['kind'] == 'vault':
|
summary_fields['credentials'] = all_creds
|
||||||
vault_credential = summarized_cred
|
|
||||||
# Selectively apply those fields, depending on view deetails
|
|
||||||
if (self.is_detail_view or self.version == 1) and credential:
|
|
||||||
summary_fields['credential'] = credential
|
|
||||||
else:
|
|
||||||
# Credential could be an empty dictionary in this case
|
|
||||||
summary_fields.pop('credential', None)
|
|
||||||
if (self.is_detail_view or self.version == 1) and vault_credential:
|
|
||||||
summary_fields['vault_credential'] = vault_credential
|
|
||||||
else:
|
|
||||||
# vault credential could be empty dictionary
|
|
||||||
summary_fields.pop('vault_credential', None)
|
|
||||||
if self.version > 1:
|
|
||||||
if self.is_detail_view:
|
|
||||||
summary_fields['extra_credentials'] = extra_creds
|
|
||||||
summary_fields['credentials'] = all_creds
|
|
||||||
return summary_fields
|
return summary_fields
|
||||||
|
|
||||||
|
|
||||||
@@ -3250,6 +2883,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
activity_stream = self.reverse('api:job_activity_stream_list', kwargs={'pk': obj.pk}),
|
activity_stream = self.reverse('api:job_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||||
notifications = self.reverse('api:job_notifications_list', kwargs={'pk': obj.pk}),
|
notifications = self.reverse('api:job_notifications_list', kwargs={'pk': obj.pk}),
|
||||||
labels = self.reverse('api:job_label_list', kwargs={'pk': obj.pk}),
|
labels = self.reverse('api:job_label_list', kwargs={'pk': obj.pk}),
|
||||||
|
create_schedule = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
try:
|
try:
|
||||||
if obj.job_template:
|
if obj.job_template:
|
||||||
@@ -3257,8 +2891,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
kwargs={'pk': obj.job_template.pk})
|
kwargs={'pk': obj.job_template.pk})
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
setattr(obj, 'job_template', None)
|
setattr(obj, 'job_template', None)
|
||||||
if (obj.can_start or True) and self.version == 1: # TODO: remove in 3.3
|
|
||||||
res['start'] = self.reverse('api:job_start', kwargs={'pk': obj.pk})
|
|
||||||
if obj.can_cancel or True:
|
if obj.can_cancel or True:
|
||||||
res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk})
|
res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk})
|
||||||
try:
|
try:
|
||||||
@@ -3268,8 +2900,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
if self.version > 1:
|
|
||||||
res['create_schedule'] = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk})
|
|
||||||
res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk})
|
res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -3320,9 +2950,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
summary_fields = super(JobSerializer, self).get_summary_fields(obj)
|
summary_fields = super(JobSerializer, self).get_summary_fields(obj)
|
||||||
all_creds = []
|
all_creds = []
|
||||||
# Organize credential data into multitude of deprecated fields
|
# Organize credential data into multitude of deprecated fields
|
||||||
# TODO: remove most of this as v1 is removed
|
|
||||||
vault_credential = None
|
|
||||||
credential = None
|
|
||||||
extra_creds = []
|
extra_creds = []
|
||||||
if obj.pk:
|
if obj.pk:
|
||||||
for cred in obj.credentials.all():
|
for cred in obj.credentials.all():
|
||||||
@@ -3333,30 +2960,12 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
'kind': cred.kind,
|
'kind': cred.kind,
|
||||||
'cloud': cred.credential_type.kind == 'cloud'
|
'cloud': cred.credential_type.kind == 'cloud'
|
||||||
}
|
}
|
||||||
if self.version > 1:
|
|
||||||
summarized_cred['credential_type_id'] = cred.credential_type_id
|
|
||||||
all_creds.append(summarized_cred)
|
all_creds.append(summarized_cred)
|
||||||
if cred.credential_type.kind in ('cloud', 'net'):
|
if cred.credential_type.kind in ('cloud', 'net'):
|
||||||
extra_creds.append(summarized_cred)
|
extra_creds.append(summarized_cred)
|
||||||
elif summarized_cred['kind'] == 'ssh':
|
if self.is_detail_view:
|
||||||
credential = summarized_cred
|
summary_fields['extra_credentials'] = extra_creds
|
||||||
elif summarized_cred['kind'] == 'vault':
|
summary_fields['credentials'] = all_creds
|
||||||
vault_credential = summarized_cred
|
|
||||||
# Selectively apply those fields, depending on view deetails
|
|
||||||
if (self.is_detail_view or self.version == 1) and credential:
|
|
||||||
summary_fields['credential'] = credential
|
|
||||||
else:
|
|
||||||
# Credential could be an empty dictionary in this case
|
|
||||||
summary_fields.pop('credential', None)
|
|
||||||
if (self.is_detail_view or self.version == 1) and vault_credential:
|
|
||||||
summary_fields['vault_credential'] = vault_credential
|
|
||||||
else:
|
|
||||||
# vault credential could be empty dictionary
|
|
||||||
summary_fields.pop('vault_credential', None)
|
|
||||||
if self.version > 1:
|
|
||||||
if self.is_detail_view:
|
|
||||||
summary_fields['extra_credentials'] = extra_creds
|
|
||||||
summary_fields['credentials'] = all_creds
|
|
||||||
return summary_fields
|
return summary_fields
|
||||||
|
|
||||||
|
|
||||||
@@ -3696,9 +3305,8 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
|
|||||||
access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}),
|
access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}),
|
||||||
object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}),
|
||||||
survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}),
|
survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}),
|
||||||
|
copy = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['copy'] = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk})
|
|
||||||
if obj.organization:
|
if obj.organization:
|
||||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||||
return res
|
return res
|
||||||
@@ -4401,17 +4009,16 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
name=getattrd(obj, '%s.name' % field_name, None),
|
name=getattrd(obj, '%s.name' % field_name, None),
|
||||||
id=getattrd(obj, '%s.pk' % field_name, None))
|
id=getattrd(obj, '%s.pk' % field_name, None))
|
||||||
elif field_name == 'credentials':
|
elif field_name == 'credentials':
|
||||||
if self.version > 1:
|
for cred in obj.credentials.all():
|
||||||
for cred in obj.credentials.all():
|
cred_dict = dict(
|
||||||
cred_dict = dict(
|
id=cred.id,
|
||||||
id=cred.id,
|
name=cred.name,
|
||||||
name=cred.name,
|
credential_type=cred.credential_type.pk,
|
||||||
credential_type=cred.credential_type.pk,
|
passwords_needed=cred.passwords_needed
|
||||||
passwords_needed=cred.passwords_needed
|
)
|
||||||
)
|
if cred.credential_type.managed_by_tower and 'vault_id' in cred.credential_type.defined_fields:
|
||||||
if cred.credential_type.managed_by_tower and 'vault_id' in cred.credential_type.defined_fields:
|
cred_dict['vault_id'] = cred.get_input('vault_id', default=None)
|
||||||
cred_dict['vault_id'] = cred.get_input('vault_id', default=None)
|
defaults_dict.setdefault(field_name, []).append(cred_dict)
|
||||||
defaults_dict.setdefault(field_name, []).append(cred_dict)
|
|
||||||
else:
|
else:
|
||||||
defaults_dict[field_name] = getattr(obj, field_name)
|
defaults_dict[field_name] = getattr(obj, field_name)
|
||||||
return defaults_dict
|
return defaults_dict
|
||||||
@@ -4584,9 +4191,8 @@ class NotificationTemplateSerializer(BaseSerializer):
|
|||||||
res.update(dict(
|
res.update(dict(
|
||||||
test = self.reverse('api:notification_template_test', kwargs={'pk': obj.pk}),
|
test = self.reverse('api:notification_template_test', kwargs={'pk': obj.pk}),
|
||||||
notifications = self.reverse('api:notification_template_notification_list', kwargs={'pk': obj.pk}),
|
notifications = self.reverse('api:notification_template_notification_list', kwargs={'pk': obj.pk}),
|
||||||
|
copy = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if self.version > 1:
|
|
||||||
res['copy'] = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk})
|
|
||||||
if obj.organization:
|
if obj.organization:
|
||||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ job template.
|
|||||||
|
|
||||||
For example, using curl:
|
For example, using curl:
|
||||||
|
|
||||||
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY"}' http://server/api/v1/job_templates/N/callback/
|
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY"}' http://server/api/v2/job_templates/N/callback/
|
||||||
|
|
||||||
Or using wget:
|
Or using wget:
|
||||||
|
|
||||||
wget -O /dev/null --post-data='{"host_config_key": "HOST_CONFIG_KEY"}' --header=Content-Type:application/json http://server/api/v1/job_templates/N/callback/
|
wget -O /dev/null --post-data='{"host_config_key": "HOST_CONFIG_KEY"}' --header=Content-Type:application/json http://server/api/v2/job_templates/N/callback/
|
||||||
|
|
||||||
You may also pass `extra_vars` to the callback:
|
You may also pass `extra_vars` to the callback:
|
||||||
|
|
||||||
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY", "extra_vars": {"key": "value"}}' http://server/api/v1/job_templates/N/callback/
|
curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY", "extra_vars": {"key": "value"}}' http://server/api/v2/job_templates/N/callback/
|
||||||
|
|
||||||
The response will return status 202 if the request is valid, 403 for an
|
The response will return status 202 if the request is valid, 403 for an
|
||||||
invalid host config key, or 400 if the host cannot be determined from the
|
invalid host config key, or 400 if the host cannot be determined from the
|
||||||
@@ -30,7 +30,7 @@ A GET request may be used to verify that the correct host will be selected.
|
|||||||
This request must authenticate as a valid user with permission to edit the
|
This request must authenticate as a valid user with permission to edit the
|
||||||
job template. For example:
|
job template. For example:
|
||||||
|
|
||||||
curl http://user:password@server/api/v1/job_templates/N/callback/
|
curl http://user:password@server/api/v2/job_templates/N/callback/
|
||||||
|
|
||||||
The response will include the host config key as well as the host name(s)
|
The response will include the host config key as well as the host name(s)
|
||||||
that would match the request:
|
that would match the request:
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ One result should be returned containing the following fields:
|
|||||||
|
|
||||||
{% include "api/_result_fields_common.md" %}
|
{% include "api/_result_fields_common.md" %}
|
||||||
|
|
||||||
Use the primary URL for the user (/api/v1/users/N/) to modify the user.
|
Use the primary URL for the user (/api/v2/users/N/) to modify the user.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.conf.urls import url
|
|||||||
from awx.api.views import (
|
from awx.api.views import (
|
||||||
JobList,
|
JobList,
|
||||||
JobDetail,
|
JobDetail,
|
||||||
JobStart,
|
|
||||||
JobCancel,
|
JobCancel,
|
||||||
JobRelaunch,
|
JobRelaunch,
|
||||||
JobCreateSchedule,
|
JobCreateSchedule,
|
||||||
@@ -23,7 +22,6 @@ from awx.api.views import (
|
|||||||
urls = [
|
urls = [
|
||||||
url(r'^$', JobList.as_view(), name='job_list'),
|
url(r'^$', JobList.as_view(), name='job_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', JobDetail.as_view(), name='job_detail'),
|
url(r'^(?P<pk>[0-9]+)/$', JobDetail.as_view(), name='job_detail'),
|
||||||
url(r'^(?P<pk>[0-9]+)/start/$', JobStart.as_view(), name='job_start'), # Todo: Remove In 3.3
|
|
||||||
url(r'^(?P<pk>[0-9]+)/cancel/$', JobCancel.as_view(), name='job_cancel'),
|
url(r'^(?P<pk>[0-9]+)/cancel/$', JobCancel.as_view(), name='job_cancel'),
|
||||||
url(r'^(?P<pk>[0-9]+)/relaunch/$', JobRelaunch.as_view(), name='job_relaunch'),
|
url(r'^(?P<pk>[0-9]+)/relaunch/$', JobRelaunch.as_view(), name='job_relaunch'),
|
||||||
url(r'^(?P<pk>[0-9]+)/create_schedule/$', JobCreateSchedule.as_view(), name='job_create_schedule'),
|
url(r'^(?P<pk>[0-9]+)/create_schedule/$', JobCreateSchedule.as_view(), name='job_create_schedule'),
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ from awx.api.generics import (
|
|||||||
)
|
)
|
||||||
from awx.api.views import (
|
from awx.api.views import (
|
||||||
ApiRootView,
|
ApiRootView,
|
||||||
ApiV1RootView,
|
|
||||||
ApiV2RootView,
|
ApiV2RootView,
|
||||||
ApiV1PingView,
|
ApiV2PingView,
|
||||||
ApiV1ConfigView,
|
ApiV2ConfigView,
|
||||||
AuthView,
|
AuthView,
|
||||||
UserMeList,
|
UserMeList,
|
||||||
DashboardView,
|
DashboardView,
|
||||||
@@ -74,10 +73,25 @@ from .oauth2 import urls as oauth2_urls
|
|||||||
from .oauth2_root import urls as oauth2_root_urls
|
from .oauth2_root import urls as oauth2_root_urls
|
||||||
|
|
||||||
|
|
||||||
v1_urls = [
|
v2_urls = [
|
||||||
url(r'^$', ApiV1RootView.as_view(), name='api_v1_root_view'),
|
url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'),
|
||||||
url(r'^ping/$', ApiV1PingView.as_view(), name='api_v1_ping_view'),
|
url(r'^credential_types/', include(credential_type_urls)),
|
||||||
url(r'^config/$', ApiV1ConfigView.as_view(), name='api_v1_config_view'),
|
url(r'^credential_input_sources/', include(credential_input_source_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'),
|
||||||
|
url(r'^schedules/preview/$', SchedulePreview.as_view(), name='schedule_rrule'),
|
||||||
|
url(r'^schedules/zoneinfo/$', ScheduleZoneInfo.as_view(), name='schedule_zoneinfo'),
|
||||||
|
url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
||||||
|
url(r'^applications/(?P<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
|
||||||
|
url(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'),
|
||||||
|
url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
||||||
|
url(r'^', include(oauth2_urls)),
|
||||||
|
url(r'^metrics/$', MetricsView.as_view(), name='metrics_view'),
|
||||||
|
url(r'^ping/$', ApiV2PingView.as_view(), name='api_v2_ping_view'),
|
||||||
|
url(r'^config/$', ApiV2ConfigView.as_view(), name='api_v2_config_view'),
|
||||||
url(r'^auth/$', AuthView.as_view()),
|
url(r'^auth/$', AuthView.as_view()),
|
||||||
url(r'^me/$', UserMeList.as_view(), name='user_me_list'),
|
url(r'^me/$', UserMeList.as_view(), name='user_me_list'),
|
||||||
url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'),
|
url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'),
|
||||||
@@ -119,30 +133,10 @@ v1_urls = [
|
|||||||
url(r'^activity_stream/', include(activity_stream_urls)),
|
url(r'^activity_stream/', include(activity_stream_urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
v2_urls = [
|
|
||||||
url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'),
|
|
||||||
url(r'^credential_types/', include(credential_type_urls)),
|
|
||||||
url(r'^credential_input_sources/', include(credential_input_source_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'),
|
|
||||||
url(r'^schedules/preview/$', SchedulePreview.as_view(), name='schedule_rrule'),
|
|
||||||
url(r'^schedules/zoneinfo/$', ScheduleZoneInfo.as_view(), name='schedule_zoneinfo'),
|
|
||||||
url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
|
||||||
url(r'^applications/(?P<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
|
|
||||||
url(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'),
|
|
||||||
url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
|
||||||
url(r'^', include(oauth2_urls)),
|
|
||||||
url(r'^metrics/$', MetricsView.as_view(), name='metrics_view'),
|
|
||||||
]
|
|
||||||
|
|
||||||
app_name = 'api'
|
app_name = 'api'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
|
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
|
||||||
url(r'^(?P<version>(v2))/', include(v2_urls)),
|
url(r'^(?P<version>(v2))/', include(v2_urls)),
|
||||||
url(r'^(?P<version>(v1|v2))/', include(v1_urls)),
|
|
||||||
url(r'^login/$', LoggedLoginView.as_view(
|
url(r'^login/$', LoggedLoginView.as_view(
|
||||||
template_name='rest_framework/login.html',
|
template_name='rest_framework/login.html',
|
||||||
extra_context={'inside_login_context': True}
|
extra_context={'inside_login_context': True}
|
||||||
|
|||||||
@@ -27,19 +27,6 @@ def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **e
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def get_request_version(request):
|
|
||||||
"""
|
|
||||||
The API version of a request as an integer i.e., 1 or 2
|
|
||||||
"""
|
|
||||||
version = settings.REST_FRAMEWORK['DEFAULT_VERSION']
|
|
||||||
if request and hasattr(request, 'version'):
|
|
||||||
version = request.version
|
|
||||||
if version is None:
|
|
||||||
# For requests to /api/
|
|
||||||
return None
|
|
||||||
return int(version.lstrip('v'))
|
|
||||||
|
|
||||||
|
|
||||||
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||||
if request is None or getattr(request, 'version', None) is None:
|
if request is None or getattr(request, 'version', None) is None:
|
||||||
# We need the "current request" to determine the correct version to
|
# We need the "current request" to determine the correct version to
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ from wsgiref.util import FileWrapper
|
|||||||
# AWX
|
# AWX
|
||||||
from awx.main.tasks import send_notifications, update_inventory_computed_fields
|
from awx.main.tasks import send_notifications, update_inventory_computed_fields
|
||||||
from awx.main.access import get_user_queryset, HostAccess
|
from awx.main.access import get_user_queryset, HostAccess
|
||||||
from awx.api.filters import V1CredentialFilterBackend
|
|
||||||
from awx.api.generics import (
|
from awx.api.generics import (
|
||||||
APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin,
|
APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin,
|
||||||
GenericAPIView, ListAPIView, ListCreateAPIView,
|
GenericAPIView, ListAPIView, ListCreateAPIView,
|
||||||
@@ -72,7 +71,7 @@ from awx.api.generics import (
|
|||||||
SubListCreateAPIView, SubListCreateAttachDetachAPIView,
|
SubListCreateAPIView, SubListCreateAttachDetachAPIView,
|
||||||
SubListDestroyAPIView, get_view_name
|
SubListDestroyAPIView, get_view_name
|
||||||
)
|
)
|
||||||
from awx.api.versioning import reverse, get_request_version
|
from awx.api.versioning import reverse
|
||||||
from awx.conf.license import get_license
|
from awx.conf.license import get_license
|
||||||
from awx.main import models
|
from awx.main import models
|
||||||
from awx.main.utils import (
|
from awx.main.utils import (
|
||||||
@@ -96,7 +95,7 @@ from awx.api.permissions import (
|
|||||||
)
|
)
|
||||||
from awx.api import renderers
|
from awx.api import renderers
|
||||||
from awx.api import serializers
|
from awx.api import serializers
|
||||||
from awx.api.metadata import RoleMetadata, JobTypeMetadata
|
from awx.api.metadata import RoleMetadata
|
||||||
from awx.main.constants import ACTIVE_STATES
|
from awx.main.constants import ACTIVE_STATES
|
||||||
from awx.main.scheduler.dag_workflow import WorkflowDAG
|
from awx.main.scheduler.dag_workflow import WorkflowDAG
|
||||||
from awx.api.views.mixin import (
|
from awx.api.views.mixin import (
|
||||||
@@ -143,10 +142,9 @@ from awx.api.views.root import ( # noqa
|
|||||||
ApiRootView,
|
ApiRootView,
|
||||||
ApiOAuthAuthorizationRootView,
|
ApiOAuthAuthorizationRootView,
|
||||||
ApiVersionRootView,
|
ApiVersionRootView,
|
||||||
ApiV1RootView,
|
|
||||||
ApiV2RootView,
|
ApiV2RootView,
|
||||||
ApiV1PingView,
|
ApiV2PingView,
|
||||||
ApiV1ConfigView,
|
ApiV2ConfigView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1246,22 +1244,10 @@ class CredentialTypeActivityStreamList(SubListAPIView):
|
|||||||
search_fields = ('changes',)
|
search_fields = ('changes',)
|
||||||
|
|
||||||
|
|
||||||
# remove in 3.3
|
class CredentialList(ListCreateAPIView):
|
||||||
class CredentialViewMixin(object):
|
|
||||||
|
|
||||||
@property
|
|
||||||
def related_search_fields(self):
|
|
||||||
ret = super(CredentialViewMixin, self).related_search_fields
|
|
||||||
if get_request_version(self.request) == 1 and 'credential_type__search' in ret:
|
|
||||||
ret.remove('credential_type__search')
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class CredentialList(CredentialViewMixin, ListCreateAPIView):
|
|
||||||
|
|
||||||
model = models.Credential
|
model = models.Credential
|
||||||
serializer_class = serializers.CredentialSerializerCreate
|
serializer_class = serializers.CredentialSerializerCreate
|
||||||
filter_backends = ListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
|
||||||
|
|
||||||
|
|
||||||
class CredentialOwnerUsersList(SubListAPIView):
|
class CredentialOwnerUsersList(SubListAPIView):
|
||||||
@@ -1289,13 +1275,12 @@ class CredentialOwnerTeamsList(SubListAPIView):
|
|||||||
return self.model.objects.filter(pk__in=teams)
|
return self.model.objects.filter(pk__in=teams)
|
||||||
|
|
||||||
|
|
||||||
class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
class UserCredentialsList(SubListCreateAPIView):
|
||||||
|
|
||||||
model = models.Credential
|
model = models.Credential
|
||||||
serializer_class = serializers.UserCredentialSerializerCreate
|
serializer_class = serializers.UserCredentialSerializerCreate
|
||||||
parent_model = models.User
|
parent_model = models.User
|
||||||
parent_key = 'user'
|
parent_key = 'user'
|
||||||
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.get_parent_object()
|
user = self.get_parent_object()
|
||||||
@@ -1306,13 +1291,12 @@ class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
|||||||
return user_creds & visible_creds
|
return user_creds & visible_creds
|
||||||
|
|
||||||
|
|
||||||
class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
class TeamCredentialsList(SubListCreateAPIView):
|
||||||
|
|
||||||
model = models.Credential
|
model = models.Credential
|
||||||
serializer_class = serializers.TeamCredentialSerializerCreate
|
serializer_class = serializers.TeamCredentialSerializerCreate
|
||||||
parent_model = models.Team
|
parent_model = models.Team
|
||||||
parent_key = 'team'
|
parent_key = 'team'
|
||||||
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
team = self.get_parent_object()
|
team = self.get_parent_object()
|
||||||
@@ -1323,13 +1307,12 @@ class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView):
|
|||||||
return (team_creds & visible_creds).distinct()
|
return (team_creds & visible_creds).distinct()
|
||||||
|
|
||||||
|
|
||||||
class OrganizationCredentialList(CredentialViewMixin, SubListCreateAPIView):
|
class OrganizationCredentialList(SubListCreateAPIView):
|
||||||
|
|
||||||
model = models.Credential
|
model = models.Credential
|
||||||
serializer_class = serializers.OrganizationCredentialSerializerCreate
|
serializer_class = serializers.OrganizationCredentialSerializerCreate
|
||||||
parent_model = models.Organization
|
parent_model = models.Organization
|
||||||
parent_key = 'organization'
|
parent_key = 'organization'
|
||||||
filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
organization = self.get_parent_object()
|
organization = self.get_parent_object()
|
||||||
@@ -1348,7 +1331,6 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
|
|
||||||
model = models.Credential
|
model = models.Credential
|
||||||
serializer_class = serializers.CredentialSerializer
|
serializer_class = serializers.CredentialSerializer
|
||||||
filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend]
|
|
||||||
|
|
||||||
|
|
||||||
class CredentialActivityStreamList(SubListAPIView):
|
class CredentialActivityStreamList(SubListAPIView):
|
||||||
@@ -1754,10 +1736,10 @@ class EnforceParentRelationshipMixin(object):
|
|||||||
* Tower uses a shallow (2-deep only) url pattern. For example:
|
* Tower uses a shallow (2-deep only) url pattern. For example:
|
||||||
|
|
||||||
When an object hangs off of a parent object you would have the url of the
|
When an object hangs off of a parent object you would have the url of the
|
||||||
form /api/v1/parent_model/34/child_model. If you then wanted a child of the
|
form /api/v2/parent_model/34/child_model. If you then wanted a child of the
|
||||||
child model you would NOT do /api/v1/parent_model/34/child_model/87/child_child_model
|
child model you would NOT do /api/v2/parent_model/34/child_model/87/child_child_model
|
||||||
Instead, you would access the child_child_model via /api/v1/child_child_model/87/
|
Instead, you would access the child_child_model via /api/v2/child_child_model/87/
|
||||||
and you would create child_child_model's off of /api/v1/child_model/87/child_child_model_set
|
and you would create child_child_model's off of /api/v2/child_model/87/child_child_model_set
|
||||||
Now, when creating child_child_model related to child_model you still want to
|
Now, when creating child_child_model related to child_model you still want to
|
||||||
link child_child_model to parent_model. That's what this class is for
|
link child_child_model to parent_model. That's what this class is for
|
||||||
'''
|
'''
|
||||||
@@ -1899,11 +1881,6 @@ class GroupDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveU
|
|||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if not request.user.can_access(self.model, 'delete', obj):
|
if not request.user.can_access(self.model, 'delete', obj):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
if get_request_version(request) == 1: # TODO: deletion of automatic inventory_source, remove in 3.3
|
|
||||||
try:
|
|
||||||
obj.deprecated_inventory_source.delete()
|
|
||||||
except models.Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
obj.delete_recursive()
|
obj.delete_recursive()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@@ -2093,13 +2070,6 @@ class InventorySourceList(ListCreateAPIView):
|
|||||||
serializer_class = serializers.InventorySourceSerializer
|
serializer_class = serializers.InventorySourceSerializer
|
||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
|
|
||||||
@property
|
|
||||||
def allowed_methods(self):
|
|
||||||
methods = super(InventorySourceList, self).allowed_methods
|
|
||||||
if get_request_version(getattr(self, 'request', None)) == 1:
|
|
||||||
methods.remove('POST')
|
|
||||||
return methods
|
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
@@ -2290,7 +2260,6 @@ class InventoryUpdateNotificationsList(SubListAPIView):
|
|||||||
class JobTemplateList(ListCreateAPIView):
|
class JobTemplateList(ListCreateAPIView):
|
||||||
|
|
||||||
model = models.JobTemplate
|
model = models.JobTemplate
|
||||||
metadata_class = JobTypeMetadata
|
|
||||||
serializer_class = serializers.JobTemplateSerializer
|
serializer_class = serializers.JobTemplateSerializer
|
||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
|
|
||||||
@@ -2305,7 +2274,6 @@ class JobTemplateList(ListCreateAPIView):
|
|||||||
class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
model = models.JobTemplate
|
model = models.JobTemplate
|
||||||
metadata_class = JobTypeMetadata
|
|
||||||
serializer_class = serializers.JobTemplateSerializer
|
serializer_class = serializers.JobTemplateSerializer
|
||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
|
|
||||||
@@ -2314,7 +2282,6 @@ class JobTemplateLaunch(RetrieveAPIView):
|
|||||||
|
|
||||||
model = models.JobTemplate
|
model = models.JobTemplate
|
||||||
obj_permission_type = 'start'
|
obj_permission_type = 'start'
|
||||||
metadata_class = JobTypeMetadata
|
|
||||||
serializer_class = serializers.JobLaunchSerializer
|
serializer_class = serializers.JobLaunchSerializer
|
||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
|
|
||||||
@@ -2358,65 +2325,44 @@ class JobTemplateLaunch(RetrieveAPIView):
|
|||||||
ignored_fields = {}
|
ignored_fields = {}
|
||||||
modern_data = data.copy()
|
modern_data = data.copy()
|
||||||
|
|
||||||
for fd in ('credential', 'vault_credential', 'inventory'):
|
id_fd = '{}_id'.format('inventory')
|
||||||
id_fd = '{}_id'.format(fd)
|
if 'inventory' not in modern_data and id_fd in modern_data:
|
||||||
if fd not in modern_data and id_fd in modern_data:
|
modern_data['inventory'] = modern_data[id_fd]
|
||||||
modern_data[fd] = modern_data[id_fd]
|
|
||||||
|
|
||||||
# This block causes `extra_credentials` to _always_ raise error if
|
|
||||||
# the launch endpoint if we're accessing `/api/v1/`
|
|
||||||
if get_request_version(self.request) == 1 and 'extra_credentials' in modern_data:
|
|
||||||
raise ParseError({"extra_credentials": _(
|
|
||||||
"Field is not allowed for use with v1 API."
|
|
||||||
)})
|
|
||||||
|
|
||||||
# Automatically convert legacy launch credential arguments into a list of `.credentials`
|
# Automatically convert legacy launch credential arguments into a list of `.credentials`
|
||||||
if 'credentials' in modern_data and (
|
if 'credentials' in modern_data and 'extra_credentials' in modern_data:
|
||||||
'credential' in modern_data or
|
|
||||||
'vault_credential' in modern_data or
|
|
||||||
'extra_credentials' in modern_data
|
|
||||||
):
|
|
||||||
raise ParseError({"error": _(
|
raise ParseError({"error": _(
|
||||||
"'credentials' cannot be used in combination with 'credential', 'vault_credential', or 'extra_credentials'."
|
"'credentials' cannot be used in combination with 'extra_credentials'."
|
||||||
)})
|
)})
|
||||||
|
|
||||||
if (
|
if 'extra_credentials' in modern_data:
|
||||||
'credential' in modern_data or
|
|
||||||
'vault_credential' in modern_data or
|
|
||||||
'extra_credentials' in modern_data
|
|
||||||
):
|
|
||||||
# make a list of the current credentials
|
# make a list of the current credentials
|
||||||
existing_credentials = obj.credentials.all()
|
existing_credentials = obj.credentials.all()
|
||||||
template_credentials = list(existing_credentials) # save copy of existing
|
template_credentials = list(existing_credentials) # save copy of existing
|
||||||
new_credentials = []
|
new_credentials = []
|
||||||
for key, conditional, _type, type_repr in (
|
if 'extra_credentials' in modern_data:
|
||||||
('credential', lambda cred: cred.credential_type.kind != 'ssh', int, 'pk value'),
|
existing_credentials = [
|
||||||
('vault_credential', lambda cred: cred.credential_type.kind != 'vault', int, 'pk value'),
|
cred for cred in existing_credentials
|
||||||
('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'), Iterable, 'a list')
|
if cred.credential_type.kind not in ('cloud', 'net')
|
||||||
):
|
]
|
||||||
if key in modern_data:
|
prompted_value = modern_data.pop('extra_credentials')
|
||||||
# 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 = modern_data.pop(key)
|
|
||||||
|
|
||||||
# validate type, since these are not covered by a serializer
|
# validate type, since these are not covered by a serializer
|
||||||
if not isinstance(prompted_value, _type):
|
if not isinstance(prompted_value, Iterable):
|
||||||
msg = _(
|
msg = _(
|
||||||
"Incorrect type. Expected {}, received {}."
|
"Incorrect type. Expected a list received {}."
|
||||||
).format(type_repr, prompted_value.__class__.__name__)
|
).format(prompted_value.__class__.__name__)
|
||||||
raise ParseError({key: [msg], 'credentials': [msg]})
|
raise ParseError({'extra_credentials': [msg], 'credentials': [msg]})
|
||||||
|
|
||||||
# add the deprecated credential specified in the request
|
# add the deprecated credential specified in the request
|
||||||
if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str):
|
if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str):
|
||||||
prompted_value = [prompted_value]
|
prompted_value = [prompted_value]
|
||||||
|
|
||||||
# If user gave extra_credentials, special case to use exactly
|
# If user gave extra_credentials, special case to use exactly
|
||||||
# the given list without merging with JT credentials
|
# the given list without merging with JT credentials
|
||||||
if key == 'extra_credentials' and prompted_value:
|
if prompted_value:
|
||||||
obj._deprecated_credential_launch = True # signal to not merge credentials
|
obj._deprecated_credential_launch = True # signal to not merge credentials
|
||||||
new_credentials.extend(prompted_value)
|
new_credentials.extend(prompted_value)
|
||||||
|
|
||||||
# combine the list of "new" and the filtered list of "old"
|
# combine the list of "new" and the filtered list of "old"
|
||||||
new_credentials.extend([cred.pk for cred in existing_credentials])
|
new_credentials.extend([cred.pk for cred in existing_credentials])
|
||||||
@@ -2926,7 +2872,7 @@ class JobTemplateCallback(GenericAPIView):
|
|||||||
return Response(status=status.HTTP_201_CREATED, headers=headers)
|
return Response(status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateJobsList(SubListCreateAPIView):
|
class JobTemplateJobsList(SubListAPIView):
|
||||||
|
|
||||||
model = models.Job
|
model = models.Job
|
||||||
serializer_class = serializers.JobListSerializer
|
serializer_class = serializers.JobListSerializer
|
||||||
@@ -2934,13 +2880,6 @@ class JobTemplateJobsList(SubListCreateAPIView):
|
|||||||
relationship = 'jobs'
|
relationship = 'jobs'
|
||||||
parent_key = 'job_template'
|
parent_key = 'job_template'
|
||||||
|
|
||||||
@property
|
|
||||||
def allowed_methods(self):
|
|
||||||
methods = super(JobTemplateJobsList, self).allowed_methods
|
|
||||||
if get_request_version(getattr(self, 'request', None)) > 1:
|
|
||||||
methods.remove('POST')
|
|
||||||
return methods
|
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView):
|
class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView):
|
||||||
|
|
||||||
@@ -3135,8 +3074,6 @@ class WorkflowJobTemplateCopy(CopyAPIView):
|
|||||||
copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer
|
copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if get_request_version(request) < 2:
|
|
||||||
return self.v1_not_allowed()
|
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if not request.user.can_access(obj.__class__, 'read', obj):
|
if not request.user.can_access(obj.__class__, 'read', obj):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
@@ -3493,56 +3430,17 @@ class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetac
|
|||||||
relationship = 'notification_templates_success'
|
relationship = 'notification_templates_success'
|
||||||
|
|
||||||
|
|
||||||
class JobList(ListCreateAPIView):
|
class JobList(ListAPIView):
|
||||||
|
|
||||||
model = models.Job
|
model = models.Job
|
||||||
metadata_class = JobTypeMetadata
|
|
||||||
serializer_class = serializers.JobListSerializer
|
serializer_class = serializers.JobListSerializer
|
||||||
|
|
||||||
@property
|
|
||||||
def allowed_methods(self):
|
|
||||||
methods = super(JobList, self).allowed_methods
|
|
||||||
if get_request_version(getattr(self, 'request', None)) > 1:
|
|
||||||
methods.remove('POST')
|
|
||||||
return methods
|
|
||||||
|
|
||||||
# NOTE: Remove in 3.3, switch ListCreateAPIView to ListAPIView
|
class JobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
if get_request_version(self.request) > 1:
|
|
||||||
return Response({"error": _("POST not allowed for Job launching in version 2 of the api")},
|
|
||||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
||||||
return super(JobList, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView):
|
|
||||||
|
|
||||||
model = models.Job
|
model = models.Job
|
||||||
metadata_class = JobTypeMetadata
|
|
||||||
serializer_class = serializers.JobDetailSerializer
|
serializer_class = serializers.JobDetailSerializer
|
||||||
|
|
||||||
# NOTE: When removing the V1 API in 3.4, delete the following four methods,
|
|
||||||
# and let this class inherit from RetrieveDestroyAPIView instead of
|
|
||||||
# RetrieveUpdateDestroyAPIView.
|
|
||||||
@property
|
|
||||||
def allowed_methods(self):
|
|
||||||
methods = super(JobDetail, self).allowed_methods
|
|
||||||
if get_request_version(getattr(self, 'request', None)) > 1:
|
|
||||||
methods.remove('PUT')
|
|
||||||
methods.remove('PATCH')
|
|
||||||
return methods
|
|
||||||
|
|
||||||
def put(self, request, *args, **kwargs):
|
|
||||||
if get_request_version(self.request) > 1:
|
|
||||||
return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")},
|
|
||||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
||||||
return super(JobDetail, self).put(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
if get_request_version(self.request) > 1:
|
|
||||||
return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")},
|
|
||||||
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
||||||
return super(JobDetail, self).patch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
# Only allow changes (PUT/PATCH) when job status is "new".
|
# Only allow changes (PUT/PATCH) when job status is "new".
|
||||||
@@ -3591,44 +3489,6 @@ class JobActivityStreamList(SubListAPIView):
|
|||||||
search_fields = ('changes',)
|
search_fields = ('changes',)
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove endpoint in 3.3
|
|
||||||
class JobStart(GenericAPIView):
|
|
||||||
|
|
||||||
model = models.Job
|
|
||||||
obj_permission_type = 'start'
|
|
||||||
serializer_class = serializers.EmptySerializer
|
|
||||||
deprecated = True
|
|
||||||
|
|
||||||
def v2_not_allowed(self):
|
|
||||||
return Response({'detail': 'Action only possible through v1 API.'},
|
|
||||||
status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
if get_request_version(request) > 1:
|
|
||||||
return self.v2_not_allowed()
|
|
||||||
obj = self.get_object()
|
|
||||||
data = dict(
|
|
||||||
can_start=obj.can_start,
|
|
||||||
)
|
|
||||||
if obj.can_start:
|
|
||||||
data['passwords_needed_to_start'] = obj.passwords_needed_to_start
|
|
||||||
return Response(data)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
if get_request_version(request) > 1:
|
|
||||||
return self.v2_not_allowed()
|
|
||||||
obj = self.get_object()
|
|
||||||
if obj.can_start:
|
|
||||||
result = obj.signal_start(**request.data)
|
|
||||||
if not result:
|
|
||||||
data = dict(passwords_needed_to_start=obj.passwords_needed_to_start)
|
|
||||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
else:
|
|
||||||
return Response(status=status.HTTP_202_ACCEPTED)
|
|
||||||
else:
|
|
||||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class JobCancel(RetrieveAPIView):
|
class JobCancel(RetrieveAPIView):
|
||||||
|
|
||||||
model = models.Job
|
model = models.Job
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ from awx.api.serializers import (
|
|||||||
InstanceGroupSerializer,
|
InstanceGroupSerializer,
|
||||||
InventoryUpdateEventSerializer,
|
InventoryUpdateEventSerializer,
|
||||||
CustomInventoryScriptSerializer,
|
CustomInventoryScriptSerializer,
|
||||||
InventoryDetailSerializer,
|
|
||||||
JobTemplateSerializer,
|
JobTemplateSerializer,
|
||||||
)
|
)
|
||||||
from awx.api.views.mixin import (
|
from awx.api.views.mixin import (
|
||||||
@@ -119,7 +118,7 @@ class InventoryList(ListCreateAPIView):
|
|||||||
class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
|
class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
model = Inventory
|
model = Inventory
|
||||||
serializer_class = InventoryDetailSerializer
|
serializer_class = InventorySerializer
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from awx.main.utils import (
|
|||||||
get_custom_venv_choices,
|
get_custom_venv_choices,
|
||||||
to_python_boolean,
|
to_python_boolean,
|
||||||
)
|
)
|
||||||
from awx.api.versioning import reverse, get_request_version, drf_reverse
|
from awx.api.versioning import reverse, drf_reverse
|
||||||
from awx.conf.license import get_license
|
from awx.conf.license import get_license
|
||||||
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
@@ -50,12 +50,11 @@ class ApiRootView(APIView):
|
|||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
''' List supported API versions '''
|
''' List supported API versions '''
|
||||||
|
|
||||||
v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'})
|
|
||||||
v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'})
|
v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'})
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
data['description'] = _('AWX REST API')
|
data['description'] = _('AWX REST API')
|
||||||
data['current_version'] = v2
|
data['current_version'] = v2
|
||||||
data['available_versions'] = dict(v1 = v1, v2 = v2)
|
data['available_versions'] = dict(v2 = v2)
|
||||||
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
||||||
data['custom_logo'] = settings.CUSTOM_LOGO
|
data['custom_logo'] = settings.CUSTOM_LOGO
|
||||||
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
||||||
@@ -85,10 +84,10 @@ class ApiVersionRootView(APIView):
|
|||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
''' List top level resources '''
|
''' List top level resources '''
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
data['ping'] = reverse('api:api_v1_ping_view', request=request)
|
data['ping'] = reverse('api:api_v2_ping_view', request=request)
|
||||||
data['instances'] = reverse('api:instance_list', request=request)
|
data['instances'] = reverse('api:instance_list', request=request)
|
||||||
data['instance_groups'] = reverse('api:instance_group_list', request=request)
|
data['instance_groups'] = reverse('api:instance_group_list', request=request)
|
||||||
data['config'] = reverse('api:api_v1_config_view', request=request)
|
data['config'] = reverse('api:api_v2_config_view', request=request)
|
||||||
data['settings'] = reverse('api:setting_category_list', request=request)
|
data['settings'] = reverse('api:setting_category_list', request=request)
|
||||||
data['me'] = reverse('api:user_me_list', request=request)
|
data['me'] = reverse('api:user_me_list', request=request)
|
||||||
data['dashboard'] = reverse('api:dashboard_view', request=request)
|
data['dashboard'] = reverse('api:dashboard_view', request=request)
|
||||||
@@ -98,12 +97,11 @@ class ApiVersionRootView(APIView):
|
|||||||
data['project_updates'] = reverse('api:project_update_list', request=request)
|
data['project_updates'] = reverse('api:project_update_list', request=request)
|
||||||
data['teams'] = reverse('api:team_list', request=request)
|
data['teams'] = reverse('api:team_list', request=request)
|
||||||
data['credentials'] = reverse('api:credential_list', request=request)
|
data['credentials'] = reverse('api:credential_list', request=request)
|
||||||
if get_request_version(request) > 1:
|
data['credential_types'] = reverse('api:credential_type_list', request=request)
|
||||||
data['credential_types'] = reverse('api:credential_type_list', request=request)
|
data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request)
|
||||||
data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request)
|
data['applications'] = reverse('api:o_auth2_application_list', request=request)
|
||||||
data['applications'] = reverse('api:o_auth2_application_list', request=request)
|
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
||||||
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
data['metrics'] = reverse('api:metrics_view', request=request)
|
||||||
data['metrics'] = reverse('api:metrics_view', request=request)
|
|
||||||
data['inventory'] = reverse('api:inventory_list', request=request)
|
data['inventory'] = reverse('api:inventory_list', request=request)
|
||||||
data['inventory_scripts'] = reverse('api:inventory_script_list', request=request)
|
data['inventory_scripts'] = reverse('api:inventory_script_list', request=request)
|
||||||
data['inventory_sources'] = reverse('api:inventory_source_list', request=request)
|
data['inventory_sources'] = reverse('api:inventory_source_list', request=request)
|
||||||
@@ -131,15 +129,11 @@ class ApiVersionRootView(APIView):
|
|||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
class ApiV1RootView(ApiVersionRootView):
|
|
||||||
view_name = _('Version 1')
|
|
||||||
|
|
||||||
|
|
||||||
class ApiV2RootView(ApiVersionRootView):
|
class ApiV2RootView(ApiVersionRootView):
|
||||||
view_name = _('Version 2')
|
view_name = _('Version 2')
|
||||||
|
|
||||||
|
|
||||||
class ApiV1PingView(APIView):
|
class ApiV2PingView(APIView):
|
||||||
"""A simple view that reports very basic information about this
|
"""A simple view that reports very basic information about this
|
||||||
instance, which is acceptable to be public information.
|
instance, which is acceptable to be public information.
|
||||||
"""
|
"""
|
||||||
@@ -174,14 +168,14 @@ class ApiV1PingView(APIView):
|
|||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
class ApiV1ConfigView(APIView):
|
class ApiV2ConfigView(APIView):
|
||||||
|
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
view_name = _('Configuration')
|
view_name = _('Configuration')
|
||||||
swagger_topic = 'System Configuration'
|
swagger_topic = 'System Configuration'
|
||||||
|
|
||||||
def check_permissions(self, request):
|
def check_permissions(self, request):
|
||||||
super(ApiV1ConfigView, self).check_permissions(request)
|
super(ApiV2ConfigView, self).check_permissions(request)
|
||||||
if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}:
|
if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}:
|
||||||
self.permission_denied(request) # Raises PermissionDenied exception.
|
self.permission_denied(request) # Raises PermissionDenied exception.
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class SettingSingletonSerializer(serializers.Serializer):
|
|||||||
continue
|
continue
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
# Make LICENSE and AWX_ISOLATED_KEY_GENERATION read-only here;
|
# Make LICENSE and AWX_ISOLATED_KEY_GENERATION read-only here;
|
||||||
# LICENSE is only updated via /api/v1/config/
|
# LICENSE is only updated via /api/v2/config/
|
||||||
# AWX_ISOLATED_KEY_GENERATION is only set/unset via the setup playbook
|
# AWX_ISOLATED_KEY_GENERATION is only set/unset via the setup playbook
|
||||||
if key in ('LICENSE', 'AWX_ISOLATED_KEY_GENERATION'):
|
if key in ('LICENSE', 'AWX_ISOLATED_KEY_GENERATION'):
|
||||||
extra_kwargs['read_only'] = True
|
extra_kwargs['read_only'] = True
|
||||||
|
|||||||
@@ -65,41 +65,6 @@ def test_non_admin_user_does_not_see_categories(api_request, dummy_setting, norm
|
|||||||
assert not response.data['results']
|
assert not response.data['results']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@mock.patch(
|
|
||||||
'awx.conf.views.VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE',
|
|
||||||
{
|
|
||||||
1: set([]),
|
|
||||||
2: set(['foobar']),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_version_specific_category_slug_to_exclude_does_not_show_up(api_request, dummy_setting):
|
|
||||||
with dummy_setting(
|
|
||||||
'FOO_BAR',
|
|
||||||
field_class=fields.IntegerField,
|
|
||||||
category='FooBar',
|
|
||||||
category_slug='foobar'
|
|
||||||
):
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_category_list',
|
|
||||||
kwargs={'version': 'v2'})
|
|
||||||
)
|
|
||||||
for item in response.data['results']:
|
|
||||||
assert item['slug'] != 'foobar'
|
|
||||||
response = api_request(
|
|
||||||
'get',
|
|
||||||
reverse('api:setting_category_list',
|
|
||||||
kwargs={'version': 'v1'})
|
|
||||||
)
|
|
||||||
contains = False
|
|
||||||
for item in response.data['results']:
|
|
||||||
if item['slug'] != 'foobar':
|
|
||||||
contains = True
|
|
||||||
break
|
|
||||||
assert contains
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
|
def test_setting_singleton_detail_retrieve(api_request, dummy_setting):
|
||||||
with dummy_setting(
|
with dummy_setting(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from awx.api.generics import (
|
|||||||
RetrieveUpdateDestroyAPIView,
|
RetrieveUpdateDestroyAPIView,
|
||||||
)
|
)
|
||||||
from awx.api.permissions import IsSuperUser
|
from awx.api.permissions import IsSuperUser
|
||||||
from awx.api.versioning import reverse, get_request_version
|
from awx.api.versioning import reverse
|
||||||
from awx.main.utils import camelcase_to_underscore
|
from awx.main.utils import camelcase_to_underscore
|
||||||
from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException
|
from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException
|
||||||
from awx.main.tasks import handle_setting_changes
|
from awx.main.tasks import handle_setting_changes
|
||||||
@@ -35,13 +35,6 @@ from awx.conf import settings_registry
|
|||||||
|
|
||||||
SettingCategory = collections.namedtuple('SettingCategory', ('url', 'slug', 'name'))
|
SettingCategory = collections.namedtuple('SettingCategory', ('url', 'slug', 'name'))
|
||||||
|
|
||||||
VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE = {
|
|
||||||
1: set([
|
|
||||||
'named-url',
|
|
||||||
]),
|
|
||||||
2: set([]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SettingCategoryList(ListAPIView):
|
class SettingCategoryList(ListAPIView):
|
||||||
|
|
||||||
@@ -60,8 +53,6 @@ class SettingCategoryList(ListAPIView):
|
|||||||
else:
|
else:
|
||||||
categories = {}
|
categories = {}
|
||||||
for category_slug in sorted(categories.keys()):
|
for category_slug in sorted(categories.keys()):
|
||||||
if category_slug in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]:
|
|
||||||
continue
|
|
||||||
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': category_slug}, request=self.request)
|
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': category_slug}, request=self.request)
|
||||||
setting_categories.append(SettingCategory(url, category_slug, categories[category_slug]))
|
setting_categories.append(SettingCategory(url, category_slug, categories[category_slug]))
|
||||||
return setting_categories
|
return setting_categories
|
||||||
@@ -77,8 +68,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.category_slug = self.kwargs.get('category_slug', 'all')
|
self.category_slug = self.kwargs.get('category_slug', 'all')
|
||||||
all_category_slugs = list(settings_registry.get_registered_categories().keys())
|
all_category_slugs = list(settings_registry.get_registered_categories().keys())
|
||||||
for slug_to_delete in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]:
|
|
||||||
all_category_slugs.remove(slug_to_delete)
|
|
||||||
if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False):
|
if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False):
|
||||||
category_slugs = all_category_slugs
|
category_slugs = all_category_slugs
|
||||||
else:
|
else:
|
||||||
@@ -90,7 +79,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
|
|
||||||
registered_settings = settings_registry.get_registered_settings(
|
registered_settings = settings_registry.get_registered_settings(
|
||||||
category_slug=self.category_slug, read_only=False,
|
category_slug=self.category_slug, read_only=False,
|
||||||
slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
|
|
||||||
)
|
)
|
||||||
if self.category_slug == 'user':
|
if self.category_slug == 'user':
|
||||||
return Setting.objects.filter(key__in=registered_settings, user=self.request.user)
|
return Setting.objects.filter(key__in=registered_settings, user=self.request.user)
|
||||||
@@ -101,7 +89,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
|
|||||||
settings_qs = self.get_queryset()
|
settings_qs = self.get_queryset()
|
||||||
registered_settings = settings_registry.get_registered_settings(
|
registered_settings = settings_registry.get_registered_settings(
|
||||||
category_slug=self.category_slug,
|
category_slug=self.category_slug,
|
||||||
slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
|
|
||||||
)
|
)
|
||||||
all_settings = {}
|
all_settings = {}
|
||||||
for setting in settings_qs:
|
for setting in settings_qs:
|
||||||
|
|||||||
@@ -1001,19 +1001,6 @@ class GroupAccess(BaseAccess):
|
|||||||
def can_delete(self, obj):
|
def can_delete(self, obj):
|
||||||
return bool(obj and self.user in obj.inventory.admin_role)
|
return bool(obj and self.user in obj.inventory.admin_role)
|
||||||
|
|
||||||
def can_start(self, obj, validate_license=True):
|
|
||||||
# TODO: Delete for 3.3, only used by v1 serializer
|
|
||||||
# Used as another alias to inventory_source start access for user_capabilities
|
|
||||||
if obj:
|
|
||||||
try:
|
|
||||||
return self.user.can_access(
|
|
||||||
InventorySource, 'start', obj.deprecated_inventory_source,
|
|
||||||
validate_license=validate_license)
|
|
||||||
obj.deprecated_inventory_source
|
|
||||||
except Group.deprecated_inventory_source.RelatedObjectDoesNotExist:
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceAccess(NotificationAttachMixin, BaseAccess):
|
class InventorySourceAccess(NotificationAttachMixin, BaseAccess):
|
||||||
'''
|
'''
|
||||||
@@ -2387,11 +2374,6 @@ class UnifiedJobTemplateAccess(BaseAccess):
|
|||||||
Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs(
|
Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs(
|
||||||
Inventory, self.user, 'read_role')))
|
Inventory, self.user, 'read_role')))
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
# TODO: remove after the depreciation of v1 API
|
|
||||||
qs = super(UnifiedJobTemplateAccess, self).get_queryset()
|
|
||||||
return qs.exclude(inventorysource__source="")
|
|
||||||
|
|
||||||
def can_start(self, obj, validate_license=True):
|
def can_start(self, obj, validate_license=True):
|
||||||
access_class = access_registry[obj.__class__]
|
access_class = access_registry[obj.__class__]
|
||||||
access_instance = access_class(self.user)
|
access_instance = access_class(self.user)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ register(
|
|||||||
default=_load_default_license_from_file,
|
default=_load_default_license_from_file,
|
||||||
label=_('License'),
|
label=_('License'),
|
||||||
help_text=_('The license controls which features and functionality are '
|
help_text=_('The license controls which features and functionality are '
|
||||||
'enabled. Use /api/v1/config/ to update or change '
|
'enabled. Use /api/v2/config/ to update or change '
|
||||||
'the license.'),
|
'the license.'),
|
||||||
category=_('System'),
|
category=_('System'),
|
||||||
category_slug='system',
|
category_slug='system',
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ class CredentialInputField(JSONSchemaField):
|
|||||||
v != '$encrypted$',
|
v != '$encrypted$',
|
||||||
model_instance.pk
|
model_instance.pk
|
||||||
]):
|
]):
|
||||||
if not isinstance(getattr(model_instance, k), str):
|
if not isinstance(model_instance.inputs.get(k), str):
|
||||||
raise django_exceptions.ValidationError(
|
raise django_exceptions.ValidationError(
|
||||||
_('secret values must be of type string, not {}').format(type(v).__name__),
|
_('secret values must be of type string, not {}').format(type(v).__name__),
|
||||||
code='invalid',
|
code='invalid',
|
||||||
@@ -704,15 +704,15 @@ class CredentialInputField(JSONSchemaField):
|
|||||||
# 'ssh_key_unlock': 'do-you-need-me?',
|
# 'ssh_key_unlock': 'do-you-need-me?',
|
||||||
# }
|
# }
|
||||||
# ...we have to fetch the actual key value from the database
|
# ...we have to fetch the actual key value from the database
|
||||||
if model_instance.pk and model_instance.ssh_key_data == '$encrypted$':
|
if model_instance.pk and model_instance.inputs.get('ssh_key_data') == '$encrypted$':
|
||||||
model_instance.ssh_key_data = model_instance.__class__.objects.get(
|
model_instance.inputs['ssh_key_data'] = model_instance.__class__.objects.get(
|
||||||
pk=model_instance.pk
|
pk=model_instance.pk
|
||||||
).ssh_key_data
|
).inputs.get('ssh_key_data')
|
||||||
|
|
||||||
if model_instance.has_encrypted_ssh_key_data and not value.get('ssh_key_unlock'):
|
if model_instance.has_encrypted_ssh_key_data and not value.get('ssh_key_unlock'):
|
||||||
errors['ssh_key_unlock'] = [_('must be set when SSH key is encrypted.')]
|
errors['ssh_key_unlock'] = [_('must be set when SSH key is encrypted.')]
|
||||||
if all([
|
if all([
|
||||||
model_instance.ssh_key_data,
|
model_instance.inputs.get('ssh_key_data'),
|
||||||
value.get('ssh_key_unlock'),
|
value.get('ssh_key_unlock'),
|
||||||
not model_instance.has_encrypted_ssh_key_data
|
not model_instance.has_encrypted_ssh_key_data
|
||||||
]):
|
]):
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class Command(BaseCommand):
|
|||||||
scm_update_cache_timeout=0,
|
scm_update_cache_timeout=0,
|
||||||
organization=o)
|
organization=o)
|
||||||
p.save(skip_update=True)
|
p.save(skip_update=True)
|
||||||
ssh_type = CredentialType.from_v1_kind('ssh')
|
ssh_type = CredentialType.objects.filter(namespace='ssh').first()
|
||||||
c = Credential.objects.create(credential_type=ssh_type,
|
c = Credential.objects.create(credential_type=ssh_type,
|
||||||
name='Demo Credential',
|
name='Demo Credential',
|
||||||
inputs={
|
inputs={
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from awx.main.models.organization import ( # noqa
|
|||||||
Organization, Profile, Team, UserSessionMembership
|
Organization, Profile, Team, UserSessionMembership
|
||||||
)
|
)
|
||||||
from awx.main.models.credential import ( # noqa
|
from awx.main.models.credential import ( # noqa
|
||||||
Credential, CredentialType, CredentialInputSource, ManagedCredentialType, V1Credential, build_safe_env
|
Credential, CredentialType, CredentialInputSource, ManagedCredentialType, build_safe_env
|
||||||
)
|
)
|
||||||
from awx.main.models.projects import Project, ProjectUpdate # noqa
|
from awx.main.models.projects import Project, ProjectUpdate # noqa
|
||||||
from awx.main.models.inventory import ( # noqa
|
from awx.main.models.inventory import ( # noqa
|
||||||
@@ -174,9 +174,6 @@ User.add_to_class('is_in_enterprise_category', user_is_in_enterprise_category)
|
|||||||
|
|
||||||
|
|
||||||
def o_auth2_application_get_absolute_url(self, request=None):
|
def o_auth2_application_get_absolute_url(self, request=None):
|
||||||
# this page does not exist in v1
|
|
||||||
if request.version == 'v1':
|
|
||||||
return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}) # use default version
|
|
||||||
return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request)
|
return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request)
|
||||||
|
|
||||||
|
|
||||||
@@ -184,9 +181,6 @@ OAuth2Application.add_to_class('get_absolute_url', o_auth2_application_get_absol
|
|||||||
|
|
||||||
|
|
||||||
def o_auth2_token_get_absolute_url(self, request=None):
|
def o_auth2_token_get_absolute_url(self, request=None):
|
||||||
# this page does not exist in v1
|
|
||||||
if request.version == 'v1':
|
|
||||||
return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}) # use default version
|
|
||||||
return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}, request=request)
|
return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}, request=request)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ from awx.main.models.rbac import (
|
|||||||
from awx.main.utils import encrypt_field
|
from awx.main.utils import encrypt_field
|
||||||
from . import injectors as builtin_injectors
|
from . import injectors as builtin_injectors
|
||||||
|
|
||||||
__all__ = ['Credential', 'CredentialType', 'CredentialInputSource', 'V1Credential', 'build_safe_env']
|
__all__ = ['Credential', 'CredentialType', 'CredentialInputSource', 'build_safe_env']
|
||||||
|
|
||||||
logger = logging.getLogger('awx.main.models.credential')
|
logger = logging.getLogger('awx.main.models.credential')
|
||||||
credential_plugins = dict(
|
credential_plugins = dict(
|
||||||
@@ -73,164 +73,6 @@ def build_safe_env(env):
|
|||||||
return safe_env
|
return safe_env
|
||||||
|
|
||||||
|
|
||||||
class V1Credential(object):
|
|
||||||
|
|
||||||
#
|
|
||||||
# API v1 backwards compat; as long as we continue to support the
|
|
||||||
# /api/v1/credentials/ endpoint, we'll keep these definitions around.
|
|
||||||
# The credential serializers are smart enough to detect the request
|
|
||||||
# version and use *these* fields for constructing the serializer if the URL
|
|
||||||
# starts with /api/v1/
|
|
||||||
#
|
|
||||||
PASSWORD_FIELDS = ('password', 'security_token', 'ssh_key_data',
|
|
||||||
'ssh_key_unlock', 'become_password',
|
|
||||||
'vault_password', 'secret', 'authorize_password')
|
|
||||||
KIND_CHOICES = [
|
|
||||||
('ssh', 'Machine'),
|
|
||||||
('net', 'Network'),
|
|
||||||
('scm', 'Source Control'),
|
|
||||||
('aws', 'Amazon Web Services'),
|
|
||||||
('vmware', 'VMware vCenter'),
|
|
||||||
('satellite6', 'Red Hat Satellite 6'),
|
|
||||||
('cloudforms', 'Red Hat CloudForms'),
|
|
||||||
('gce', 'Google Compute Engine'),
|
|
||||||
('azure_rm', 'Microsoft Azure Resource Manager'),
|
|
||||||
('openstack', 'OpenStack'),
|
|
||||||
('rhv', 'Red Hat Virtualization'),
|
|
||||||
('insights', 'Insights'),
|
|
||||||
('tower', 'Ansible Tower'),
|
|
||||||
]
|
|
||||||
FIELDS = {
|
|
||||||
'kind': models.CharField(
|
|
||||||
max_length=32,
|
|
||||||
choices=[
|
|
||||||
(kind[0], _(kind[1]))
|
|
||||||
for kind in KIND_CHOICES
|
|
||||||
],
|
|
||||||
default='ssh',
|
|
||||||
),
|
|
||||||
'cloud': models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
editable=False,
|
|
||||||
),
|
|
||||||
'host': models.CharField(
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
max_length=1024,
|
|
||||||
verbose_name=_('Host'),
|
|
||||||
help_text=_('The hostname or IP address to use.'),
|
|
||||||
),
|
|
||||||
'username': models.CharField(
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
max_length=1024,
|
|
||||||
verbose_name=_('Username'),
|
|
||||||
help_text=_('Username for this credential.'),
|
|
||||||
),
|
|
||||||
'password': models.CharField(
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
max_length=1024,
|
|
||||||
verbose_name=_('Password'),
|
|
||||||
help_text=_('Password for this credential (or "ASK" to prompt the '
|
|
||||||
'user for machine credentials).'),
|
|
||||||
),
|
|
||||||
'security_token': models.CharField(
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
max_length=1024,
|
|
||||||
verbose_name=_('Security Token'),
|
|
||||||
help_text=_('Security Token for this credential'),
|
|
||||||
),
|
|
||||||
'project': models.CharField(
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
max_length=100,
|
|
||||||
verbose_name=_('Project'),
|
|
||||||
help_text=_('The identifier for the project.'),
|
|
||||||
),
|
|
||||||
'domain': models.CharField(
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
max_length=100,
|
|
||||||
verbose_name=_('Domain'),
|
|
||||||
help_text=_('The identifier for the domain.'),
|
|
||||||
),
|
|
||||||
'ssh_key_data': models.TextField(
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
verbose_name=_('SSH private key'),
|
|
||||||
help_text=_('RSA or DSA private key to be used instead of password.'),
|
|
||||||
),
|
|
||||||
'ssh_key_unlock': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
verbose_name=_('SSH key unlock'),
|
|
||||||
help_text=_('Passphrase to unlock SSH private key if encrypted (or '
|
|
||||||
'"ASK" to prompt the user for machine credentials).'),
|
|
||||||
),
|
|
||||||
'become_method': models.CharField(
|
|
||||||
max_length=32,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Privilege escalation method.')
|
|
||||||
),
|
|
||||||
'become_username': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Privilege escalation username.'),
|
|
||||||
),
|
|
||||||
'become_password': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Password for privilege escalation method.')
|
|
||||||
),
|
|
||||||
'vault_password': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Vault password (or "ASK" to prompt the user).'),
|
|
||||||
),
|
|
||||||
'authorize': models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text=_('Whether to use the authorize mechanism.'),
|
|
||||||
),
|
|
||||||
'authorize_password': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Password used by the authorize mechanism.'),
|
|
||||||
),
|
|
||||||
'client': models.CharField(
|
|
||||||
max_length=128,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Client Id or Application Id for the credential'),
|
|
||||||
),
|
|
||||||
'secret': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Secret Token for this credential'),
|
|
||||||
),
|
|
||||||
'subscription': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Subscription identifier for this credential'),
|
|
||||||
),
|
|
||||||
'tenant': models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
blank=True,
|
|
||||||
default='',
|
|
||||||
help_text=_('Tenant identifier for this credential'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||||
'''
|
'''
|
||||||
A credential contains information about how to talk to a remote resource
|
A credential contains information about how to talk to a remote resource
|
||||||
@@ -286,34 +128,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
'admin_role',
|
'admin_role',
|
||||||
])
|
])
|
||||||
|
|
||||||
def __getattr__(self, item):
|
|
||||||
if item != 'inputs':
|
|
||||||
if item in V1Credential.FIELDS:
|
|
||||||
return self.inputs.get(item, V1Credential.FIELDS[item].default)
|
|
||||||
elif item in self.inputs:
|
|
||||||
return self.inputs[item]
|
|
||||||
raise AttributeError(item)
|
|
||||||
|
|
||||||
def __setattr__(self, item, value):
|
|
||||||
if item in V1Credential.FIELDS and item in self.credential_type.defined_fields:
|
|
||||||
if value:
|
|
||||||
self.inputs[item] = value
|
|
||||||
elif item in self.inputs:
|
|
||||||
del self.inputs[item]
|
|
||||||
return
|
|
||||||
super(Credential, self).__setattr__(item, value)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kind(self):
|
def kind(self):
|
||||||
# TODO 3.3: remove the need for this helper property by removing its
|
return self.credential_type.namespace
|
||||||
# usage throughout the codebase
|
|
||||||
type_ = self.credential_type
|
|
||||||
if type_.kind != 'cloud':
|
|
||||||
return type_.kind
|
|
||||||
for field in V1Credential.KIND_CHOICES:
|
|
||||||
kind, name = field
|
|
||||||
if name == type_.name:
|
|
||||||
return kind
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cloud(self):
|
def cloud(self):
|
||||||
@@ -330,7 +147,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
#
|
#
|
||||||
@property
|
@property
|
||||||
def needs_ssh_password(self):
|
def needs_ssh_password(self):
|
||||||
return self.credential_type.kind == 'ssh' and self.password == 'ASK'
|
return self.credential_type.kind == 'ssh' and self.inputs.get('password') == 'ASK'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_encrypted_ssh_key_data(self):
|
def has_encrypted_ssh_key_data(self):
|
||||||
@@ -350,17 +167,17 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def needs_ssh_key_unlock(self):
|
def needs_ssh_key_unlock(self):
|
||||||
if self.credential_type.kind == 'ssh' and self.ssh_key_unlock in ('ASK', ''):
|
if self.credential_type.kind == 'ssh' and self.inputs.get('ssh_key_unlock') in ('ASK', ''):
|
||||||
return self.has_encrypted_ssh_key_data
|
return self.has_encrypted_ssh_key_data
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needs_become_password(self):
|
def needs_become_password(self):
|
||||||
return self.credential_type.kind == 'ssh' and self.become_password == 'ASK'
|
return self.credential_type.kind == 'ssh' and self.inputs.get('become_password') == 'ASK'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needs_vault_password(self):
|
def needs_vault_password(self):
|
||||||
return self.credential_type.kind == 'vault' and self.vault_password == 'ASK'
|
return self.credential_type.kind == 'vault' and self.inputs.get('vault_password') == 'ASK'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def passwords_needed(self):
|
def passwords_needed(self):
|
||||||
@@ -396,6 +213,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
|
|
||||||
super(Credential, self).save(*args, **kwargs)
|
super(Credential, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def mark_field_for_save(self, update_fields, field):
|
||||||
|
if 'inputs' not in update_fields:
|
||||||
|
update_fields.append('inputs')
|
||||||
|
|
||||||
def encrypt_field(self, field, ask):
|
def encrypt_field(self, field, ask):
|
||||||
if field not in self.inputs:
|
if field not in self.inputs:
|
||||||
return None
|
return None
|
||||||
@@ -405,13 +226,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
elif field in self.inputs:
|
elif field in self.inputs:
|
||||||
del self.inputs[field]
|
del self.inputs[field]
|
||||||
|
|
||||||
def mark_field_for_save(self, update_fields, field):
|
|
||||||
if field in self.credential_type.secret_fields:
|
|
||||||
# If we've encrypted a v1 field, we actually want to persist
|
|
||||||
# self.inputs
|
|
||||||
field = 'inputs'
|
|
||||||
super(Credential, self).mark_field_for_save(update_fields, field)
|
|
||||||
|
|
||||||
def display_inputs(self):
|
def display_inputs(self):
|
||||||
field_val = self.inputs.copy()
|
field_val = self.inputs.copy()
|
||||||
for k, v in field_val.items():
|
for k, v in field_val.items():
|
||||||
@@ -429,7 +243,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
type_alias = self.credential_type.name
|
type_alias = self.credential_type.name
|
||||||
else:
|
else:
|
||||||
type_alias = self.credential_type_id
|
type_alias = self.credential_type_id
|
||||||
if self.kind == 'vault' and self.has_input('vault_id'):
|
if self.credential_type.kind == 'vault' and self.has_input('vault_id'):
|
||||||
if display:
|
if display:
|
||||||
fmt_str = '{} (id={})'
|
fmt_str = '{} (id={})'
|
||||||
else:
|
else:
|
||||||
@@ -456,7 +270,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
|||||||
:param field_name(str): The name of the input field.
|
:param field_name(str): The name of the input field.
|
||||||
:param default(optional[str]): A default return value to use.
|
:param default(optional[str]): A default return value to use.
|
||||||
"""
|
"""
|
||||||
if self.kind != 'external' and field_name in self.dynamic_input_fields:
|
if self.credential_type.kind != 'external' and field_name in self.dynamic_input_fields:
|
||||||
return self._get_dynamic_input(field_name)
|
return self._get_dynamic_input(field_name)
|
||||||
if field_name in self.credential_type.secret_fields:
|
if field_name in self.credential_type.secret_fields:
|
||||||
try:
|
try:
|
||||||
@@ -552,15 +366,8 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
def get_absolute_url(self, request=None):
|
def get_absolute_url(self, request=None):
|
||||||
# Page does not exist in API v1
|
|
||||||
if request.version == 'v1':
|
|
||||||
return reverse('api:credential_type_detail', kwargs={'pk': self.pk})
|
|
||||||
return reverse('api:credential_type_detail', kwargs={'pk': self.pk}, request=request)
|
return reverse('api:credential_type_detail', kwargs={'pk': self.pk}, request=request)
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_by_kind(self):
|
|
||||||
return self.kind != 'cloud'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def defined_fields(self):
|
def defined_fields(self):
|
||||||
return [field.get('id') for field in self.inputs.get('fields', [])]
|
return [field.get('id') for field in self.inputs.get('fields', [])]
|
||||||
@@ -629,29 +436,6 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
inputs=plugin.inputs
|
inputs=plugin.inputs
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_v1_kind(cls, kind, data={}):
|
|
||||||
match = None
|
|
||||||
kind = kind or 'ssh'
|
|
||||||
kind_choices = dict(V1Credential.KIND_CHOICES)
|
|
||||||
requirements = {}
|
|
||||||
if kind == 'ssh':
|
|
||||||
if data.get('vault_password'):
|
|
||||||
requirements['kind'] = 'vault'
|
|
||||||
else:
|
|
||||||
requirements['kind'] = 'ssh'
|
|
||||||
elif kind in ('net', 'scm', 'insights'):
|
|
||||||
requirements['kind'] = kind
|
|
||||||
elif kind in kind_choices:
|
|
||||||
requirements.update(dict(
|
|
||||||
kind='cloud',
|
|
||||||
name=kind_choices[kind]
|
|
||||||
))
|
|
||||||
if requirements:
|
|
||||||
requirements['managed_by_tower'] = True
|
|
||||||
match = cls.objects.filter(**requirements)[:1].get()
|
|
||||||
return match
|
|
||||||
|
|
||||||
def inject_credential(self, credential, env, safe_env, args, private_data_dir):
|
def inject_credential(self, credential, env, safe_env, args, private_data_dir):
|
||||||
"""
|
"""
|
||||||
Inject credential data into the environment variables and arguments
|
Inject credential data into the environment variables and arguments
|
||||||
@@ -678,9 +462,11 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
files)
|
files)
|
||||||
"""
|
"""
|
||||||
if not self.injectors:
|
if not self.injectors:
|
||||||
if self.managed_by_tower and credential.kind in dir(builtin_injectors):
|
if self.managed_by_tower and credential.credential_type.namespace in dir(builtin_injectors):
|
||||||
injected_env = {}
|
injected_env = {}
|
||||||
getattr(builtin_injectors, credential.kind)(credential, injected_env, private_data_dir)
|
getattr(builtin_injectors, credential.credential_type.namespace)(
|
||||||
|
credential, injected_env, private_data_dir
|
||||||
|
)
|
||||||
env.update(injected_env)
|
env.update(injected_env)
|
||||||
safe_env.update(build_safe_env(injected_env))
|
safe_env.update(build_safe_env(injected_env))
|
||||||
return
|
return
|
||||||
@@ -1335,12 +1121,12 @@ class CredentialInputSource(PrimordialModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def clean_target_credential(self):
|
def clean_target_credential(self):
|
||||||
if self.target_credential.kind == 'external':
|
if self.target_credential.credential_type.kind == 'external':
|
||||||
raise ValidationError(_('Target must be a non-external credential'))
|
raise ValidationError(_('Target must be a non-external credential'))
|
||||||
return self.target_credential
|
return self.target_credential
|
||||||
|
|
||||||
def clean_source_credential(self):
|
def clean_source_credential(self):
|
||||||
if self.source_credential.kind != 'external':
|
if self.source_credential.credential_type.kind != 'external':
|
||||||
raise ValidationError(_('Source must be an external credential'))
|
raise ValidationError(_('Source must be an external credential'))
|
||||||
return self.source_credential
|
return self.source_credential
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from django.db import models
|
|||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError, FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
|
|
||||||
# REST Framework
|
# REST Framework
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
@@ -152,21 +152,9 @@ class JobOptions(BaseModel):
|
|||||||
|
|
||||||
extra_vars_dict = VarsDictProperty('extra_vars', True)
|
extra_vars_dict = VarsDictProperty('extra_vars', True)
|
||||||
|
|
||||||
def clean_credential(self):
|
@property
|
||||||
cred = self.credential
|
def machine_credential(self):
|
||||||
if cred and cred.kind != 'ssh':
|
return self.credentials.filter(credential_type__kind='ssh').first()
|
||||||
raise ValidationError(
|
|
||||||
_('You must provide an SSH credential.'),
|
|
||||||
)
|
|
||||||
return cred
|
|
||||||
|
|
||||||
def clean_vault_credential(self):
|
|
||||||
cred = self.vault_credential
|
|
||||||
if cred and cred.kind != 'vault':
|
|
||||||
raise ValidationError(
|
|
||||||
_('You must provide a Vault credential.'),
|
|
||||||
)
|
|
||||||
return cred
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def network_credentials(self):
|
def network_credentials(self):
|
||||||
@@ -180,41 +168,6 @@ class JobOptions(BaseModel):
|
|||||||
def vault_credentials(self):
|
def vault_credentials(self):
|
||||||
return list(self.credentials.filter(credential_type__kind='vault'))
|
return list(self.credentials.filter(credential_type__kind='vault'))
|
||||||
|
|
||||||
@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):
|
|
||||||
for cred in self.credentials.all():
|
|
||||||
if cred.credential_type.kind == kind:
|
|
||||||
return cred
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
@property
|
|
||||||
def cloud_credential(self):
|
|
||||||
try:
|
|
||||||
return self.cloud_credentials[-1].pk
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
|
||||||
@property
|
|
||||||
def network_credential(self):
|
|
||||||
try:
|
|
||||||
return self.network_credentials[-1].pk
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def passwords_needed_to_start(self):
|
def passwords_needed_to_start(self):
|
||||||
'''Return list of password field names needed to start the job.'''
|
'''Return list of password field names needed to start the job.'''
|
||||||
@@ -707,7 +660,7 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
|||||||
data.update(dict(inventory=self.inventory.name if self.inventory else None,
|
data.update(dict(inventory=self.inventory.name if self.inventory else None,
|
||||||
project=self.project.name if self.project else None,
|
project=self.project.name if self.project else None,
|
||||||
playbook=self.playbook,
|
playbook=self.playbook,
|
||||||
credential=getattr(self.get_deprecated_credential('ssh'), 'name', None),
|
credential=getattr(self.machine_credential, 'name', None),
|
||||||
limit=self.limit,
|
limit=self.limit,
|
||||||
extra_vars=self.display_extra_vars(),
|
extra_vars=self.display_extra_vars(),
|
||||||
hosts=all_hosts))
|
hosts=all_hosts))
|
||||||
|
|||||||
@@ -794,7 +794,7 @@ class BaseTask(object):
|
|||||||
data += '\n'
|
data += '\n'
|
||||||
# For credentials used with ssh-add, write to a named pipe which
|
# For credentials used with ssh-add, write to a named pipe which
|
||||||
# will be read then closed, instead of leaving the SSH key on disk.
|
# will be read then closed, instead of leaving the SSH key on disk.
|
||||||
if credential and credential.kind in ('ssh', 'scm') and not ssh_too_old:
|
if credential and credential.credential_type.namespace in ('ssh', 'scm') and not ssh_too_old:
|
||||||
try:
|
try:
|
||||||
os.mkdir(os.path.join(private_data_dir, 'env'))
|
os.mkdir(os.path.join(private_data_dir, 'env'))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@@ -1324,7 +1324,7 @@ class RunJob(BaseTask):
|
|||||||
and ansible-vault.
|
and ansible-vault.
|
||||||
'''
|
'''
|
||||||
passwords = super(RunJob, self).build_passwords(job, runtime_passwords)
|
passwords = super(RunJob, self).build_passwords(job, runtime_passwords)
|
||||||
cred = job.get_deprecated_credential('ssh')
|
cred = job.machine_credential
|
||||||
if cred:
|
if cred:
|
||||||
for field in ('ssh_key_unlock', 'ssh_password', 'become_password', 'vault_password'):
|
for field in ('ssh_key_unlock', 'ssh_password', 'become_password', 'vault_password'):
|
||||||
value = runtime_passwords.get(field, cred.get_input('password' if field == 'ssh_password' else field, default=''))
|
value = runtime_passwords.get(field, cred.get_input('password' if field == 'ssh_password' else field, default=''))
|
||||||
@@ -1408,6 +1408,9 @@ class RunJob(BaseTask):
|
|||||||
|
|
||||||
# Set environment variables for cloud credentials.
|
# Set environment variables for cloud credentials.
|
||||||
cred_files = private_data_files.get('credentials', {})
|
cred_files = private_data_files.get('credentials', {})
|
||||||
|
for cloud_cred in job.cloud_credentials:
|
||||||
|
if cloud_cred and cloud_cred.credential_type.namespace == 'openstack':
|
||||||
|
env['OS_CLIENT_CONFIG_FILE'] = cred_files.get(cloud_cred, '')
|
||||||
|
|
||||||
for network_cred in job.network_credentials:
|
for network_cred in job.network_credentials:
|
||||||
env['ANSIBLE_NET_USERNAME'] = network_cred.get_input('username', default='')
|
env['ANSIBLE_NET_USERNAME'] = network_cred.get_input('username', default='')
|
||||||
@@ -1429,7 +1432,7 @@ class RunJob(BaseTask):
|
|||||||
Build command line argument list for running ansible-playbook,
|
Build command line argument list for running ansible-playbook,
|
||||||
optionally using ssh-agent for public/private key authentication.
|
optionally using ssh-agent for public/private key authentication.
|
||||||
'''
|
'''
|
||||||
creds = job.get_deprecated_credential('ssh')
|
creds = job.machine_credential
|
||||||
|
|
||||||
ssh_username, become_username, become_method = '', '', ''
|
ssh_username, become_username, become_method = '', '', ''
|
||||||
if creds:
|
if creds:
|
||||||
@@ -2228,9 +2231,9 @@ class RunAdHocCommand(BaseTask):
|
|||||||
creds = ad_hoc_command.credential
|
creds = ad_hoc_command.credential
|
||||||
ssh_username, become_username, become_method = '', '', ''
|
ssh_username, become_username, become_method = '', '', ''
|
||||||
if creds:
|
if creds:
|
||||||
ssh_username = creds.username
|
ssh_username = creds.get_input('username', default='')
|
||||||
become_method = creds.become_method
|
become_method = creds.get_input('become_method', default='')
|
||||||
become_username = creds.become_username
|
become_username = creds.get_input('become_username', default='')
|
||||||
else:
|
else:
|
||||||
become_method = None
|
become_method = None
|
||||||
become_username = ""
|
become_username = ""
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ def mk_job_template(name, job_type='run',
|
|||||||
if persisted and credential:
|
if persisted and credential:
|
||||||
jt.save()
|
jt.save()
|
||||||
jt.credentials.add(credential)
|
jt.credentials.add(credential)
|
||||||
if jt.credential is None:
|
if jt.machine_credential is None:
|
||||||
jt.ask_credential_on_launch = True
|
jt.ask_credential_on_launch = True
|
||||||
|
|
||||||
jt.project = project
|
jt.project = project
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import itertools
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from unittest import mock # noqa
|
from unittest import mock # noqa
|
||||||
@@ -27,197 +26,6 @@ def test_idempotent_credential_type_setup():
|
|||||||
assert CredentialType.objects.count() == total
|
assert CredentialType.objects.count() == total
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.parametrize('kind, total', [
|
|
||||||
('ssh', 1), ('net', 0)
|
|
||||||
])
|
|
||||||
def test_filter_by_v1_kind(get, admin, organization, kind, total):
|
|
||||||
CredentialType.setup_tower_managed_defaults()
|
|
||||||
cred = Credential(
|
|
||||||
credential_type=CredentialType.from_v1_kind('ssh'),
|
|
||||||
name='Best credential ever',
|
|
||||||
organization=organization,
|
|
||||||
inputs={
|
|
||||||
'username': u'jim',
|
|
||||||
'password': u'secret'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cred.save()
|
|
||||||
|
|
||||||
response = get(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
admin,
|
|
||||||
QUERY_STRING='kind=%s' % kind
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data['count'] == total
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_filter_by_v1_kind_with_vault(get, admin, organization):
|
|
||||||
CredentialType.setup_tower_managed_defaults()
|
|
||||||
cred = Credential(
|
|
||||||
credential_type=CredentialType.objects.get(kind='ssh'),
|
|
||||||
name='Best credential ever',
|
|
||||||
organization=organization,
|
|
||||||
inputs={
|
|
||||||
'username': u'jim',
|
|
||||||
'password': u'secret'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cred.save()
|
|
||||||
cred = Credential(
|
|
||||||
credential_type=CredentialType.objects.get(kind='vault'),
|
|
||||||
name='Best credential ever',
|
|
||||||
organization=organization,
|
|
||||||
inputs={
|
|
||||||
'vault_password': u'vault!'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cred.save()
|
|
||||||
|
|
||||||
response = get(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
admin,
|
|
||||||
QUERY_STRING='kind=ssh'
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data['count'] == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_insights_credentials_in_v1_api_list(get, admin, organization):
|
|
||||||
credential_type = CredentialType.defaults['insights']()
|
|
||||||
credential_type.save()
|
|
||||||
cred = Credential(
|
|
||||||
credential_type=credential_type,
|
|
||||||
name='Best credential ever',
|
|
||||||
organization=organization,
|
|
||||||
inputs={
|
|
||||||
'username': u'joe',
|
|
||||||
'password': u'secret'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cred.save()
|
|
||||||
|
|
||||||
response = get(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
admin
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data['count'] == 1
|
|
||||||
cred = response.data['results'][0]
|
|
||||||
assert cred['kind'] == 'insights'
|
|
||||||
assert cred['username'] == 'joe'
|
|
||||||
assert cred['password'] == '$encrypted$'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_create_insights_credentials_in_v1(get, post, admin, organization):
|
|
||||||
credential_type = CredentialType.defaults['insights']()
|
|
||||||
credential_type.save()
|
|
||||||
|
|
||||||
response = post(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
{
|
|
||||||
'name': 'Best Credential Ever',
|
|
||||||
'organization': organization.id,
|
|
||||||
'kind': 'insights',
|
|
||||||
'username': 'joe',
|
|
||||||
'password': 'secret'
|
|
||||||
},
|
|
||||||
admin
|
|
||||||
)
|
|
||||||
assert response.status_code == 201
|
|
||||||
cred = Credential.objects.get(pk=response.data['id'])
|
|
||||||
assert cred.username == 'joe'
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.credential_type == credential_type
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_custom_credentials_not_in_v1_api_list(get, admin, organization):
|
|
||||||
"""
|
|
||||||
'Custom' credentials (those not managed by Tower) shouldn't be visible from
|
|
||||||
the V1 credentials API list
|
|
||||||
"""
|
|
||||||
credential_type = CredentialType(
|
|
||||||
kind='cloud',
|
|
||||||
name='MyCloud',
|
|
||||||
inputs = {
|
|
||||||
'fields': [{
|
|
||||||
'id': 'password',
|
|
||||||
'label': 'Password',
|
|
||||||
'type': 'string',
|
|
||||||
'secret': True
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
credential_type.save()
|
|
||||||
cred = Credential(
|
|
||||||
credential_type=credential_type,
|
|
||||||
name='Best credential ever',
|
|
||||||
organization=organization,
|
|
||||||
inputs={
|
|
||||||
'password': u'secret'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cred.save()
|
|
||||||
|
|
||||||
response = get(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
admin
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data['count'] == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_custom_credentials_not_in_v1_api_detail(get, admin, organization):
|
|
||||||
"""
|
|
||||||
'Custom' credentials (those not managed by Tower) shouldn't be visible from
|
|
||||||
the V1 credentials API detail
|
|
||||||
"""
|
|
||||||
credential_type = CredentialType(
|
|
||||||
kind='cloud',
|
|
||||||
name='MyCloud',
|
|
||||||
inputs = {
|
|
||||||
'fields': [{
|
|
||||||
'id': 'password',
|
|
||||||
'label': 'Password',
|
|
||||||
'type': 'string',
|
|
||||||
'secret': True
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
credential_type.save()
|
|
||||||
cred = Credential(
|
|
||||||
credential_type=credential_type,
|
|
||||||
name='Best credential ever',
|
|
||||||
organization=organization,
|
|
||||||
inputs={
|
|
||||||
'password': u'secret'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cred.save()
|
|
||||||
|
|
||||||
response = get(
|
|
||||||
reverse('api:credential_detail', kwargs={'version': 'v1', 'pk': cred.pk}),
|
|
||||||
admin
|
|
||||||
)
|
|
||||||
assert response.status_code == 404
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_filter_by_v1_invalid_kind(get, admin, organization):
|
|
||||||
response = get(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
admin,
|
|
||||||
QUERY_STRING='kind=bad_kind'
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# user credential creation
|
# user credential creation
|
||||||
#
|
#
|
||||||
@@ -225,7 +33,6 @@ def test_filter_by_v1_invalid_kind(get, admin, organization):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_user_credential_via_credentials_list(post, get, alice, credentialtype_ssh, version, params):
|
def test_create_user_credential_via_credentials_list(post, get, alice, credentialtype_ssh, version, params):
|
||||||
@@ -245,7 +52,6 @@ def test_create_user_credential_via_credentials_list(post, get, alice, credentia
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_credential_validation_error_with_bad_user(post, admin, version, credentialtype_ssh, params):
|
def test_credential_validation_error_with_bad_user(post, admin, version, credentialtype_ssh, params):
|
||||||
@@ -262,7 +68,6 @@ def test_credential_validation_error_with_bad_user(post, admin, version, credent
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_user_credential_via_user_credentials_list(post, get, alice, credentialtype_ssh, version, params):
|
def test_create_user_credential_via_user_credentials_list(post, get, alice, credentialtype_ssh, version, params):
|
||||||
@@ -282,7 +87,6 @@ def test_create_user_credential_via_user_credentials_list(post, get, alice, cred
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, version, params):
|
def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, version, params):
|
||||||
@@ -298,7 +102,6 @@ def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, ver
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob, version, params):
|
def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob, version, params):
|
||||||
@@ -319,7 +122,6 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_team_credential(post, get, team, organization, org_admin, team_member, credentialtype_ssh, version, params):
|
def test_create_team_credential(post, get, team, organization, org_admin, team_member, credentialtype_ssh, version, params):
|
||||||
@@ -345,7 +147,6 @@ def test_create_team_credential(post, get, team, organization, org_admin, team_m
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member, credentialtype_ssh, version, params):
|
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member, credentialtype_ssh, version, params):
|
||||||
@@ -368,7 +169,6 @@ def test_create_team_credential_via_team_credentials_list(post, get, team, org_a
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_team_credential_by_urelated_user_xfail(post, team, organization, alice, team_member, version, params):
|
def test_create_team_credential_by_urelated_user_xfail(post, team, organization, alice, team_member, version, params):
|
||||||
@@ -385,7 +185,6 @@ def test_create_team_credential_by_urelated_user_xfail(post, team, organization,
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_team_credential_by_team_member_xfail(post, team, organization, alice, team_member, version, params):
|
def test_create_team_credential_by_team_member_xfail(post, team, organization, alice, team_member, version, params):
|
||||||
@@ -407,7 +206,7 @@ def test_create_team_credential_by_team_member_xfail(post, team, organization, a
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_org_credential_to_org_user_through_role_users(post, credential, organization, org_admin, org_member, version):
|
def test_grant_org_credential_to_org_user_through_role_users(post, credential, organization, org_admin, org_member, version):
|
||||||
credential.organization = organization
|
credential.organization = organization
|
||||||
credential.save()
|
credential.save()
|
||||||
@@ -418,7 +217,7 @@ def test_grant_org_credential_to_org_user_through_role_users(post, credential, o
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_org_credential_to_org_user_through_user_roles(post, credential, organization, org_admin, org_member, version):
|
def test_grant_org_credential_to_org_user_through_user_roles(post, credential, organization, org_admin, org_member, version):
|
||||||
credential.organization = organization
|
credential.organization = organization
|
||||||
credential.save()
|
credential.save()
|
||||||
@@ -429,7 +228,7 @@ def test_grant_org_credential_to_org_user_through_user_roles(post, credential, o
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_org_credential_to_non_org_user_through_role_users(post, credential, organization, org_admin, alice, version):
|
def test_grant_org_credential_to_non_org_user_through_role_users(post, credential, organization, org_admin, alice, version):
|
||||||
credential.organization = organization
|
credential.organization = organization
|
||||||
credential.save()
|
credential.save()
|
||||||
@@ -440,7 +239,7 @@ def test_grant_org_credential_to_non_org_user_through_role_users(post, credentia
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_org_credential_to_non_org_user_through_user_roles(post, credential, organization, org_admin, alice, version):
|
def test_grant_org_credential_to_non_org_user_through_user_roles(post, credential, organization, org_admin, alice, version):
|
||||||
credential.organization = organization
|
credential.organization = organization
|
||||||
credential.save()
|
credential.save()
|
||||||
@@ -451,7 +250,7 @@ def test_grant_org_credential_to_non_org_user_through_user_roles(post, credentia
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob, version):
|
def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob, version):
|
||||||
# normal users can't do this
|
# normal users can't do this
|
||||||
credential.admin_role.members.add(alice)
|
credential.admin_role.members.add(alice)
|
||||||
@@ -462,7 +261,7 @@ def test_grant_private_credential_to_user_through_role_users(post, credential, a
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_private_credential_to_org_user_through_role_users(post, credential, org_admin, org_member, version):
|
def test_grant_private_credential_to_org_user_through_role_users(post, credential, org_admin, org_member, version):
|
||||||
# org admins can't either
|
# org admins can't either
|
||||||
credential.admin_role.members.add(org_admin)
|
credential.admin_role.members.add(org_admin)
|
||||||
@@ -473,7 +272,7 @@ def test_grant_private_credential_to_org_user_through_role_users(post, credentia
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_sa_grant_private_credential_to_user_through_role_users(post, credential, admin, bob, version):
|
def test_sa_grant_private_credential_to_user_through_role_users(post, credential, admin, bob, version):
|
||||||
# but system admins can
|
# but system admins can
|
||||||
response = post(reverse('api:role_users_list', kwargs={'version': version, 'pk': credential.use_role.id}), {
|
response = post(reverse('api:role_users_list', kwargs={'version': version, 'pk': credential.use_role.id}), {
|
||||||
@@ -483,7 +282,7 @@ def test_sa_grant_private_credential_to_user_through_role_users(post, credential
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob, version):
|
def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob, version):
|
||||||
# normal users can't do this
|
# normal users can't do this
|
||||||
credential.admin_role.members.add(alice)
|
credential.admin_role.members.add(alice)
|
||||||
@@ -494,7 +293,7 @@ def test_grant_private_credential_to_user_through_user_roles(post, credential, a
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_private_credential_to_org_user_through_user_roles(post, credential, org_admin, org_member, version):
|
def test_grant_private_credential_to_org_user_through_user_roles(post, credential, org_admin, org_member, version):
|
||||||
# org admins can't either
|
# org admins can't either
|
||||||
credential.admin_role.members.add(org_admin)
|
credential.admin_role.members.add(org_admin)
|
||||||
@@ -505,7 +304,7 @@ def test_grant_private_credential_to_org_user_through_user_roles(post, credentia
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_sa_grant_private_credential_to_user_through_user_roles(post, credential, admin, bob, version):
|
def test_sa_grant_private_credential_to_user_through_user_roles(post, credential, admin, bob, version):
|
||||||
# but system admins can
|
# but system admins can
|
||||||
response = post(reverse('api:user_roles_list', kwargs={'version': version, 'pk': bob.id}), {
|
response = post(reverse('api:user_roles_list', kwargs={'version': version, 'pk': bob.id}), {
|
||||||
@@ -515,7 +314,7 @@ def test_sa_grant_private_credential_to_user_through_user_roles(post, credential
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_org_credential_to_team_through_role_teams(post, credential, organization, org_admin, org_auditor, team, version):
|
def test_grant_org_credential_to_team_through_role_teams(post, credential, organization, org_admin, org_auditor, team, version):
|
||||||
assert org_auditor not in credential.read_role
|
assert org_auditor not in credential.read_role
|
||||||
credential.organization = organization
|
credential.organization = organization
|
||||||
@@ -528,7 +327,7 @@ def test_grant_org_credential_to_team_through_role_teams(post, credential, organ
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_grant_org_credential_to_team_through_team_roles(post, credential, organization, org_admin, org_auditor, team, version):
|
def test_grant_org_credential_to_team_through_team_roles(post, credential, organization, org_admin, org_auditor, team, version):
|
||||||
assert org_auditor not in credential.read_role
|
assert org_auditor not in credential.read_role
|
||||||
credential.organization = organization
|
credential.organization = organization
|
||||||
@@ -541,7 +340,7 @@ def test_grant_org_credential_to_team_through_team_roles(post, credential, organ
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_sa_grant_private_credential_to_team_through_role_teams(post, credential, admin, team, version):
|
def test_sa_grant_private_credential_to_team_through_role_teams(post, credential, admin, team, version):
|
||||||
# not even a system admin can grant a private cred to a team though
|
# not even a system admin can grant a private cred to a team though
|
||||||
response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': credential.use_role.id}), {
|
response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': credential.use_role.id}), {
|
||||||
@@ -551,7 +350,7 @@ def test_sa_grant_private_credential_to_team_through_role_teams(post, credential
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version', ['v1', 'v2'])
|
@pytest.mark.parametrize('version', ['v2'])
|
||||||
def test_sa_grant_private_credential_to_team_through_team_roles(post, credential, admin, team, version):
|
def test_sa_grant_private_credential_to_team_through_team_roles(post, credential, admin, team, version):
|
||||||
# not even a system admin can grant a private cred to a team though
|
# not even a system admin can grant a private cred to a team though
|
||||||
response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': team.id}), {
|
response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': team.id}), {
|
||||||
@@ -567,7 +366,6 @@ def test_sa_grant_private_credential_to_team_through_team_roles(post, credential
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_org_credential_as_not_admin(post, organization, org_member, credentialtype_ssh, version, params):
|
def test_create_org_credential_as_not_admin(post, organization, org_member, credentialtype_ssh, version, params):
|
||||||
@@ -583,7 +381,6 @@ def test_create_org_credential_as_not_admin(post, organization, org_member, cred
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_create_org_credential_as_admin(post, organization, org_admin, credentialtype_ssh, version, params):
|
def test_create_org_credential_as_admin(post, organization, org_admin, credentialtype_ssh, version, params):
|
||||||
@@ -599,7 +396,6 @@ def test_create_org_credential_as_admin(post, organization, org_admin, credentia
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_credential_detail(post, get, organization, org_admin, credentialtype_ssh, version, params):
|
def test_credential_detail(post, get, organization, org_admin, credentialtype_ssh, version, params):
|
||||||
@@ -624,7 +420,6 @@ def test_credential_detail(post, get, organization, org_admin, credentialtype_ss
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'username': 'someusername'}],
|
|
||||||
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
def test_list_created_org_credentials(post, get, organization, org_admin, org_member, credentialtype_ssh, version, params):
|
def test_list_created_org_credentials(post, get, organization, org_admin, org_member, credentialtype_ssh, version, params):
|
||||||
@@ -667,12 +462,11 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('order_by', ('password', '-password', 'password,pk', '-password,pk'))
|
@pytest.mark.parametrize('order_by', ('password', '-password', 'password,pk', '-password,pk'))
|
||||||
@pytest.mark.parametrize('version', ('v1', 'v2'))
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by, version):
|
def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by):
|
||||||
for i, password in enumerate(('abc', 'def', 'xyz')):
|
for i, password in enumerate(('abc', 'def', 'xyz')):
|
||||||
response = post(
|
response = post(
|
||||||
reverse('api:credential_list', kwargs={'version': version}),
|
reverse('api:credential_list', kwargs={'version': 'v2'}),
|
||||||
{
|
{
|
||||||
'organization': organization.id,
|
'organization': organization.id,
|
||||||
'name': 'C%d' % i,
|
'name': 'C%d' % i,
|
||||||
@@ -682,7 +476,7 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = get(
|
response = get(
|
||||||
reverse('api:credential_list', kwargs={'version': version}),
|
reverse('api:credential_list', kwargs={'version': 'v2'}),
|
||||||
org_admin,
|
org_admin,
|
||||||
QUERY_STRING='order_by=%s' % order_by,
|
QUERY_STRING='order_by=%s' % order_by,
|
||||||
status=400
|
status=400
|
||||||
@@ -690,22 +484,6 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin
|
|||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_v1_credential_kind_validity(get, post, organization, admin, credentialtype_ssh):
|
|
||||||
params = {
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'organization': organization.id,
|
|
||||||
'kind': 'nonsense'
|
|
||||||
}
|
|
||||||
response = post(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
params,
|
|
||||||
admin
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.data['kind'] == ['"nonsense" is not a valid choice']
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, credentialtype_ssh):
|
def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, credentialtype_ssh):
|
||||||
params = {
|
params = {
|
||||||
@@ -725,34 +503,6 @@ def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, cred
|
|||||||
assert "'invalid_field' was unexpected" in response.data['inputs'][0]
|
assert "'invalid_field' was unexpected" in response.data['inputs'][0]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.parametrize('field_name, field_value', itertools.product(
|
|
||||||
['username', 'password', 'ssh_key_data', 'become_method', 'become_username', 'become_password'], # noqa
|
|
||||||
['', None]
|
|
||||||
))
|
|
||||||
def test_nullish_field_data(get, post, organization, admin, field_name, field_value):
|
|
||||||
ssh = CredentialType.defaults['ssh']()
|
|
||||||
ssh.save()
|
|
||||||
params = {
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'credential_type': ssh.pk,
|
|
||||||
'organization': organization.id,
|
|
||||||
'inputs': {
|
|
||||||
field_name: field_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response = post(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v2'}),
|
|
||||||
params,
|
|
||||||
admin
|
|
||||||
)
|
|
||||||
assert response.status_code == 201
|
|
||||||
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
cred = Credential.objects.all()[:1].get()
|
|
||||||
assert getattr(cred, field_name) == ''
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('field_value', ['', None, False])
|
@pytest.mark.parametrize('field_value', ['', None, False])
|
||||||
def test_falsey_field_data(get, post, organization, admin, field_value):
|
def test_falsey_field_data(get, post, organization, admin, field_value):
|
||||||
@@ -776,7 +526,7 @@ def test_falsey_field_data(get, post, organization, admin, field_value):
|
|||||||
|
|
||||||
assert Credential.objects.count() == 1
|
assert Credential.objects.count() == 1
|
||||||
cred = Credential.objects.all()[:1].get()
|
cred = Credential.objects.all()[:1].get()
|
||||||
assert cred.authorize is False
|
assert cred.inputs['authorize'] is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -811,14 +561,6 @@ def test_field_dependencies(get, post, organization, admin, kind, extraneous):
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'scm',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'username': 'some_username',
|
|
||||||
'password': 'some_password',
|
|
||||||
'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY,
|
|
||||||
'ssh_key_unlock': 'some_key_unlock',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -851,12 +593,6 @@ def test_scm_create_ok(post, organization, admin, version, params):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'ssh',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'password': 'secret',
|
|
||||||
'vault_password': '',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -882,38 +618,11 @@ def test_ssh_create_ok(post, organization, admin, version, params):
|
|||||||
assert decrypt_field(cred, 'password') == 'secret'
|
assert decrypt_field(cred, 'password') == 'secret'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_v1_ssh_vault_ambiguity(post, organization, admin):
|
|
||||||
vault = CredentialType.defaults['vault']()
|
|
||||||
vault.save()
|
|
||||||
params = {
|
|
||||||
'organization': organization.id,
|
|
||||||
'kind': 'ssh',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'username': 'joe',
|
|
||||||
'password': 'secret',
|
|
||||||
'ssh_key_data': 'some_key_data',
|
|
||||||
'ssh_key_unlock': 'some_key_unlock',
|
|
||||||
'vault_password': 'vault_password',
|
|
||||||
}
|
|
||||||
response = post(
|
|
||||||
reverse('api:credential_list', kwargs={'version': 'v1'}),
|
|
||||||
params,
|
|
||||||
admin
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Vault Credentials
|
# Vault Credentials
|
||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'ssh',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'vault_password': 'some_password',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -968,16 +677,6 @@ def test_vault_password_required(post, organization, admin):
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'net',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'username': 'some_username',
|
|
||||||
'password': 'some_password',
|
|
||||||
'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY,
|
|
||||||
'ssh_key_unlock': 'some_key_unlock',
|
|
||||||
'authorize': True,
|
|
||||||
'authorize_password': 'some_authorize_password',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1017,13 +716,6 @@ def test_net_create_ok(post, organization, admin, version, params):
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'cloudforms',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'host': 'some_host',
|
|
||||||
'username': 'some_username',
|
|
||||||
'password': 'some_password',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1057,13 +749,6 @@ def test_cloudforms_create_ok(post, organization, admin, version, params):
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'gce',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'username': 'some_username',
|
|
||||||
'project': 'some_project',
|
|
||||||
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1097,16 +782,6 @@ def test_gce_create_ok(post, organization, admin, version, params):
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'azure_rm',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'subscription': 'some_subscription',
|
|
||||||
'username': 'some_username',
|
|
||||||
'password': 'some_password',
|
|
||||||
'client': 'some_client',
|
|
||||||
'secret': 'some_secret',
|
|
||||||
'tenant': 'some_tenant'
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1146,13 +821,6 @@ def test_azure_rm_create_ok(post, organization, admin, version, params):
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'satellite6',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'host': 'some_host',
|
|
||||||
'username': 'some_username',
|
|
||||||
'password': 'some_password',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1186,13 +854,6 @@ def test_satellite6_create_ok(post, organization, admin, version, params):
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'aws',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'username': 'some_username',
|
|
||||||
'password': 'some_password',
|
|
||||||
'security_token': 'abc123'
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1223,10 +884,6 @@ def test_aws_create_ok(post, organization, admin, version, params):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'aws',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1258,13 +915,6 @@ def test_aws_create_fail_required_fields(post, organization, admin, version, par
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'vmware',
|
|
||||||
'host': 'some_host',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'username': 'some_username',
|
|
||||||
'password': 'some_password'
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1295,10 +945,6 @@ def test_vmware_create_ok(post, organization, admin, version, params):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'vmware',
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
@@ -1330,12 +976,6 @@ def test_vmware_create_fail_required_fields(post, organization, admin, version,
|
|||||||
#
|
#
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'username': 'some_user',
|
|
||||||
'password': 'some_password',
|
|
||||||
'project': 'some_project',
|
|
||||||
'host': 'some_host',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'inputs': {
|
'inputs': {
|
||||||
@@ -1396,7 +1036,6 @@ def test_openstack_verify_ssl(get, post, organization, admin, verify_ssl, expect
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'inputs': {}
|
'inputs': {}
|
||||||
@@ -1425,12 +1064,6 @@ def test_openstack_create_fail_required_fields(post, organization, admin, versio
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'kind': 'ssh',
|
|
||||||
'username': 'joe',
|
|
||||||
'password': '',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
@@ -1624,12 +1257,6 @@ def test_cloud_credential_type_mutability(patch, organization, admin, credential
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'kind': 'ssh',
|
|
||||||
'username': 'joe',
|
|
||||||
'ssh_key_data': '$encrypted$',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
@@ -1664,13 +1291,6 @@ def test_ssh_unlock_needed(put, organization, admin, credentialtype_ssh, version
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'kind': 'ssh',
|
|
||||||
'username': 'joe',
|
|
||||||
'ssh_key_data': '$encrypted$',
|
|
||||||
'ssh_key_unlock': 'superfluous-key-unlock',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
@@ -1705,13 +1325,6 @@ def test_ssh_unlock_not_needed(put, organization, admin, credentialtype_ssh, ver
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'name': 'Best credential ever',
|
|
||||||
'kind': 'ssh',
|
|
||||||
'username': 'joe',
|
|
||||||
'ssh_key_data': '$encrypted$',
|
|
||||||
'ssh_key_unlock': 'new-unlock',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'name': 'Best credential ever',
|
'name': 'Best credential ever',
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
@@ -1753,11 +1366,6 @@ def test_ssh_unlock_with_prior_value(put, organization, admin, credentialtype_ss
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'kind': 'ssh',
|
|
||||||
'username': 'joe',
|
|
||||||
'password': 'secret',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'credential_type': 1,
|
'credential_type': 1,
|
||||||
'inputs': {
|
'inputs': {
|
||||||
@@ -1783,12 +1391,8 @@ def test_secret_encryption_on_create(get, post, organization, admin, credentialt
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data['count'] == 1
|
assert response.data['count'] == 1
|
||||||
cred = response.data['results'][0]
|
cred = response.data['results'][0]
|
||||||
if version == 'v1':
|
assert cred['inputs']['username'] == 'joe'
|
||||||
assert cred['username'] == 'joe'
|
assert cred['inputs']['password'] == '$encrypted$'
|
||||||
assert cred['password'] == '$encrypted$'
|
|
||||||
elif version == 'v2':
|
|
||||||
assert cred['inputs']['username'] == 'joe'
|
|
||||||
assert cred['inputs']['password'] == '$encrypted$'
|
|
||||||
|
|
||||||
cred = Credential.objects.all()[:1].get()
|
cred = Credential.objects.all()[:1].get()
|
||||||
assert cred.inputs['password'].startswith('$encrypted$UTF8$AES')
|
assert cred.inputs['password'].startswith('$encrypted$UTF8$AES')
|
||||||
@@ -1797,7 +1401,6 @@ def test_secret_encryption_on_create(get, post, organization, admin, credentialt
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'password': 'secret'}],
|
|
||||||
['v2', {'inputs': {'username': 'joe', 'password': 'secret'}}]
|
['v2', {'inputs': {'username': 'joe', 'password': 'secret'}}]
|
||||||
])
|
])
|
||||||
def test_secret_encryption_on_update(get, post, patch, organization, admin, credentialtype_ssh, version, params):
|
def test_secret_encryption_on_update(get, post, patch, organization, admin, credentialtype_ssh, version, params):
|
||||||
@@ -1829,12 +1432,8 @@ def test_secret_encryption_on_update(get, post, patch, organization, admin, cred
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data['count'] == 1
|
assert response.data['count'] == 1
|
||||||
cred = response.data['results'][0]
|
cred = response.data['results'][0]
|
||||||
if version == 'v1':
|
assert cred['inputs']['username'] == 'joe'
|
||||||
assert cred['username'] == 'joe'
|
assert cred['inputs']['password'] == '$encrypted$'
|
||||||
assert cred['password'] == '$encrypted$'
|
|
||||||
elif version == 'v2':
|
|
||||||
assert cred['inputs']['username'] == 'joe'
|
|
||||||
assert cred['inputs']['password'] == '$encrypted$'
|
|
||||||
|
|
||||||
cred = Credential.objects.all()[:1].get()
|
cred = Credential.objects.all()[:1].get()
|
||||||
assert cred.inputs['password'].startswith('$encrypted$UTF8$AES')
|
assert cred.inputs['password'].startswith('$encrypted$UTF8$AES')
|
||||||
@@ -1843,10 +1442,6 @@ def test_secret_encryption_on_update(get, post, patch, organization, admin, cred
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {
|
|
||||||
'username': 'joe',
|
|
||||||
'password': '$encrypted$',
|
|
||||||
}],
|
|
||||||
['v2', {
|
['v2', {
|
||||||
'inputs': {
|
'inputs': {
|
||||||
'username': 'joe',
|
'username': 'joe',
|
||||||
@@ -1930,7 +1525,6 @@ def test_custom_credential_type_create(get, post, organization, admin):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('version, params', [
|
@pytest.mark.parametrize('version, params', [
|
||||||
['v1', {'name': 'Some name', 'username': 'someusername'}],
|
|
||||||
['v2', {'name': 'Some name', 'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
['v2', {'name': 'Some name', 'credential_type': 1, 'inputs': {'username': 'someusername'}}]
|
||||||
])
|
])
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -25,74 +24,6 @@ def job_template(job_template, project, inventory):
|
|||||||
return job_template
|
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
|
|
||||||
@pytest.mark.parametrize('key', ('credential', 'vault_credential', 'cloud_credential', 'network_credential'))
|
|
||||||
def test_invalid_credential_update(get, patch, job_template, admin, key):
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk, 'version': 'v1'})
|
|
||||||
resp = patch(url, {key: 999999}, admin, expect=400)
|
|
||||||
assert 'Credential 999999 does not exist' in json.loads(smart_str(smart_str(resp.content)))[key]
|
|
||||||
|
|
||||||
|
|
||||||
@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 smart_str(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 smart_str(resp.content)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_extra_credentials_filtering(get, job_template, admin,
|
def test_extra_credentials_filtering(get, job_template, admin,
|
||||||
machine_credential, vault_credential, credential):
|
machine_credential, vault_credential, credential):
|
||||||
@@ -209,24 +140,6 @@ def test_extra_credentials_unique_by_kind(get, post, job_template, admin,
|
|||||||
assert 'Cannot assign multiple Amazon Web Services credentials.' in smart_str(resp.content)
|
assert 'Cannot assign multiple Amazon Web Services credentials.' in smart_str(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
|
@pytest.mark.django_db
|
||||||
def test_extra_credentials_at_launch(get, post, job_template, admin, credential):
|
def test_extra_credentials_at_launch(get, post, job_template, admin, credential):
|
||||||
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
@@ -236,30 +149,6 @@ def test_extra_credentials_at_launch(get, post, job_template, admin, credential)
|
|||||||
assert len(summary_fields['credentials']) == 1
|
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)
|
|
||||||
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)
|
|
||||||
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
|
@pytest.mark.django_db
|
||||||
def test_modify_extra_credentials_at_launch(get, post, job_template, admin,
|
def test_modify_extra_credentials_at_launch(get, post, job_template, admin,
|
||||||
machine_credential, vault_credential, credential):
|
machine_credential, vault_credential, credential):
|
||||||
@@ -272,22 +161,6 @@ def test_modify_extra_credentials_at_launch(get, post, job_template, admin,
|
|||||||
assert len(summary_fields['credentials']) == 3
|
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)
|
|
||||||
|
|
||||||
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
|
@pytest.mark.django_db
|
||||||
def test_ssh_password_prompted_at_launch(get, post, job_template, admin, machine_credential):
|
def test_ssh_password_prompted_at_launch(get, post, job_template, admin, machine_credential):
|
||||||
job_template.credentials.add(machine_credential)
|
job_template.credentials.add(machine_credential)
|
||||||
@@ -375,49 +248,6 @@ def test_invalid_mixed_credentials_specification(get, post, job_template, admin,
|
|||||||
user=admin, expect=400)
|
user=admin, expect=400)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_rbac_default_credential_usage(get, post, job_template, alice, machine_credential):
|
|
||||||
job_template.credentials.add(machine_credential)
|
|
||||||
job_template.execute_role.members.add(alice)
|
|
||||||
|
|
||||||
# alice can launch; she's not adding any _new_ credentials, and she has
|
|
||||||
# execute access to the JT
|
|
||||||
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
|
||||||
post(url, {'credential': machine_credential.pk}, alice, expect=201)
|
|
||||||
|
|
||||||
# make (copy) a _new_ SSH cred
|
|
||||||
new_cred = Credential.objects.create(
|
|
||||||
name=machine_credential.name,
|
|
||||||
credential_type=machine_credential.credential_type,
|
|
||||||
inputs=machine_credential.inputs
|
|
||||||
)
|
|
||||||
|
|
||||||
# alice is attempting to launch with a *different* SSH cred, but
|
|
||||||
# she does not have access to it, so she cannot launch
|
|
||||||
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
|
||||||
post(url, {'credential': new_cred.pk}, alice, expect=403)
|
|
||||||
|
|
||||||
# if alice has gains access to the credential, she *can* launch
|
|
||||||
new_cred.use_role.members.add(alice)
|
|
||||||
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
|
||||||
post(url, {'credential': new_cred.pk}, alice, expect=201)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_inventory_source_deprecated_credential(get, patch, admin, ec2_source, credential):
|
|
||||||
url = reverse('api:inventory_source_detail', kwargs={'pk': ec2_source.pk})
|
|
||||||
patch(url, {'credential': credential.pk}, admin, expect=200)
|
|
||||||
resp = get(url, admin, expect=200)
|
|
||||||
assert json.loads(smart_str(resp.content))['credential'] == credential.pk
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_inventory_source_invalid_deprecated_credential(patch, admin, ec2_source, credential):
|
|
||||||
url = reverse('api:inventory_source_detail', kwargs={'pk': ec2_source.pk})
|
|
||||||
resp = patch(url, {'credential': 999999}, admin, expect=400)
|
|
||||||
assert 'Credential 999999 does not exist' in smart_str(resp.content)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_deprecated_credential_activity_stream(patch, admin_user, machine_credential, job_template):
|
def test_deprecated_credential_activity_stream(patch, admin_user, machine_credential, job_template):
|
||||||
job_template.credentials.add(machine_credential)
|
job_template.credentials.add(machine_credential)
|
||||||
|
|||||||
@@ -309,8 +309,8 @@ def test_job_launch_with_default_creds(machine_credential, vault_credential, dep
|
|||||||
|
|
||||||
prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
||||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||||
assert job_obj.credential == machine_credential.pk
|
assert job_obj.machine_credential.pk == machine_credential.pk
|
||||||
assert job_obj.vault_credential == vault_credential.pk
|
assert job_obj.vault_credentials[0].pk == vault_credential.pk
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -350,14 +350,14 @@ def test_job_launch_with_empty_creds(machine_credential, vault_credential, deplo
|
|||||||
|
|
||||||
prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**serializer.validated_data)
|
prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**serializer.validated_data)
|
||||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||||
assert job_obj.credential is deploy_jobtemplate.credential
|
assert job_obj.machine_credential.pk == deploy_jobtemplate.machine_credential.pk
|
||||||
assert job_obj.vault_credential is deploy_jobtemplate.vault_credential
|
assert job_obj.vault_credentials[0].pk == deploy_jobtemplate.vault_credentials[0].pk
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_credential,
|
def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_credential,
|
||||||
deploy_jobtemplate, post, rando):
|
deploy_jobtemplate, post, rando):
|
||||||
vault_credential.vault_password = 'ASK'
|
vault_credential.inputs['vault_password'] = 'ASK'
|
||||||
vault_credential.save()
|
vault_credential.save()
|
||||||
deploy_jobtemplate.credentials.add(vault_credential)
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
deploy_jobtemplate.execute_role.members.add(rando)
|
deploy_jobtemplate.execute_role.members.add(rando)
|
||||||
@@ -440,7 +440,7 @@ def test_job_launch_fails_with_missing_multivault_password(machine_credential, v
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_jobtemplate, post,
|
def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_jobtemplate, post,
|
||||||
rando):
|
rando):
|
||||||
machine_credential.password = 'ASK'
|
machine_credential.inputs['password'] = 'ASK'
|
||||||
machine_credential.save()
|
machine_credential.save()
|
||||||
deploy_jobtemplate.credentials.add(machine_credential)
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
deploy_jobtemplate.execute_role.members.add(rando)
|
deploy_jobtemplate.execute_role.members.add(rando)
|
||||||
@@ -457,9 +457,9 @@ def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_j
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential, vault_credential,
|
def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential, vault_credential,
|
||||||
deploy_jobtemplate, post, rando):
|
deploy_jobtemplate, post, rando):
|
||||||
vault_credential.vault_password = 'ASK'
|
vault_credential.inputs['vault_password'] = 'ASK'
|
||||||
vault_credential.save()
|
vault_credential.save()
|
||||||
machine_credential.password = 'ASK'
|
machine_credential.inputs['password'] = 'ASK'
|
||||||
machine_credential.save()
|
machine_credential.save()
|
||||||
deploy_jobtemplate.credentials.add(machine_credential)
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
deploy_jobtemplate.credentials.add(vault_credential)
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
@@ -477,7 +477,7 @@ def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_credential,
|
def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_credential,
|
||||||
deploy_jobtemplate, post, rando):
|
deploy_jobtemplate, post, rando):
|
||||||
vault_credential.vault_password = 'ASK'
|
vault_credential.inputs['vault_password'] = 'ASK'
|
||||||
vault_credential.save()
|
vault_credential.save()
|
||||||
deploy_jobtemplate.credentials.add(machine_credential)
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
deploy_jobtemplate.credentials.add(vault_credential)
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
|
|||||||
@@ -19,148 +19,27 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"grant_project, grant_credential, grant_inventory, expect", [
|
"grant_project, grant_inventory, expect", [
|
||||||
(True, True, True, 201),
|
(True, True, 201),
|
||||||
(True, True, False, 403),
|
(True, False, 403),
|
||||||
(True, False, True, 403),
|
(False, True, 403),
|
||||||
(False, True, True, 403),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_credential, grant_inventory, expect):
|
def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_inventory, expect):
|
||||||
if grant_project:
|
if grant_project:
|
||||||
project.use_role.members.add(alice)
|
project.use_role.members.add(alice)
|
||||||
if grant_credential:
|
|
||||||
machine_credential.use_role.members.add(alice)
|
|
||||||
if grant_inventory:
|
if grant_inventory:
|
||||||
inventory.use_role.members.add(alice)
|
inventory.use_role.members.add(alice)
|
||||||
|
|
||||||
r = post(reverse('api:job_template_list'), {
|
r = post(reverse('api:job_template_list'), {
|
||||||
'name': 'Some name',
|
'name': 'Some name',
|
||||||
'project': project.id,
|
'project': project.id,
|
||||||
'credential': machine_credential.id, # TODO: remove in 3.3
|
|
||||||
'inventory': inventory.id,
|
'inventory': inventory.id,
|
||||||
'playbook': 'helloworld.yml',
|
'playbook': 'helloworld.yml',
|
||||||
}, alice)
|
}, alice)
|
||||||
if expect == 201:
|
|
||||||
jt = JobTemplate.objects.get(id=r.data['id'])
|
|
||||||
assert set(jt.credentials.values_list('id', flat=True)) == set([machine_credential.id])
|
|
||||||
assert r.status_code == expect
|
assert r.status_code == expect
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_create_with_v1_deprecated_credentials(get, post, project, machine_credential, credential, net_credential, inventory, alice):
|
|
||||||
project.use_role.members.add(alice)
|
|
||||||
machine_credential.use_role.members.add(alice)
|
|
||||||
credential.use_role.members.add(alice)
|
|
||||||
net_credential.use_role.members.add(alice)
|
|
||||||
inventory.use_role.members.add(alice)
|
|
||||||
|
|
||||||
pk = post(reverse('api:job_template_list', kwargs={'version': 'v1'}), {
|
|
||||||
'name': 'Some name',
|
|
||||||
'project': project.id,
|
|
||||||
'credential': machine_credential.id,
|
|
||||||
'cloud_credential': credential.id,
|
|
||||||
'network_credential': net_credential.id,
|
|
||||||
'inventory': inventory.id,
|
|
||||||
'playbook': 'helloworld.yml',
|
|
||||||
}, alice, expect=201).data['id']
|
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': pk})
|
|
||||||
response = get(url, alice)
|
|
||||||
assert response.data.get('cloud_credential') == credential.pk
|
|
||||||
assert response.data.get('network_credential') == net_credential.pk
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_create_with_empty_v1_deprecated_credentials(get, post, project, machine_credential, inventory, alice):
|
|
||||||
project.use_role.members.add(alice)
|
|
||||||
machine_credential.use_role.members.add(alice)
|
|
||||||
inventory.use_role.members.add(alice)
|
|
||||||
|
|
||||||
pk = post(reverse('api:job_template_list', kwargs={'version': 'v1'}), {
|
|
||||||
'name': 'Some name',
|
|
||||||
'project': project.id,
|
|
||||||
'credential': machine_credential.id,
|
|
||||||
'cloud_credential': None,
|
|
||||||
'network_credential': None,
|
|
||||||
'inventory': inventory.id,
|
|
||||||
'playbook': 'helloworld.yml',
|
|
||||||
}, alice, expect=201).data['id']
|
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': pk})
|
|
||||||
response = get(url, alice)
|
|
||||||
assert response.data.get('cloud_credential') is None
|
|
||||||
assert response.data.get('network_credential') is None
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_create_v1_rbac_check(get, post, project, credential, net_credential, rando):
|
|
||||||
project.use_role.members.add(rando)
|
|
||||||
|
|
||||||
base_kwargs = dict(
|
|
||||||
name = 'Made with cloud/net creds I have no access to',
|
|
||||||
project = project.id,
|
|
||||||
ask_inventory_on_launch = True,
|
|
||||||
ask_credential_on_launch = True,
|
|
||||||
playbook = 'helloworld.yml',
|
|
||||||
)
|
|
||||||
|
|
||||||
base_kwargs['cloud_credential'] = credential.pk
|
|
||||||
post(reverse('api:job_template_list', kwargs={'version': 'v1'}), base_kwargs, rando, expect=403)
|
|
||||||
|
|
||||||
base_kwargs.pop('cloud_credential')
|
|
||||||
base_kwargs['network_credential'] = net_credential.pk
|
|
||||||
post(reverse('api:job_template_list', kwargs={'version': 'v1'}), base_kwargs, rando, expect=403)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove as each field tested has support removed
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_jt_deprecated_summary_fields(
|
|
||||||
project, inventory,
|
|
||||||
machine_credential, net_credential, vault_credential,
|
|
||||||
mocker):
|
|
||||||
jt = JobTemplate.objects.create(
|
|
||||||
project=project,
|
|
||||||
inventory=inventory,
|
|
||||||
playbook='helloworld.yml'
|
|
||||||
)
|
|
||||||
|
|
||||||
class MockView:
|
|
||||||
kwargs = {}
|
|
||||||
request = None
|
|
||||||
|
|
||||||
class MockRequest:
|
|
||||||
version = 'v1'
|
|
||||||
user = None
|
|
||||||
|
|
||||||
view = MockView()
|
|
||||||
request = MockRequest()
|
|
||||||
view.request = request
|
|
||||||
serializer = JobTemplateSerializer(instance=jt, context={'view': view, 'request': request})
|
|
||||||
|
|
||||||
for kwargs in [{}, {'pk': 1}]: # detail vs. list view
|
|
||||||
for version in ['v1', 'v2']:
|
|
||||||
view.kwargs = kwargs
|
|
||||||
request.version = version
|
|
||||||
sf = serializer.get_summary_fields(jt)
|
|
||||||
assert 'credential' not in sf
|
|
||||||
assert 'vault_credential' not in sf
|
|
||||||
|
|
||||||
jt.credentials.add(machine_credential, net_credential, vault_credential)
|
|
||||||
|
|
||||||
view.kwargs = {'pk': 1}
|
|
||||||
for version in ['v1', 'v2']:
|
|
||||||
request.version = version
|
|
||||||
sf = serializer.get_summary_fields(jt)
|
|
||||||
assert 'credential' in sf
|
|
||||||
assert sf['credential'] # not empty dict
|
|
||||||
assert 'vault_credential' in sf
|
|
||||||
assert sf['vault_credential']
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_extra_credential_creation(get, post, organization_factory, job_template_factory, credentialtype_aws):
|
def test_extra_credential_creation(get, post, organization_factory, job_template_factory, credentialtype_aws):
|
||||||
objs = organization_factory("org", superusers=['admin'])
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
@@ -293,79 +172,6 @@ def test_attach_extra_credential_wrong_kind_xfail(get, post, organization_factor
|
|||||||
assert response.data.get('count') == 0
|
assert response.data.get('count') == 0
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_v1_extra_credentials_detail(get, organization_factory, job_template_factory, 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.credentials.add(credential)
|
|
||||||
jt.credentials.add(net_credential)
|
|
||||||
jt.save()
|
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
|
|
||||||
response = get(url, user=objs.superusers.admin)
|
|
||||||
assert response.data.get('cloud_credential') == credential.pk
|
|
||||||
assert response.data.get('network_credential') == net_credential.pk
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_v1_set_extra_credentials_assignment(get, patch, organization_factory, job_template_factory, 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.save()
|
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
|
|
||||||
response = patch(url, {
|
|
||||||
'cloud_credential': credential.pk,
|
|
||||||
'network_credential': net_credential.pk
|
|
||||||
}, objs.superusers.admin)
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
|
|
||||||
response = get(url, user=objs.superusers.admin)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data.get('cloud_credential') == credential.pk
|
|
||||||
assert response.data.get('network_credential') == net_credential.pk
|
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
|
|
||||||
response = patch(url, {
|
|
||||||
'cloud_credential': None,
|
|
||||||
'network_credential': None,
|
|
||||||
}, objs.superusers.admin)
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
|
|
||||||
response = get(url, user=objs.superusers.admin)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.data.get('cloud_credential') is None
|
|
||||||
assert response.data.get('network_credential') is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_filter_by_v1(get, organization_factory, job_template_factory, 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.credentials.add(credential)
|
|
||||||
jt.credentials.add(net_credential)
|
|
||||||
jt.save()
|
|
||||||
|
|
||||||
for query in (
|
|
||||||
('cloud_credential', str(credential.pk)),
|
|
||||||
('network_credential', str(net_credential.pk))
|
|
||||||
):
|
|
||||||
url = reverse('api:job_template_list', kwargs={'version': 'v1'})
|
|
||||||
response = get(
|
|
||||||
url,
|
|
||||||
user=objs.superusers.admin,
|
|
||||||
QUERY_STRING='='.join(query)
|
|
||||||
)
|
|
||||||
assert response.data.get('count') == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"grant_project, grant_inventory, expect", [
|
"grant_project, grant_inventory, expect", [
|
||||||
@@ -588,29 +394,6 @@ def test_launch_with_extra_credentials_not_allowed(get, post, organization_facto
|
|||||||
assert resp.data.get('count') == 0
|
assert resp.data.get('count') == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_v1_launch_with_extra_credentials(get, post, organization_factory,
|
|
||||||
job_template_factory, machine_credential,
|
|
||||||
credential, net_credential):
|
|
||||||
# launch requests to `/api/v1/job_templates/N/launch/` should ignore
|
|
||||||
# `extra_credentials`, as they're only supported in v2 of the API.
|
|
||||||
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.save()
|
|
||||||
|
|
||||||
resp = post(
|
|
||||||
reverse('api:job_template_launch', kwargs={'pk': jt.pk, 'version': 'v1'}),
|
|
||||||
dict(
|
|
||||||
credential=machine_credential.pk,
|
|
||||||
extra_credentials=[credential.pk, net_credential.pk]
|
|
||||||
),
|
|
||||||
objs.superusers.admin, expect=400
|
|
||||||
)
|
|
||||||
assert 'Field is not allowed for use with v1 API' in resp.data.get('extra_credentials')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_jt_without_project(inventory):
|
def test_jt_without_project(inventory):
|
||||||
data = dict(name="Test", job_type="run",
|
data = dict(name="Test", job_type="run",
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class TestJobTemplateCopyEdit:
|
|||||||
return objects.job_template
|
return objects.job_template
|
||||||
|
|
||||||
def fake_context(self, user):
|
def fake_context(self, user):
|
||||||
request = RequestFactory().get('/api/v1/resource/42/')
|
request = RequestFactory().get('/api/v2/resource/42/')
|
||||||
request.user = user
|
request.user = user
|
||||||
|
|
||||||
class FakeView(object):
|
class FakeView(object):
|
||||||
@@ -151,7 +151,7 @@ def mock_access_method(mocker):
|
|||||||
class TestAccessListCapabilities:
|
class TestAccessListCapabilities:
|
||||||
"""
|
"""
|
||||||
Test that the access_list serializer shows the exact output of the RoleAccess.can_attach
|
Test that the access_list serializer shows the exact output of the RoleAccess.can_attach
|
||||||
- looks at /api/v1/inventories/N/access_list/
|
- looks at /api/v2/inventories/N/access_list/
|
||||||
- test for types: direct, indirect, and team access
|
- test for types: direct, indirect, and team access
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def test_node_rejects_unprompted_fields(inventory, project, workflow_job_templat
|
|||||||
ask_limit_on_launch = False
|
ask_limit_on_launch = False
|
||||||
)
|
)
|
||||||
url = reverse('api:workflow_job_template_workflow_nodes_list',
|
url = reverse('api:workflow_job_template_workflow_nodes_list',
|
||||||
kwargs={'pk': workflow_job_template.pk, 'version': 'v1'})
|
kwargs={'pk': workflow_job_template.pk, 'version': 'v2'})
|
||||||
r = post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'},
|
r = post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'},
|
||||||
user=admin_user, expect=400)
|
user=admin_user, expect=400)
|
||||||
assert 'limit' in r.data
|
assert 'limit' in r.data
|
||||||
@@ -71,7 +71,7 @@ def test_node_accepts_prompted_fields(inventory, project, workflow_job_template,
|
|||||||
ask_limit_on_launch = True
|
ask_limit_on_launch = True
|
||||||
)
|
)
|
||||||
url = reverse('api:workflow_job_template_workflow_nodes_list',
|
url = reverse('api:workflow_job_template_workflow_nodes_list',
|
||||||
kwargs={'pk': workflow_job_template.pk, 'version': 'v1'})
|
kwargs={'pk': workflow_job_template.pk, 'version': 'v2'})
|
||||||
post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'},
|
post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'},
|
||||||
user=admin_user, expect=201)
|
user=admin_user, expect=201)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ class TestCreateUnifiedJob:
|
|||||||
second_job = job_with_links.copy_unified_job()
|
second_job = job_with_links.copy_unified_job()
|
||||||
|
|
||||||
# Check that job data matches the original variables
|
# Check that job data matches the original variables
|
||||||
assert second_job.credential == job_with_links.credential
|
assert [c.pk for c in second_job.credentials.all()] == [
|
||||||
|
machine_credential.pk,
|
||||||
|
net_credential.pk
|
||||||
|
]
|
||||||
assert second_job.inventory == job_with_links.inventory
|
assert second_job.inventory == job_with_links.inventory
|
||||||
assert second_job.limit == 'my_server'
|
assert second_job.limit == 'my_server'
|
||||||
assert net_credential in second_job.credentials.all()
|
assert net_credential in second_job.credentials.all()
|
||||||
|
|||||||
@@ -99,25 +99,6 @@ def test_default_cred_types():
|
|||||||
assert type_().managed_by_tower is True
|
assert type_().managed_by_tower is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.parametrize('kind', ['net', 'scm', 'ssh', 'vault'])
|
|
||||||
def test_cred_type_kind_uniqueness(kind):
|
|
||||||
"""
|
|
||||||
non-cloud credential types are exclusive_on_kind (you can only use *one* of
|
|
||||||
them at a time)
|
|
||||||
"""
|
|
||||||
assert CredentialType.defaults[kind]().unique_by_kind is True
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_cloud_kind_uniqueness():
|
|
||||||
"""
|
|
||||||
you can specify more than one cloud credential type (as long as they have
|
|
||||||
different names so you don't e.g., use ec2 twice")
|
|
||||||
"""
|
|
||||||
assert CredentialType.defaults['aws']().unique_by_kind is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_credential_creation(organization_factory):
|
def test_credential_creation(organization_factory):
|
||||||
org = organization_factory('test').organization
|
org = organization_factory('test').organization
|
||||||
@@ -141,7 +122,7 @@ def test_credential_creation(organization_factory):
|
|||||||
cred.full_clean()
|
cred.full_clean()
|
||||||
assert isinstance(cred, Credential)
|
assert isinstance(cred, Credential)
|
||||||
assert cred.name == "Bob's Credential"
|
assert cred.name == "Bob's Credential"
|
||||||
assert cred.inputs['username'] == cred.username == 'bob'
|
assert cred.inputs['username'] == 'bob'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.api.serializers import JobTemplateSerializer
|
|
||||||
from awx.main.access import (
|
from awx.main.access import (
|
||||||
BaseAccess,
|
BaseAccess,
|
||||||
JobTemplateAccess,
|
JobTemplateAccess,
|
||||||
@@ -29,16 +26,18 @@ def test_job_template_access_superuser(check_license, user, deploy_jobtemplate):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_template_access_read_level(jt_linked, rando):
|
def test_job_template_access_read_level(jt_linked, rando):
|
||||||
|
ssh_cred = jt_linked.machine_credential
|
||||||
|
vault_cred = jt_linked.vault_credentials[0]
|
||||||
|
|
||||||
access = JobTemplateAccess(rando)
|
access = JobTemplateAccess(rando)
|
||||||
jt_linked.project.read_role.members.add(rando)
|
jt_linked.project.read_role.members.add(rando)
|
||||||
jt_linked.inventory.read_role.members.add(rando)
|
jt_linked.inventory.read_role.members.add(rando)
|
||||||
jt_linked.get_deprecated_credential('ssh').read_role.members.add(rando)
|
ssh_cred.read_role.members.add(rando)
|
||||||
|
|
||||||
proj_pk = jt_linked.project.pk
|
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(inventory=jt_linked.inventory.pk, project=proj_pk))
|
||||||
assert not access.can_add(dict(credential=jt_linked.credential, project=proj_pk))
|
assert not access.can_add(dict(credential=ssh_cred.pk, project=proj_pk))
|
||||||
assert not access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk))
|
assert not access.can_add(dict(vault_credential=vault_cred.pk, project=proj_pk))
|
||||||
|
|
||||||
for cred in jt_linked.credentials.all():
|
for cred in jt_linked.credentials.all():
|
||||||
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||||
@@ -46,17 +45,19 @@ def test_job_template_access_read_level(jt_linked, rando):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_template_access_use_level(jt_linked, rando):
|
def test_job_template_access_use_level(jt_linked, rando):
|
||||||
|
ssh_cred = jt_linked.machine_credential
|
||||||
|
vault_cred = jt_linked.vault_credentials[0]
|
||||||
|
|
||||||
access = JobTemplateAccess(rando)
|
access = JobTemplateAccess(rando)
|
||||||
jt_linked.project.use_role.members.add(rando)
|
jt_linked.project.use_role.members.add(rando)
|
||||||
jt_linked.inventory.use_role.members.add(rando)
|
jt_linked.inventory.use_role.members.add(rando)
|
||||||
jt_linked.get_deprecated_credential('ssh').use_role.members.add(rando)
|
ssh_cred.use_role.members.add(rando)
|
||||||
jt_linked.get_deprecated_credential('vault').use_role.members.add(rando)
|
vault_cred.use_role.members.add(rando)
|
||||||
|
|
||||||
proj_pk = jt_linked.project.pk
|
proj_pk = jt_linked.project.pk
|
||||||
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
||||||
assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk))
|
assert access.can_add(dict(credential=ssh_cred.pk, project=proj_pk))
|
||||||
assert access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk))
|
assert access.can_add(dict(vault_credential=vault_cred.pk, project=proj_pk))
|
||||||
|
|
||||||
for cred in jt_linked.credentials.all():
|
for cred in jt_linked.credentials.all():
|
||||||
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||||
@@ -65,6 +66,8 @@ def test_job_template_access_use_level(jt_linked, rando):
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize("role_names", [("admin_role",), ("job_template_admin_role", "inventory_admin_role", "project_admin_role")])
|
@pytest.mark.parametrize("role_names", [("admin_role",), ("job_template_admin_role", "inventory_admin_role", "project_admin_role")])
|
||||||
def test_job_template_access_admin(role_names, jt_linked, rando):
|
def test_job_template_access_admin(role_names, jt_linked, rando):
|
||||||
|
ssh_cred = jt_linked.machine_credential
|
||||||
|
|
||||||
access = JobTemplateAccess(rando)
|
access = JobTemplateAccess(rando)
|
||||||
# Appoint this user as admin of the organization
|
# Appoint this user as admin of the organization
|
||||||
#jt_linked.inventory.organization.admin_role.members.add(rando)
|
#jt_linked.inventory.organization.admin_role.members.add(rando)
|
||||||
@@ -77,11 +80,11 @@ def test_job_template_access_admin(role_names, jt_linked, rando):
|
|||||||
|
|
||||||
# Assign organization permission in the same way the create view does
|
# Assign organization permission in the same way the create view does
|
||||||
organization = jt_linked.inventory.organization
|
organization = jt_linked.inventory.organization
|
||||||
jt_linked.get_deprecated_credential('ssh').admin_role.parents.add(organization.admin_role)
|
ssh_cred.admin_role.parents.add(organization.admin_role)
|
||||||
|
|
||||||
proj_pk = jt_linked.project.pk
|
proj_pk = jt_linked.project.pk
|
||||||
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
||||||
assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk))
|
assert access.can_add(dict(credential=ssh_cred.pk, project=proj_pk))
|
||||||
|
|
||||||
for cred in jt_linked.credentials.all():
|
for cred in jt_linked.credentials.all():
|
||||||
assert access.can_unattach(jt_linked, cred, 'credentials', {})
|
assert access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||||
@@ -104,7 +107,7 @@ def test_job_template_extra_credentials_prompts_access(
|
|||||||
jt.execute_role.members.add(rando)
|
jt.execute_role.members.add(rando)
|
||||||
r = post(
|
r = post(
|
||||||
reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}),
|
reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}),
|
||||||
{'vault_credential': vault_credential.pk}, rando
|
{'credentials': [machine_credential.pk, vault_credential.pk]}, rando
|
||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
@@ -126,57 +129,6 @@ class TestJobTemplateCredentials:
|
|||||||
assert JobTemplateAccess(rando).can_attach(
|
assert JobTemplateAccess(rando).can_attach(
|
||||||
job_template, credential, 'credentials', {})
|
job_template, credential, 'credentials', {})
|
||||||
|
|
||||||
def test_job_template_vault_cred_check(self, mocker, job_template, vault_credential, rando, project):
|
|
||||||
# TODO: remove in 3.4
|
|
||||||
job_template.admin_role.members.add(rando)
|
|
||||||
# not allowed to use the vault cred
|
|
||||||
# this is checked in the serializer validate method, not access.py
|
|
||||||
view = mocker.MagicMock()
|
|
||||||
view.request = mocker.MagicMock()
|
|
||||||
view.request.user = rando
|
|
||||||
serializer = JobTemplateSerializer(job_template, context={'view': view})
|
|
||||||
with pytest.raises(PermissionDenied):
|
|
||||||
serializer.validate({
|
|
||||||
'vault_credential': vault_credential.pk,
|
|
||||||
'project': project, # necessary because job_template fixture fails validation
|
|
||||||
'ask_inventory_on_launch': True,
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_job_template_vault_cred_check_noop(self, mocker, job_template, vault_credential, rando, project):
|
|
||||||
# TODO: remove in 3.4
|
|
||||||
job_template.credentials.add(vault_credential)
|
|
||||||
job_template.admin_role.members.add(rando)
|
|
||||||
# not allowed to use the vault cred
|
|
||||||
# this is checked in the serializer validate method, not access.py
|
|
||||||
view = mocker.MagicMock()
|
|
||||||
view.request = mocker.MagicMock()
|
|
||||||
view.request.user = rando
|
|
||||||
serializer = JobTemplateSerializer(job_template, context={'view': view})
|
|
||||||
# should not raise error:
|
|
||||||
serializer.validate({
|
|
||||||
'vault_credential': vault_credential.pk,
|
|
||||||
'project': project, # necessary because job_template fixture fails validation
|
|
||||||
'playbook': 'helloworld.yml',
|
|
||||||
'ask_inventory_on_launch': True,
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_new_jt_with_vault(self, mocker, vault_credential, project, rando):
|
|
||||||
project.admin_role.members.add(rando)
|
|
||||||
# TODO: remove in 3.4
|
|
||||||
# this is checked in the serializer validate method, not access.py
|
|
||||||
view = mocker.MagicMock()
|
|
||||||
view.request = mocker.MagicMock()
|
|
||||||
view.request.user = rando
|
|
||||||
serializer = JobTemplateSerializer(context={'view': view})
|
|
||||||
with pytest.raises(PermissionDenied):
|
|
||||||
serializer.validate({
|
|
||||||
'vault_credential': vault_credential.pk,
|
|
||||||
'project': project,
|
|
||||||
'playbook': 'helloworld.yml',
|
|
||||||
'ask_inventory_on_launch': True,
|
|
||||||
'name': 'asdf'
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestOrphanJobTemplate:
|
class TestOrphanJobTemplate:
|
||||||
|
|||||||
@@ -103,8 +103,7 @@ class TestJobTemplateSerializerGetSummaryFields():
|
|||||||
with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'):
|
with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'):
|
||||||
with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'):
|
with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'):
|
||||||
with mocker.patch("awx.main.access.JobTemplateAccess.can_copy", return_value='foo'):
|
with mocker.patch("awx.main.access.JobTemplateAccess.can_copy", return_value='foo'):
|
||||||
with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None):
|
response = serializer.get_summary_fields(jt_obj)
|
||||||
response = serializer.get_summary_fields(jt_obj)
|
|
||||||
|
|
||||||
assert response['user_capabilities']['copy'] == 'foo'
|
assert response['user_capabilities']['copy'] == 'foo'
|
||||||
assert response['user_capabilities']['edit'] == 'foobar'
|
assert response['user_capabilities']['edit'] == 'foobar'
|
||||||
|
|||||||
@@ -688,13 +688,19 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
job.websocket_emit_status = mock.Mock()
|
job.websocket_emit_status = mock.Mock()
|
||||||
job._credentials = []
|
job._credentials = []
|
||||||
|
|
||||||
|
def _credentials_filter(credential_type__kind=None):
|
||||||
|
creds = job._credentials
|
||||||
|
if credential_type__kind:
|
||||||
|
creds = [c for c in creds if c.credential_type.kind == credential_type__kind]
|
||||||
|
return mock.Mock(
|
||||||
|
__iter__ = lambda *args: iter(creds),
|
||||||
|
first = lambda: creds[0] if len(creds) else None
|
||||||
|
)
|
||||||
|
|
||||||
credentials_mock = mock.Mock(**{
|
credentials_mock = mock.Mock(**{
|
||||||
'all': lambda: job._credentials,
|
'all': lambda: job._credentials,
|
||||||
'add': job._credentials.append,
|
'add': job._credentials.append,
|
||||||
'filter.return_value': mock.Mock(
|
'filter.side_effect': _credentials_filter,
|
||||||
__iter__ = lambda *args: iter(job._credentials),
|
|
||||||
first = lambda: job._credentials[0]
|
|
||||||
),
|
|
||||||
'prefetch_related': lambda _: credentials_mock,
|
'prefetch_related': lambda _: credentials_mock,
|
||||||
'spec_set': ['all', 'add', 'filter', 'prefetch_related'],
|
'spec_set': ['all', 'add', 'filter', 'prefetch_related'],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from rest_framework.generics import ListAPIView
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.views import ApiErrorView
|
from awx.main.views import ApiErrorView
|
||||||
from awx.api.views import JobList, InventorySourceList
|
from awx.api.views import JobList
|
||||||
from awx.api.generics import ListCreateAPIView, SubListAttachDetachAPIView
|
from awx.api.generics import ListCreateAPIView, SubListAttachDetachAPIView
|
||||||
|
|
||||||
|
|
||||||
@@ -40,20 +40,10 @@ def test_exception_view_raises_exception(api_view_obj_fixture, method_name):
|
|||||||
getattr(api_view_obj_fixture, method_name)(request_mock)
|
getattr(api_view_obj_fixture, method_name)(request_mock)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('version, supports_post', [(1, True), (2, False)])
|
def test_disable_post_on_v2_jobs_list():
|
||||||
def test_disable_post_on_v2_jobs_list(version, supports_post):
|
|
||||||
job_list = JobList()
|
job_list = JobList()
|
||||||
job_list.request = mock.MagicMock()
|
job_list.request = mock.MagicMock()
|
||||||
with mock.patch('awx.api.views.get_request_version', return_value=version):
|
assert ('POST' in job_list.allowed_methods) is False
|
||||||
assert ('POST' in job_list.allowed_methods) == supports_post
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('version, supports_post', [(1, False), (2, True)])
|
|
||||||
def test_disable_post_on_v1_inventory_source_list(version, supports_post):
|
|
||||||
inv_source_list = InventorySourceList()
|
|
||||||
inv_source_list.request = mock.MagicMock()
|
|
||||||
with mock.patch('awx.api.views.get_request_version', return_value=version):
|
|
||||||
assert ('POST' in inv_source_list.allowed_methods) == supports_post
|
|
||||||
|
|
||||||
|
|
||||||
def test_views_have_search_fields(all_views):
|
def test_views_have_search_fields(all_views):
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
|
|||||||
|
|
||||||
$scope.$on(`${list.iterator}_options`, function(event, data){
|
$scope.$on(`${list.iterator}_options`, function(event, data){
|
||||||
$scope.options = data.data.actions.GET;
|
$scope.options = data.data.actions.GET;
|
||||||
optionsRequestDataProcessing();
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$watchCollection(`${$scope.list.name}`, function() {
|
|
||||||
optionsRequestDataProcessing();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function assignCredentialKinds () {
|
function assignCredentialKinds () {
|
||||||
@@ -69,26 +64,6 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', '
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterate over the list and add fields like type label, after the
|
|
||||||
// OPTIONS request returns, or the list is sorted/paginated/searched
|
|
||||||
function optionsRequestDataProcessing(){
|
|
||||||
if ($scope[list.name] !== undefined) {
|
|
||||||
$scope[list.name].forEach(function(item, item_idx) {
|
|
||||||
var itm = $scope[list.name][item_idx];
|
|
||||||
|
|
||||||
// Set the item type label
|
|
||||||
if (list.fields.kind && $scope.options &&
|
|
||||||
$scope.options.hasOwnProperty('kind')) {
|
|
||||||
$scope.options.kind.choices.forEach(function(choice) {
|
|
||||||
if (choice[0] === item.kind) {
|
|
||||||
itm.kind_label = choice[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.copyCredential = credential => {
|
$scope.copyCredential = credential => {
|
||||||
Wait('start');
|
Wait('start');
|
||||||
new Credential('get', credential.id)
|
new Credential('get', credential.id)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function adhocController($q, $scope, $stateParams,
|
|||||||
return {
|
return {
|
||||||
adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/',
|
adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/',
|
||||||
inventoryUrl: GetBasePath('inventory') + id + '/',
|
inventoryUrl: GetBasePath('inventory') + id + '/',
|
||||||
machineCredentialUrl: GetBasePath('credentials') + '?kind=ssh'
|
machineCredentialUrl: GetBasePath('credentials') + '?credential_type__namespace=ssh'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -133,25 +133,36 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.lookupCredential = function(){
|
$scope.lookupCredential = function(){
|
||||||
if($scope.source.value !== "scm" && $scope.source.value !== "custom") {
|
// For most source type selections, we filter for 1-1 matches to credential_type namespace.
|
||||||
let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value;
|
let searchKey = 'credential_type__namespace';
|
||||||
$state.go('.credential', {
|
let searchValue = $scope.source.value;
|
||||||
credential_search: {
|
|
||||||
kind: kind,
|
// SCM and custom source types are more generic in terms of the credentials they
|
||||||
page_size: '5',
|
// accept - any cloud or user-defined credential type can be used. We filter for
|
||||||
page: '1'
|
// these using the credential_type kind field, which categorizes all cloud and
|
||||||
}
|
// user-defined credentials as 'cloud'.
|
||||||
});
|
if ($scope.source.value === 'scm') {
|
||||||
|
searchKey = 'credential_type__kind';
|
||||||
|
searchValue = 'cloud';
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
$state.go('.credential', {
|
if ($scope.source.value === 'custom') {
|
||||||
credential_search: {
|
searchKey = 'credential_type__kind';
|
||||||
credential_type__kind: "cloud",
|
searchValue = 'cloud';
|
||||||
page_size: '5',
|
|
||||||
page: '1'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the selection is 'ec2' we actually want to filter for the 'aws' namespace.
|
||||||
|
if ($scope.source.value === 'ec2') {
|
||||||
|
searchValue = 'aws';
|
||||||
|
}
|
||||||
|
|
||||||
|
$state.go('.credential', {
|
||||||
|
credential_search: {
|
||||||
|
[searchKey]: searchValue,
|
||||||
|
page_size: '5',
|
||||||
|
page: '1'
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.lookupProject = function(){
|
$scope.lookupProject = function(){
|
||||||
@@ -169,7 +180,7 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars
|
|||||||
$scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network';
|
$scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network';
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
$scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source));
|
$scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?credential_type__namespace=aws' : GetBasePath('credentials') + (source === '' ? '' : '?credential_type__namespace=' + (source));
|
||||||
}
|
}
|
||||||
if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6" || source === "azure_rm") {
|
if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6" || source === "azure_rm") {
|
||||||
$scope.envParseType = 'yaml';
|
$scope.envParseType = 'yaml';
|
||||||
|
|||||||
@@ -302,25 +302,36 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange',
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.lookupCredential = function(){
|
$scope.lookupCredential = function(){
|
||||||
if($scope.source.value !== "scm" && $scope.source.value !== "custom") {
|
// For most source type selections, we filter for 1-1 matches to credential_type namespace.
|
||||||
let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value;
|
let searchKey = 'credential_type__namespace';
|
||||||
$state.go('.credential', {
|
let searchValue = $scope.source.value;
|
||||||
credential_search: {
|
|
||||||
kind: kind,
|
// SCM and custom source types are more generic in terms of the credentials they
|
||||||
page_size: '5',
|
// accept - any cloud or user-defined credential type can be used. We filter for
|
||||||
page: '1'
|
// these using the credential_type kind field, which categorizes all cloud and
|
||||||
}
|
// user-defined credentials as 'cloud'.
|
||||||
});
|
if ($scope.source.value === 'scm') {
|
||||||
|
searchKey = 'credential_type__kind';
|
||||||
|
searchValue = 'cloud';
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
$state.go('.credential', {
|
if ($scope.source.value === 'custom') {
|
||||||
credential_search: {
|
searchKey = 'credential_type__kind';
|
||||||
credential_type__kind: "cloud",
|
searchValue = 'cloud';
|
||||||
page_size: '5',
|
|
||||||
page: '1'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the selection is 'ec2' we actually want to filter for the 'aws' namespace.
|
||||||
|
if ($scope.source.value === 'ec2') {
|
||||||
|
searchValue = 'aws';
|
||||||
|
}
|
||||||
|
|
||||||
|
$state.go('.credential', {
|
||||||
|
credential_search: {
|
||||||
|
[searchKey]: searchValue,
|
||||||
|
page_size: '5',
|
||||||
|
page: '1'
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formCancel = function() {
|
$scope.formCancel = function() {
|
||||||
@@ -384,7 +395,7 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange',
|
|||||||
$scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network';
|
$scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network';
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
$scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?kind=aws' : GetBasePath('credentials') + (source === '' ? '' : '?kind=' + (source));
|
$scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?credential_type__namespace=aws' : GetBasePath('credentials') + (source === '' ? '' : 'credential_type__namespace=' + (source));
|
||||||
}
|
}
|
||||||
if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6") {
|
if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6") {
|
||||||
$scope.envParseType = 'yaml';
|
$scope.envParseType = 'yaml';
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default {
|
|||||||
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$transition$',
|
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$transition$',
|
||||||
(list, qs, $stateParams, GetBasePath, $transition$) => {
|
(list, qs, $stateParams, GetBasePath, $transition$) => {
|
||||||
const toState = $transition$.to();
|
const toState = $transition$.to();
|
||||||
toState.params.credential_search.value.kind = _.get($stateParams, 'credential_search.kind', null);
|
toState.params.credential_search.value.credential_type__namespace = _.get($stateParams, 'credential_search.credential_type__namespace', null);
|
||||||
toState.params.credential_search.value.credential_type__kind = _.get($stateParams, 'credential_search.credential_type__kind', null);
|
toState.params.credential_search.value.credential_type__kind = _.get($stateParams, 'credential_search.credential_type__kind', null);
|
||||||
return qs.search(GetBasePath('credentials'), $stateParams[`${list.iterator}_search`]);
|
return qs.search(GetBasePath('credentials'), $stateParams[`${list.iterator}_search`]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* /api/v2/inventories/9/
|
* /api/v2/inventories/9/
|
||||||
* /api/v2/credentials/?name=SSH Key&kind=ssh
|
* /api/v2/credentials/?name=SSH Key&credential_type__namespace=ssh
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* When constructing the URL be sure to use the GetBasePath() method found in js/shared/Utilities.js. GetBasePath uses the response objects from /api and
|
* When constructing the URL be sure to use the GetBasePath() method found in js/shared/Utilities.js. GetBasePath uses the response objects from /api and
|
||||||
|
|||||||
@@ -654,13 +654,13 @@ function(SettingsUtils, i18n, $rootScope) {
|
|||||||
else {
|
else {
|
||||||
switch(base) {
|
switch(base) {
|
||||||
case 'credential':
|
case 'credential':
|
||||||
query += '&kind=ssh&role_level=use_role';
|
query += '&credential_type__namespace=ssh&role_level=use_role';
|
||||||
break;
|
break;
|
||||||
case 'scm_credential':
|
case 'scm_credential':
|
||||||
query += '&kind=scm&role_level=use_role';
|
query += '&redential_type__namespace=scm&role_level=use_role';
|
||||||
break;
|
break;
|
||||||
case 'network_credential':
|
case 'network_credential':
|
||||||
query += '&kind=net&role_level=use_role';
|
query += '&redential_type__namespace=net&role_level=use_role';
|
||||||
break;
|
break;
|
||||||
case 'cloud_credential':
|
case 'cloud_credential':
|
||||||
query += '&cloud=true&role_level=use_role';
|
query += '&cloud=true&role_level=use_role';
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ def version(request):
|
|||||||
context.get('view'),
|
context.get('view'),
|
||||||
'deprecated',
|
'deprecated',
|
||||||
False
|
False
|
||||||
) or request.path.startswith('/api/v1/')
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,52 +262,6 @@ endpoint:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
API Backwards Compatability
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
`/api/v1/credentials/` still exists in Tower 3.2, and it transparently works as
|
|
||||||
before with minimal surprises by attempting to translate `/api/v1/` requests to
|
|
||||||
the new ``Credential`` and ``Credential Type`` models.
|
|
||||||
|
|
||||||
* When creating or modifying a ``Job Template`` through `v1` of the API,
|
|
||||||
old-style credential assignment will transparently map to the new model. For
|
|
||||||
example, the following `POST`'ed payload:
|
|
||||||
|
|
||||||
{
|
|
||||||
credential: <pk>,
|
|
||||||
vault_credential: <pk>,
|
|
||||||
cloud_credential: <pk>,
|
|
||||||
network_credential: <pk>,
|
|
||||||
}
|
|
||||||
|
|
||||||
...would transparently update ``JobTemplate.extra_credentials`` to a list
|
|
||||||
containing both the cloud and network ``Credentials``.
|
|
||||||
|
|
||||||
Similarly, an `HTTP GET /api/v1/job_credentials/N/` will populate
|
|
||||||
`cloud_credential`, and `network_credential` with the *most recently applied*
|
|
||||||
matching credential in the list.
|
|
||||||
|
|
||||||
* Custom ``Credentials`` will not be returned in the ``v1`` API; if a user
|
|
||||||
defines their own ``Credential Type``, its credentials won't show up in the
|
|
||||||
``v1`` API.
|
|
||||||
|
|
||||||
* ``HTTP POST`` requests to ``/api/v1/credentials/`` will transparently map
|
|
||||||
old-style attributes (i.e., ``username``, ``password``, ``ssh_key_data``) to
|
|
||||||
the appropriate new-style model. Similarly, ``HTTP GET
|
|
||||||
/api/v1/credentials/N/`` requests will continue to contain old-style
|
|
||||||
key-value mappings in their payloads.
|
|
||||||
|
|
||||||
* Vault credentials are a new first-level type of credential in Tower 3.2.
|
|
||||||
As such, any ``Credentials`` pre-Tower 3.2 that contain *both* SSH and Vault
|
|
||||||
parameters will be migrated to separate distinct ``Credentials``
|
|
||||||
post-migration.
|
|
||||||
|
|
||||||
For example, if your Tower 3.1 installation has one ``Credential`` with
|
|
||||||
a defined ``username``, ``password``, and ``vault_password``, after migration
|
|
||||||
*two* ``Credentials`` will exist (one which contains the ``username`` and
|
|
||||||
``password``, and another which contains only the ``vault_password``).
|
|
||||||
|
|
||||||
|
|
||||||
Additional Criteria
|
Additional Criteria
|
||||||
-------------------
|
-------------------
|
||||||
* Rackspace is being removed from official support in Tower 3.2. Pre-existing
|
* Rackspace is being removed from official support in Tower 3.2. Pre-existing
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ Notifications can succeed or fail but that will not cause its associated job to
|
|||||||
|
|
||||||
Once a Notification Template is created, its configuration can be tested by utilizing the endpoint at `/api/v2/notification_templates/<n>/test` This will emit a test notification given the configuration defined by the notification. These test notifications will also appear in the notifications list at `/api/v2/notifications`
|
Once a Notification Template is created, its configuration can be tested by utilizing the endpoint at `/api/v2/notification_templates/<n>/test` This will emit a test notification given the configuration defined by the notification. These test notifications will also appear in the notifications list at `/api/v2/notifications`
|
||||||
|
|
||||||
|
|
||||||
# Notification Types
|
# Notification Types
|
||||||
|
|
||||||
The currently defined Notification Types are:
|
The currently defined Notification Types are:
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ def make_the_data():
|
|||||||
name='%s Credential %d User %d' % (prefix, credential_id, user_idx),
|
name='%s Credential %d User %d' % (prefix, credential_id, user_idx),
|
||||||
defaults=dict(created_by=next(creator_gen),
|
defaults=dict(created_by=next(creator_gen),
|
||||||
modified_by=next(modifier_gen)),
|
modified_by=next(modifier_gen)),
|
||||||
credential_type=CredentialType.from_v1_kind('ssh')
|
credential_type=CredentialType.objects.filter(namespace='ssh').first()
|
||||||
)
|
)
|
||||||
credential.admin_role.members.add(user)
|
credential.admin_role.members.add(user)
|
||||||
credentials.append(credential)
|
credentials.append(credential)
|
||||||
@@ -355,7 +355,7 @@ def make_the_data():
|
|||||||
name='%s Credential %d team %d' % (prefix, credential_id, team_idx),
|
name='%s Credential %d team %d' % (prefix, credential_id, team_idx),
|
||||||
defaults=dict(created_by=next(creator_gen),
|
defaults=dict(created_by=next(creator_gen),
|
||||||
modified_by=next(modifier_gen)),
|
modified_by=next(modifier_gen)),
|
||||||
credential_type=CredentialType.from_v1_kind('ssh')
|
credential_type=CredentialType.objects.filter(namespace='ssh').first()
|
||||||
)
|
)
|
||||||
credential.admin_role.parents.add(team.member_role)
|
credential.admin_role.parents.add(team.member_role)
|
||||||
credentials.append(credential)
|
credentials.append(credential)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ docker run --name awx_test -it --memory="4g" --cpuset="0,1" -v /Users/meyers/ans
|
|||||||
|
|
||||||
## How to use the logstash container
|
## How to use the logstash container
|
||||||
|
|
||||||
POST the following content to `/api/v1/settings/logging/` (this uses
|
POST the following content to `/api/v2/settings/logging/` (this uses
|
||||||
authentication set up inside of the logstash configuration file).
|
authentication set up inside of the logstash configuration file).
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ and
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
These can be entered via Configure-Tower-in-Tower by making a POST to
|
These can be entered via Configure-Tower-in-Tower by making a POST to
|
||||||
`/api/v1/settings/logging/`.
|
`/api/v2/settings/logging/`.
|
||||||
|
|
||||||
### Connecting Logstash to 3rd Party Receivers
|
### Connecting Logstash to 3rd Party Receivers
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import getpass
|
|
||||||
import json
|
|
||||||
import urllib2
|
|
||||||
|
|
||||||
REST_API_URL = "http://awx.example.com/api/v1/"
|
|
||||||
REST_API_USER = "admin"
|
|
||||||
REST_API_PASS = "password"
|
|
||||||
JOB_TEMPLATE_ID = 1
|
|
||||||
|
|
||||||
# Setup urllib2 for basic password authentication.
|
|
||||||
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
|
||||||
password_mgr.add_password(None, REST_API_URL, REST_API_USER, REST_API_PASS)
|
|
||||||
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
|
|
||||||
opener = urllib2.build_opener(handler)
|
|
||||||
urllib2.install_opener(opener)
|
|
||||||
|
|
||||||
# Read the job template.
|
|
||||||
JOB_TEMPLATE_URL="%sjob_templates/%d/" % (REST_API_URL, JOB_TEMPLATE_ID)
|
|
||||||
response = urllib2.urlopen(JOB_TEMPLATE_URL)
|
|
||||||
data = json.loads(response.read())
|
|
||||||
|
|
||||||
# Update data if needed for the new job.
|
|
||||||
data.pop('id')
|
|
||||||
data.update({
|
|
||||||
'name': 'my new job started at %s' % str(datetime.datetime.now()),
|
|
||||||
'verbosity': 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Create a new job based on the template and data.
|
|
||||||
JOB_TEMPLATE_JOBS_URL="%sjobs/" % JOB_TEMPLATE_URL
|
|
||||||
request = urllib2.Request(JOB_TEMPLATE_JOBS_URL, json.dumps(data),
|
|
||||||
{'Content-type': 'application/json'})
|
|
||||||
response = urllib2.urlopen(request)
|
|
||||||
data = json.loads(response.read())
|
|
||||||
|
|
||||||
# Get the job ID and check for passwords needed to start the job.
|
|
||||||
JOB_ID = data['id']
|
|
||||||
JOB_START_URL = '%sjobs/%d/start/' % (REST_API_URL, JOB_ID)
|
|
||||||
response = urllib2.urlopen(JOB_START_URL)
|
|
||||||
data = json.loads(response.read())
|
|
||||||
|
|
||||||
# Prompt for any passwords needed.
|
|
||||||
start_data = {}
|
|
||||||
for password in data.get('passwords_needed_to_start', []):
|
|
||||||
value = getpass.getpass('%s: ' % password)
|
|
||||||
start_data[password] = value
|
|
||||||
|
|
||||||
# Make POST request to start the job.
|
|
||||||
request = urllib2.Request(JOB_START_URL, json.dumps(start_data),
|
|
||||||
{'Content-type': 'application/json'})
|
|
||||||
response = urllib2.urlopen(request)
|
|
||||||
Reference in New Issue
Block a user