From 6da445f7c06e1b7babdc20928492f1b0f7c59618 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 12 Mar 2019 17:12:16 -0400 Subject: [PATCH 1/6] remove /api/v1 and deprecated credential fields --- awx/api/filters.py | 47 -- awx/api/generics.py | 30 +- awx/api/metadata.py | 13 - awx/api/serializers.py | 525 ++---------------- .../templates/api/job_template_callback.md | 8 +- awx/api/templates/api/user_me_list.md | 2 +- awx/api/urls/job.py | 2 - awx/api/urls/urls.py | 48 +- awx/api/versioning.py | 13 - awx/api/views/__init__.py | 222 ++------ awx/api/views/inventory.py | 3 +- awx/api/views/root.py | 30 +- awx/conf/serializers.py | 2 +- awx/conf/tests/functional/test_api.py | 35 -- awx/conf/views.py | 15 +- awx/main/access.py | 18 - awx/main/conf.py | 2 +- awx/main/fields.py | 10 +- .../commands/create_preload_data.py | 2 +- awx/main/models/__init__.py | 8 +- awx/main/models/credential/__init__.py | 250 +-------- awx/main/models/jobs.py | 57 +- awx/main/tasks.py | 15 +- awx/main/tests/factories/fixtures.py | 2 +- .../tests/functional/api/test_credential.py | 450 +-------------- .../test_deprecated_credential_assignment.py | 170 ------ .../functional/api/test_job_runtime_params.py | 18 +- .../tests/functional/api/test_job_template.py | 227 +------- .../functional/api/test_rbac_displays.py | 4 +- .../functional/api/test_workflow_node.py | 4 +- .../functional/models/test_unified_job.py | 5 +- awx/main/tests/functional/test_credential.py | 21 +- .../functional/test_rbac_job_templates.py | 80 +-- .../test_job_template_serializers.py | 3 +- awx/main/tests/unit/test_tasks.py | 14 +- awx/main/tests/unit/test_views.py | 16 +- awx/ui/context_processors.py | 2 +- docs/custom_credential_types.md | 46 -- docs/notification_system.md | 1 - .../rbac_dummy_data_generator.py | 4 +- tools/docker-compose/README | 2 +- tools/elastic/README.md | 2 +- tools/scripts/launch_job.py | 54 -- 43 files changed, 271 insertions(+), 2211 deletions(-) delete mode 100755 tools/scripts/launch_job.py diff --git a/awx/api/filters.py b/awx/api/filters.py index b41a191627..547d1b3a1a 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -24,20 +24,6 @@ from rest_framework.filters import BaseFilterBackend # AWX 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.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): @@ -292,39 +278,6 @@ class FieldLookupBackend(BaseFilterBackend): key = key[5:] 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=&or__credential_type=` - 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. for value in values: if q_int: diff --git a/awx/api/generics.py b/awx/api/generics.py index 6417e59871..0a33b889cd 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -34,7 +34,7 @@ from rest_framework.negotiation import DefaultContentNegotiation # AWX from awx.api.filters import FieldLookupBackend from awx.main.models import ( - UnifiedJob, UnifiedJobTemplate, User, Role + UnifiedJob, UnifiedJobTemplate, User, Role, Credential ) from awx.main.access import access_registry 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.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 __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView', @@ -288,12 +288,6 @@ class APIView(views.APIView): template_list.append('api/%s.md' % template_basename) 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) if context.get('deprecated') and context.get('swagger_method') is None: # render deprecation messages at the very top @@ -842,10 +836,6 @@ class CopyAPIView(GenericAPIView): new_in_330 = 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): if not self.copy_return_serializer_class: return self.get_serializer(*args, **kwargs) @@ -859,15 +849,15 @@ class CopyAPIView(GenericAPIView): def _decrypt_model_field_if_needed(obj, field_name, field_val): if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []): 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: if isinstance(sub_field, str) \ and isinstance(field_val[sub_field], str): - try: - 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) + field_val[sub_field] = decrypt_field(obj, field_name, sub_field) elif isinstance(field_val, str): try: field_val = decrypt_field(obj, field_name) @@ -952,8 +942,6 @@ class CopyAPIView(GenericAPIView): return ret def get(self, request, *args, **kwargs): - if get_request_version(request) < 2: - return self.v1_not_allowed() obj = self.get_object() if not request.user.can_access(obj.__class__, 'read', obj): raise PermissionDenied() @@ -968,8 +956,6 @@ class CopyAPIView(GenericAPIView): return Response({'can_copy': can_copy}) def post(self, request, *args, **kwargs): - if get_request_version(request) < 2: - return self.v1_not_allowed() obj = self.get_object() create_kwargs = self._build_create_dict(obj) create_kwargs_check = {} diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 3f7ff7ea0b..cc44e6d0e9 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -232,19 +232,6 @@ class RoleMetadata(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): def determine_actions(self, request, view): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index ad001e44cc..0614a52e5a 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -54,7 +54,7 @@ from awx.main.models import ( OAuth2AccessToken, OAuth2Application, Organization, Project, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule, SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob, - UnifiedJobTemplate, V1Credential, WorkflowJob, WorkflowJobNode, + UnifiedJobTemplate, WorkflowJob, WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded ) 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.api.versioning import reverse, get_request_version +from awx.api.versioning import reverse from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField, VerbatimField, DeprecatedCredentialField) @@ -113,7 +113,6 @@ SUMMARIZABLE_FK_FIELDS = { 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), '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_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 { '': reverse() } 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'): return {} @@ -301,10 +300,7 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl @property def version(self): - """ - 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 + return 2 def get_type(self, obj): 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 \ type(obj) in settings.NAMED_URL_GRAPH: original_url = self.get_url(obj) - if not original_url.startswith('/api/v1'): - res['named_url'] = self._generate_named_url( - original_url, obj, settings.NAMED_URL_GRAPH[type(obj)] - ) + res['named_url'] = self._generate_named_url( + original_url, obj, settings.NAMED_URL_GRAPH[type(obj)] + ) if getattr(obj, 'created_by', None): res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk}) if getattr(obj, 'modified_by', None): @@ -396,8 +391,6 @@ class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetacl continue summary_fields[fk] = OrderedDict() 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) @@ -884,10 +877,10 @@ class UserSerializer(BaseSerializer): 'username', 'first_name', 'last_name', '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.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') 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}), 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}), + 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: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) @@ -1561,9 +1554,8 @@ class InventorySerializer(BaseSerializerWithVariables): 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}), 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: res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk}) if obj.organization: @@ -1615,20 +1607,6 @@ class InventorySerializer(BaseSerializerWithVariables): 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 Meta: @@ -1668,19 +1646,15 @@ class HostSerializer(BaseSerializerWithVariables): 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_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: res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) if obj.last_job: res['last_job'] = self.reverse('api:job_detail', kwargs={'pk': obj.last_job.pk}) 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}) - if self.version > 1: - res.update(dict( - ansible_facts = self.reverse('api:host_ansible_facts_detail', kwargs={'pk': obj.pk}), - )) return res def get_summary_fields(self, obj): @@ -1766,6 +1740,7 @@ class AnsibleFactsSerializer(BaseSerializer): class GroupSerializer(BaseSerializerWithVariables): + show_capabilities = ['copy', 'edit', 'delete'] capabilities_prefetch = ['inventory.admin', 'inventory.adhoc'] groups_with_active_failures = serializers.IntegerField( read_only=True, @@ -1779,13 +1754,6 @@ class GroupSerializer(BaseSerializerWithVariables): 'total_hosts', 'hosts_with_active_failures', 'total_groups', '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): field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(field_name, relation_info) # Inventory is read-only unless creating a new group. @@ -1794,20 +1762,6 @@ class GroupSerializer(BaseSerializerWithVariables): field_kwargs.pop('queryset', None) 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): res = super(GroupSerializer, self).get_related(obj) res.update(dict( @@ -1822,24 +1776,10 @@ class GroupSerializer(BaseSerializerWithVariables): 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}), )) - 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: res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) 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): if value in ('all', '_meta'): raise serializers.ValidationError(_('Invalid group name.')) @@ -1941,9 +1881,8 @@ class CustomInventoryScriptSerializer(BaseSerializer): res = super(CustomInventoryScriptSerializer, self).get_related(obj) res.update(dict( 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: 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) - # 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): @@ -2036,14 +1954,12 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt {'admin': 'inventory.admin'}, {'start': 'inventory.update'} ] - group = serializers.SerializerMethodField( - help_text=_('Automatic group relationship, will be removed in 3.3')) class Meta: model = InventorySource fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', '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): res = super(InventorySourceSerializer, self).get_related(obj) @@ -2069,30 +1985,10 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt if obj.last_update: res['last_update'] = self.reverse('api:inventory_update_detail', 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: res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk}) 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 if obj.deprecated_group: return obj.deprecated_group.id @@ -2127,12 +2023,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory.")) 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): if value and self.instance and self.instance.schedules.exists(): raise serializers.ValidationError(_("Setting not compatible with existing schedules.")) @@ -2253,8 +2143,7 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri if obj.inventory: 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 @@ -2562,67 +2451,22 @@ class CredentialTypeSerializer(BaseSerializer): 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): show_capabilities = ['edit', 'delete', 'copy', 'use'] capabilities_prefetch = ['admin', 'use'] class Meta: model = Credential - fields = ('*', 'organization') - - def get_fields(self): - fields = super(CredentialSerializer, self).get_fields() - - # TODO: remove when API v1 is removed - if self.version == 1: - fields.update(V1CredentialFields().get_fields()) - else: - fields.update(V2CredentialFields().get_fields()) - return fields + fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud') + extra_kwargs = { + 'credential_type': { + 'label': _('Credential Type'), + }, + } def to_representation(self, 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: value['inputs'] = data.display_inputs() return value @@ -2639,16 +2483,10 @@ class CredentialSerializer(BaseSerializer): 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_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] if parents: @@ -2684,54 +2522,12 @@ class CredentialSerializer(BaseSerializer): return summary_dict 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) for field in ('credential_type', 'inputs'): if field in ret: ret.remove(field) 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): if self.instance and credential_type.pk != self.instance.credential_type.pk: for rel in ( @@ -2788,35 +2584,12 @@ class CredentialSerializerCreate(CredentialSerializer): if attrs.get('team'): attrs['organization'] = attrs['team'].organization - try: - 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 + return super(CredentialSerializerCreate, self).validate(attrs) def create(self, validated_data): user = validated_data.pop('user', 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) if user: @@ -2895,35 +2668,6 @@ class LabelsListMixin(object): 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 Meta: @@ -2932,16 +2676,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer): 'force_handlers', 'skip_tags', 'start_at_task', 'timeout', 'use_fact_cache',) - def get_fields(self): - fields = super(JobOptionsSerializer, self).get_fields() - - # TODO: remove when API v1 is removed - if self.version == 1: - fields.update(V1JobOptionsSerializer().get_fields()) - - fields.update(LegacyCredentialFields().get_fields()) - return fields - def get_related(self, obj): res = super(JobOptionsSerializer, self).get_related(obj) res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk}) @@ -2955,40 +2689,18 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer): res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk}) except ObjectDoesNotExist: setattr(obj, 'project', None) - try: - if obj.credential: - res['credential'] = self.reverse( - 'api:credential_detail', kwargs={'pk': obj.credential} - ) - except ObjectDoesNotExist: - setattr(obj, 'credential', None) - try: - if obj.vault_credential: - res['vault_credential'] = self.reverse( - 'api:credential_detail', kwargs={'pk': obj.vault_credential} - ) - 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}) + 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}) return res @@ -3002,70 +2714,9 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer): ret['project'] = None if 'playbook' in ret: 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 - 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): - 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: project = attrs.get('project', self.instance and self.instance.project or None) playbook = attrs.get('playbook', self.instance and self.instance.playbook or '') @@ -3079,7 +2730,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer): raise serializers.ValidationError({'playbook': _('Must select playbook for project.')}) ret = super(JobOptionsSerializer, self).validate(attrs) - ret.update(v1_credentials) return ret @@ -3105,12 +2755,6 @@ class JobTemplateMixin(object): 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['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 @@ -3146,9 +2790,8 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO 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}), 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: res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk}) return res @@ -3181,9 +2824,6 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj) all_creds = [] # Organize credential data into multitude of deprecated fields - # TODO: remove most of this as v1 is removed - vault_credential = None - credential = None extra_creds = [] if obj.pk: for cred in obj.credentials.all(): @@ -3194,30 +2834,12 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO 'kind': cred.kind, 'cloud': cred.credential_type.kind == 'cloud' } - if self.version > 1: - summarized_cred['credential_type_id'] = cred.credential_type_id all_creds.append(summarized_cred) if cred.credential_type.kind in ('cloud', 'net'): extra_creds.append(summarized_cred) - elif summarized_cred['kind'] == 'ssh': - credential = summarized_cred - elif summarized_cred['kind'] == 'vault': - 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 + if self.is_detail_view: + summary_fields['extra_credentials'] = extra_creds + summary_fields['credentials'] = all_creds return summary_fields @@ -3250,6 +2872,7 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): activity_stream = self.reverse('api:job_activity_stream_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}), + create_schedule = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}), )) try: if obj.job_template: @@ -3257,8 +2880,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): kwargs={'pk': obj.job_template.pk}) except ObjectDoesNotExist: 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: res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk}) try: @@ -3268,8 +2889,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): ) except ObjectDoesNotExist: 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}) return res @@ -3320,9 +2939,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): summary_fields = super(JobSerializer, self).get_summary_fields(obj) all_creds = [] # Organize credential data into multitude of deprecated fields - # TODO: remove most of this as v1 is removed - vault_credential = None - credential = None extra_creds = [] if obj.pk: for cred in obj.credentials.all(): @@ -3333,30 +2949,12 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): 'kind': cred.kind, 'cloud': cred.credential_type.kind == 'cloud' } - if self.version > 1: - summarized_cred['credential_type_id'] = cred.credential_type_id all_creds.append(summarized_cred) if cred.credential_type.kind in ('cloud', 'net'): extra_creds.append(summarized_cred) - elif summarized_cred['kind'] == 'ssh': - credential = summarized_cred - elif summarized_cred['kind'] == 'vault': - 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 + if self.is_detail_view: + summary_fields['extra_credentials'] = extra_creds + summary_fields['credentials'] = all_creds return summary_fields @@ -3696,9 +3294,8 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo 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}), 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: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) return res @@ -4401,17 +3998,16 @@ class JobLaunchSerializer(BaseSerializer): name=getattrd(obj, '%s.name' % field_name, None), id=getattrd(obj, '%s.pk' % field_name, None)) elif field_name == 'credentials': - if self.version > 1: - for cred in obj.credentials.all(): - cred_dict = dict( - id=cred.id, - name=cred.name, - credential_type=cred.credential_type.pk, - passwords_needed=cred.passwords_needed - ) - 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) - defaults_dict.setdefault(field_name, []).append(cred_dict) + for cred in obj.credentials.all(): + cred_dict = dict( + id=cred.id, + name=cred.name, + credential_type=cred.credential_type.pk, + passwords_needed=cred.passwords_needed + ) + 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) + defaults_dict.setdefault(field_name, []).append(cred_dict) else: defaults_dict[field_name] = getattr(obj, field_name) return defaults_dict @@ -4584,9 +4180,8 @@ class NotificationTemplateSerializer(BaseSerializer): res.update(dict( test = self.reverse('api:notification_template_test', 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: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) return res diff --git a/awx/api/templates/api/job_template_callback.md b/awx/api/templates/api/job_template_callback.md index 99ae79b42a..ef3348b829 100644 --- a/awx/api/templates/api/job_template_callback.md +++ b/awx/api/templates/api/job_template_callback.md @@ -8,15 +8,15 @@ job template. 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: - 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: - 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 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 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) that would match the request: diff --git a/awx/api/templates/api/user_me_list.md b/awx/api/templates/api/user_me_list.md index 3935d23e09..ebed9cac2c 100644 --- a/awx/api/templates/api/user_me_list.md +++ b/awx/api/templates/api/user_me_list.md @@ -6,4 +6,4 @@ One result should be returned containing the following fields: {% 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. diff --git a/awx/api/urls/job.py b/awx/api/urls/job.py index ca7d1b2f14..de45cba9aa 100644 --- a/awx/api/urls/job.py +++ b/awx/api/urls/job.py @@ -6,7 +6,6 @@ from django.conf.urls import url from awx.api.views import ( JobList, JobDetail, - JobStart, JobCancel, JobRelaunch, JobCreateSchedule, @@ -23,7 +22,6 @@ from awx.api.views import ( urls = [ url(r'^$', JobList.as_view(), name='job_list'), url(r'^(?P[0-9]+)/$', JobDetail.as_view(), name='job_detail'), - url(r'^(?P[0-9]+)/start/$', JobStart.as_view(), name='job_start'), # Todo: Remove In 3.3 url(r'^(?P[0-9]+)/cancel/$', JobCancel.as_view(), name='job_cancel'), url(r'^(?P[0-9]+)/relaunch/$', JobRelaunch.as_view(), name='job_relaunch'), url(r'^(?P[0-9]+)/create_schedule/$', JobCreateSchedule.as_view(), name='job_create_schedule'), diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 4a8fb61b1f..31eb6b78d0 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -11,10 +11,9 @@ from awx.api.generics import ( ) from awx.api.views import ( ApiRootView, - ApiV1RootView, ApiV2RootView, - ApiV1PingView, - ApiV1ConfigView, + ApiV2PingView, + ApiV2ConfigView, AuthView, UserMeList, DashboardView, @@ -74,10 +73,25 @@ from .oauth2 import urls as oauth2_urls from .oauth2_root import urls as oauth2_root_urls -v1_urls = [ - url(r'^$', ApiV1RootView.as_view(), name='api_v1_root_view'), - url(r'^ping/$', ApiV1PingView.as_view(), name='api_v1_ping_view'), - url(r'^config/$', ApiV1ConfigView.as_view(), name='api_v1_config_view'), +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[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'), + url(r'^jobs/(?P[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'), + url(r'^jobs/(?P[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'), + url(r'^job_templates/(?P[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'), + url(r'^job_templates/(?P[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[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'), + url(r'^applications/(?P[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'^me/$', UserMeList.as_view(), name='user_me_list'), url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'), @@ -119,30 +133,10 @@ v1_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[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'), - url(r'^jobs/(?P[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'), - url(r'^jobs/(?P[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'), - url(r'^job_templates/(?P[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'), - url(r'^job_templates/(?P[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[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'), - url(r'^applications/(?P[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' urlpatterns = [ url(r'^$', ApiRootView.as_view(), name='api_root_view'), url(r'^(?P(v2))/', include(v2_urls)), - url(r'^(?P(v1|v2))/', include(v1_urls)), url(r'^login/$', LoggedLoginView.as_view( template_name='rest_framework/login.html', extra_context={'inside_login_context': True} diff --git a/awx/api/versioning.py b/awx/api/versioning.py index 4e5d5a9288..3ad9638832 100644 --- a/awx/api/versioning.py +++ b/awx/api/versioning.py @@ -27,19 +27,6 @@ def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **e 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): if request is None or getattr(request, 'version', None) is None: # We need the "current request" to determine the correct version to diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index e8754af8bf..c6f51ac493 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -62,7 +62,6 @@ from wsgiref.util import FileWrapper # AWX from awx.main.tasks import send_notifications, update_inventory_computed_fields from awx.main.access import get_user_queryset, HostAccess -from awx.api.filters import V1CredentialFilterBackend from awx.api.generics import ( APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin, GenericAPIView, ListAPIView, ListCreateAPIView, @@ -72,7 +71,7 @@ from awx.api.generics import ( SubListCreateAPIView, SubListCreateAttachDetachAPIView, 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.main import models from awx.main.utils import ( @@ -96,7 +95,7 @@ from awx.api.permissions import ( ) from awx.api import renderers 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.scheduler.dag_workflow import WorkflowDAG from awx.api.views.mixin import ( @@ -143,10 +142,9 @@ from awx.api.views.root import ( # noqa ApiRootView, ApiOAuthAuthorizationRootView, ApiVersionRootView, - ApiV1RootView, ApiV2RootView, - ApiV1PingView, - ApiV1ConfigView, + ApiV2PingView, + ApiV2ConfigView, ) @@ -1246,22 +1244,10 @@ class CredentialTypeActivityStreamList(SubListAPIView): search_fields = ('changes',) -# remove in 3.3 -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): +class CredentialList(ListCreateAPIView): model = models.Credential serializer_class = serializers.CredentialSerializerCreate - filter_backends = ListCreateAPIView.filter_backends + [V1CredentialFilterBackend] class CredentialOwnerUsersList(SubListAPIView): @@ -1289,13 +1275,12 @@ class CredentialOwnerTeamsList(SubListAPIView): return self.model.objects.filter(pk__in=teams) -class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView): +class UserCredentialsList(SubListCreateAPIView): model = models.Credential serializer_class = serializers.UserCredentialSerializerCreate parent_model = models.User parent_key = 'user' - filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend] def get_queryset(self): user = self.get_parent_object() @@ -1306,13 +1291,12 @@ class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView): return user_creds & visible_creds -class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView): +class TeamCredentialsList(SubListCreateAPIView): model = models.Credential serializer_class = serializers.TeamCredentialSerializerCreate parent_model = models.Team parent_key = 'team' - filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend] def get_queryset(self): team = self.get_parent_object() @@ -1323,13 +1307,12 @@ class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView): return (team_creds & visible_creds).distinct() -class OrganizationCredentialList(CredentialViewMixin, SubListCreateAPIView): +class OrganizationCredentialList(SubListCreateAPIView): model = models.Credential serializer_class = serializers.OrganizationCredentialSerializerCreate parent_model = models.Organization parent_key = 'organization' - filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend] def get_queryset(self): organization = self.get_parent_object() @@ -1348,7 +1331,6 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView): model = models.Credential serializer_class = serializers.CredentialSerializer - filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend] class CredentialActivityStreamList(SubListAPIView): @@ -1754,10 +1736,10 @@ class EnforceParentRelationshipMixin(object): * 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 - form /api/v1/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 - Instead, you would access the child_child_model via /api/v1/child_child_model/87/ - and you would create child_child_model's off of /api/v1/child_model/87/child_child_model_set + form /api/v2/parent_model/34/child_model. If you then wanted a child of the + 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/v2/child_child_model/87/ + 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 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() if not request.user.can_access(self.model, 'delete', obj): 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() return Response(status=status.HTTP_204_NO_CONTENT) @@ -2093,13 +2070,6 @@ class InventorySourceList(ListCreateAPIView): serializer_class = serializers.InventorySourceSerializer 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): @@ -2290,7 +2260,6 @@ class InventoryUpdateNotificationsList(SubListAPIView): class JobTemplateList(ListCreateAPIView): model = models.JobTemplate - metadata_class = JobTypeMetadata serializer_class = serializers.JobTemplateSerializer always_allow_superuser = False @@ -2305,7 +2274,6 @@ class JobTemplateList(ListCreateAPIView): class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = models.JobTemplate - metadata_class = JobTypeMetadata serializer_class = serializers.JobTemplateSerializer always_allow_superuser = False @@ -2314,7 +2282,6 @@ class JobTemplateLaunch(RetrieveAPIView): model = models.JobTemplate obj_permission_type = 'start' - metadata_class = JobTypeMetadata serializer_class = serializers.JobLaunchSerializer always_allow_superuser = False @@ -2358,65 +2325,44 @@ class JobTemplateLaunch(RetrieveAPIView): ignored_fields = {} modern_data = data.copy() - for fd in ('credential', 'vault_credential', 'inventory'): - id_fd = '{}_id'.format(fd) - if fd not in modern_data and id_fd in modern_data: - 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." - )}) + id_fd = '{}_id'.format('inventory') + if 'inventory' not in modern_data and id_fd in modern_data: + modern_data['inventory'] = modern_data[id_fd] # Automatically convert legacy launch credential arguments into a list of `.credentials` - if 'credentials' in modern_data and ( - 'credential' in modern_data or - 'vault_credential' in modern_data or - 'extra_credentials' in modern_data - ): + if 'credentials' in modern_data and 'extra_credentials' in modern_data: 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 ( - 'credential' in modern_data or - 'vault_credential' in modern_data or - 'extra_credentials' in modern_data - ): + if 'extra_credentials' in modern_data: # make a list of the current credentials existing_credentials = obj.credentials.all() template_credentials = list(existing_credentials) # save copy of existing new_credentials = [] - for key, conditional, _type, type_repr in ( - ('credential', lambda cred: cred.credential_type.kind != 'ssh', int, 'pk value'), - ('vault_credential', lambda cred: cred.credential_type.kind != 'vault', int, 'pk value'), - ('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'), Iterable, 'a list') - ): - if key in modern_data: - # if a specific deprecated key is specified, remove all - # credentials of _that_ type from the list of current - # credentials - existing_credentials = filter(conditional, existing_credentials) - prompted_value = modern_data.pop(key) + if 'extra_credentials' in modern_data: + existing_credentials = [ + cred for cred in existing_credentials + if cred.credential_type.kind not in ('cloud', 'net') + ] + prompted_value = modern_data.pop('extra_credentials') - # validate type, since these are not covered by a serializer - if not isinstance(prompted_value, _type): - msg = _( - "Incorrect type. Expected {}, received {}." - ).format(type_repr, prompted_value.__class__.__name__) - raise ParseError({key: [msg], 'credentials': [msg]}) + # validate type, since these are not covered by a serializer + if not isinstance(prompted_value, Iterable): + msg = _( + "Incorrect type. Expected a list received {}." + ).format(prompted_value.__class__.__name__) + raise ParseError({'extra_credentials': [msg], 'credentials': [msg]}) - # add the deprecated credential specified in the request - if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str): - prompted_value = [prompted_value] + # add the deprecated credential specified in the request + if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str): + prompted_value = [prompted_value] - # If user gave extra_credentials, special case to use exactly - # the given list without merging with JT credentials - if key == 'extra_credentials' and prompted_value: - obj._deprecated_credential_launch = True # signal to not merge credentials - new_credentials.extend(prompted_value) + # If user gave extra_credentials, special case to use exactly + # the given list without merging with JT credentials + if prompted_value: + obj._deprecated_credential_launch = True # signal to not merge credentials + new_credentials.extend(prompted_value) # combine the list of "new" and the filtered list of "old" new_credentials.extend([cred.pk for cred in existing_credentials]) @@ -2926,7 +2872,7 @@ class JobTemplateCallback(GenericAPIView): return Response(status=status.HTTP_201_CREATED, headers=headers) -class JobTemplateJobsList(SubListCreateAPIView): +class JobTemplateJobsList(SubListAPIView): model = models.Job serializer_class = serializers.JobListSerializer @@ -2934,13 +2880,6 @@ class JobTemplateJobsList(SubListCreateAPIView): relationship = 'jobs' 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): @@ -3135,8 +3074,6 @@ class WorkflowJobTemplateCopy(CopyAPIView): copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer def get(self, request, *args, **kwargs): - if get_request_version(request) < 2: - return self.v1_not_allowed() obj = self.get_object() if not request.user.can_access(obj.__class__, 'read', obj): raise PermissionDenied() @@ -3493,56 +3430,17 @@ class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetac relationship = 'notification_templates_success' -class JobList(ListCreateAPIView): +class JobList(ListAPIView): model = models.Job - metadata_class = JobTypeMetadata 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 - 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): +class JobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.Job - metadata_class = JobTypeMetadata 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): obj = self.get_object() # Only allow changes (PUT/PATCH) when job status is "new". @@ -3591,44 +3489,6 @@ class JobActivityStreamList(SubListAPIView): 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): model = models.Job diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index a057c7cdf2..15daa55232 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -44,7 +44,6 @@ from awx.api.serializers import ( InstanceGroupSerializer, InventoryUpdateEventSerializer, CustomInventoryScriptSerializer, - InventoryDetailSerializer, JobTemplateSerializer, ) from awx.api.views.mixin import ( @@ -119,7 +118,7 @@ class InventoryList(ListCreateAPIView): class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): model = Inventory - serializer_class = InventoryDetailSerializer + serializer_class = InventorySerializer def update(self, request, *args, **kwargs): obj = self.get_object() diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 0456631ce8..2e4312d613 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -25,7 +25,7 @@ from awx.main.utils import ( get_custom_venv_choices, 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.main.constants import PRIVILEGE_ESCALATION_METHODS from awx.main.models import ( @@ -50,12 +50,11 @@ class ApiRootView(APIView): def get(self, request, format=None): ''' List supported API versions ''' - v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'}) v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'}) data = OrderedDict() data['description'] = _('AWX REST API') 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['custom_logo'] = settings.CUSTOM_LOGO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO @@ -85,10 +84,10 @@ class ApiVersionRootView(APIView): def get(self, request, format=None): ''' List top level resources ''' 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['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['me'] = reverse('api:user_me_list', 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['teams'] = reverse('api:team_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_input_sources'] = reverse('api:credential_input_source_list', request=request) - data['applications'] = reverse('api:o_auth2_application_list', request=request) - data['tokens'] = reverse('api:o_auth2_token_list', request=request) - data['metrics'] = reverse('api:metrics_view', 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['applications'] = reverse('api:o_auth2_application_list', request=request) + data['tokens'] = reverse('api:o_auth2_token_list', request=request) + data['metrics'] = reverse('api:metrics_view', request=request) data['inventory'] = reverse('api:inventory_list', request=request) data['inventory_scripts'] = reverse('api:inventory_script_list', request=request) data['inventory_sources'] = reverse('api:inventory_source_list', request=request) @@ -131,15 +129,11 @@ class ApiVersionRootView(APIView): return Response(data) -class ApiV1RootView(ApiVersionRootView): - view_name = _('Version 1') - - class ApiV2RootView(ApiVersionRootView): view_name = _('Version 2') -class ApiV1PingView(APIView): +class ApiV2PingView(APIView): """A simple view that reports very basic information about this instance, which is acceptable to be public information. """ @@ -174,14 +168,14 @@ class ApiV1PingView(APIView): return Response(response) -class ApiV1ConfigView(APIView): +class ApiV2ConfigView(APIView): permission_classes = (IsAuthenticated,) view_name = _('Configuration') swagger_topic = 'System Configuration' 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'}: self.permission_denied(request) # Raises PermissionDenied exception. diff --git a/awx/conf/serializers.py b/awx/conf/serializers.py index 3c643d8417..e297fe1e69 100644 --- a/awx/conf/serializers.py +++ b/awx/conf/serializers.py @@ -88,7 +88,7 @@ class SettingSingletonSerializer(serializers.Serializer): continue extra_kwargs = {} # 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 if key in ('LICENSE', 'AWX_ISOLATED_KEY_GENERATION'): extra_kwargs['read_only'] = True diff --git a/awx/conf/tests/functional/test_api.py b/awx/conf/tests/functional/test_api.py index 15789c501b..aac1a56127 100644 --- a/awx/conf/tests/functional/test_api.py +++ b/awx/conf/tests/functional/test_api.py @@ -65,41 +65,6 @@ def test_non_admin_user_does_not_see_categories(api_request, dummy_setting, norm 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 def test_setting_singleton_detail_retrieve(api_request, dummy_setting): with dummy_setting( diff --git a/awx/conf/views.py b/awx/conf/views.py index ac704e9f37..bab468e0eb 100644 --- a/awx/conf/views.py +++ b/awx/conf/views.py @@ -24,7 +24,7 @@ from awx.api.generics import ( RetrieveUpdateDestroyAPIView, ) 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.handlers import AWXProxyHandler, LoggingConnectivityException 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')) -VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE = { - 1: set([ - 'named-url', - ]), - 2: set([]), -} - class SettingCategoryList(ListAPIView): @@ -60,8 +53,6 @@ class SettingCategoryList(ListAPIView): else: categories = {} 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) setting_categories.append(SettingCategory(url, category_slug, categories[category_slug])) return setting_categories @@ -77,8 +68,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView): def get_queryset(self): self.category_slug = self.kwargs.get('category_slug', 'all') 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): category_slugs = all_category_slugs else: @@ -90,7 +79,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView): registered_settings = settings_registry.get_registered_settings( 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': return Setting.objects.filter(key__in=registered_settings, user=self.request.user) @@ -101,7 +89,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView): settings_qs = self.get_queryset() registered_settings = settings_registry.get_registered_settings( category_slug=self.category_slug, - slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)] ) all_settings = {} for setting in settings_qs: diff --git a/awx/main/access.py b/awx/main/access.py index 1b3753c412..09b16585e5 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1001,19 +1001,6 @@ class GroupAccess(BaseAccess): def can_delete(self, obj): 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): ''' @@ -2387,11 +2374,6 @@ class UnifiedJobTemplateAccess(BaseAccess): Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs( 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): access_class = access_registry[obj.__class__] access_instance = access_class(self.user) diff --git a/awx/main/conf.py b/awx/main/conf.py index 7f7ace83f0..7db0737acf 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -118,7 +118,7 @@ register( default=_load_default_license_from_file, label=_('License'), 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.'), category=_('System'), category_slug='system', diff --git a/awx/main/fields.py b/awx/main/fields.py index ecd2211711..70ee365086 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -638,7 +638,7 @@ class CredentialInputField(JSONSchemaField): v != '$encrypted$', model_instance.pk ]): - if not isinstance(getattr(model_instance, k), str): + if not isinstance(model_instance.inputs.get(k), str): raise django_exceptions.ValidationError( _('secret values must be of type string, not {}').format(type(v).__name__), code='invalid', @@ -704,15 +704,15 @@ class CredentialInputField(JSONSchemaField): # 'ssh_key_unlock': 'do-you-need-me?', # } # ...we have to fetch the actual key value from the database - if model_instance.pk and model_instance.ssh_key_data == '$encrypted$': - model_instance.ssh_key_data = model_instance.__class__.objects.get( + if model_instance.pk and model_instance.inputs.get('ssh_key_data') == '$encrypted$': + model_instance.inputs['ssh_key_data'] = model_instance.__class__.objects.get( 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'): errors['ssh_key_unlock'] = [_('must be set when SSH key is encrypted.')] if all([ - model_instance.ssh_key_data, + model_instance.inputs.get('ssh_key_data'), value.get('ssh_key_unlock'), not model_instance.has_encrypted_ssh_key_data ]): diff --git a/awx/main/management/commands/create_preload_data.py b/awx/main/management/commands/create_preload_data.py index 21a07827e5..297622af46 100644 --- a/awx/main/management/commands/create_preload_data.py +++ b/awx/main/management/commands/create_preload_data.py @@ -34,7 +34,7 @@ class Command(BaseCommand): scm_update_cache_timeout=0, organization=o) 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, name='Demo Credential', inputs={ diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index c105dd4efc..974aca40c8 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -16,7 +16,7 @@ from awx.main.models.organization import ( # noqa Organization, Profile, Team, UserSessionMembership ) 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.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): - # 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) @@ -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): - # 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) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 227766a855..e99401ad7c 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -42,7 +42,7 @@ from awx.main.models.rbac import ( from awx.main.utils import encrypt_field 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') credential_plugins = dict( @@ -73,164 +73,6 @@ def build_safe_env(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): ''' A credential contains information about how to talk to a remote resource @@ -286,34 +128,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): '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 def kind(self): - # TODO 3.3: remove the need for this helper property by removing its - # 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 + return self.credential_type.namespace @property def cloud(self): @@ -330,7 +147,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): # @property 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 def has_encrypted_ssh_key_data(self): @@ -350,17 +167,17 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): @property 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 False @property 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 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 def passwords_needed(self): @@ -396,6 +213,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): 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): if field not in self.inputs: return None @@ -405,13 +226,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): elif field in self.inputs: 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): field_val = self.inputs.copy() for k, v in field_val.items(): @@ -429,7 +243,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): type_alias = self.credential_type.name else: 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: fmt_str = '{} (id={})' else: @@ -456,7 +270,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): :param field_name(str): The name of the input field. :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) if field_name in self.credential_type.secret_fields: try: @@ -552,15 +366,8 @@ class CredentialType(CommonModelNameNotUnique): return instance 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) - @property - def unique_by_kind(self): - return self.kind != 'cloud' - @property def defined_fields(self): return [field.get('id') for field in self.inputs.get('fields', [])] @@ -629,29 +436,6 @@ class CredentialType(CommonModelNameNotUnique): 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): """ Inject credential data into the environment variables and arguments @@ -678,9 +462,11 @@ class CredentialType(CommonModelNameNotUnique): files) """ 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 = {} - 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) safe_env.update(build_safe_env(injected_env)) return @@ -1335,12 +1121,12 @@ class CredentialInputSource(PrimordialModel): ) 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')) return self.target_credential 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')) return self.source_credential diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 27b4b28394..b8f2661c97 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -18,7 +18,7 @@ from django.db import models from django.utils.encoding import smart_str from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ValidationError, FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist # REST Framework from rest_framework.exceptions import ParseError @@ -152,21 +152,9 @@ class JobOptions(BaseModel): extra_vars_dict = VarsDictProperty('extra_vars', True) - def clean_credential(self): - cred = self.credential - if cred and cred.kind != 'ssh': - 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 + def machine_credential(self): + return self.credentials.filter(credential_type__kind='ssh').first() @property def network_credentials(self): @@ -180,41 +168,6 @@ class JobOptions(BaseModel): def vault_credentials(self): 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 def passwords_needed_to_start(self): '''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, project=self.project.name if self.project else None, playbook=self.playbook, - credential=getattr(self.get_deprecated_credential('ssh'), 'name', None), + credential=getattr(self.machine_credential, 'name', None), limit=self.limit, extra_vars=self.display_extra_vars(), hosts=all_hosts)) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 0321cf910f..662a93cd79 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -794,7 +794,7 @@ class BaseTask(object): data += '\n' # 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. - 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: os.mkdir(os.path.join(private_data_dir, 'env')) except OSError as e: @@ -1324,7 +1324,7 @@ class RunJob(BaseTask): and ansible-vault. ''' passwords = super(RunJob, self).build_passwords(job, runtime_passwords) - cred = job.get_deprecated_credential('ssh') + cred = job.machine_credential if cred: 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='')) @@ -1408,6 +1408,9 @@ class RunJob(BaseTask): # Set environment variables for cloud 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: 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, 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 = '', '', '' if creds: @@ -2226,9 +2229,9 @@ class RunAdHocCommand(BaseTask): creds = ad_hoc_command.credential ssh_username, become_username, become_method = '', '', '' if creds: - ssh_username = creds.username - become_method = creds.become_method - become_username = creds.become_username + ssh_username = creds.get_input('username', default='') + become_method = creds.get_input('become_method', default='') + become_username = creds.get_input('become_username', default='') else: become_method = None become_username = "" diff --git a/awx/main/tests/factories/fixtures.py b/awx/main/tests/factories/fixtures.py index 2f3ec0656f..d4ad255e4a 100644 --- a/awx/main/tests/factories/fixtures.py +++ b/awx/main/tests/factories/fixtures.py @@ -168,7 +168,7 @@ def mk_job_template(name, job_type='run', if persisted and credential: jt.save() jt.credentials.add(credential) - if jt.credential is None: + if jt.machine_credential is None: jt.ask_credential_on_launch = True jt.project = project diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index 879a3e4de6..31d2c444f0 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -1,4 +1,3 @@ -import itertools import re from unittest import mock # noqa @@ -27,197 +26,6 @@ def test_idempotent_credential_type_setup(): 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 # @@ -225,7 +33,6 @@ def test_filter_by_v1_invalid_kind(get, admin, organization): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) 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.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) 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.parametrize('version, params', [ - ['v1', {'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): @@ -282,7 +87,6 @@ def test_create_user_credential_via_user_credentials_list(post, get, alice, cred @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) 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.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) 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.parametrize('version, params', [ - ['v1', {'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): @@ -345,7 +147,6 @@ def test_create_team_credential(post, get, team, organization, org_admin, team_m @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'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): @@ -368,7 +169,6 @@ def test_create_team_credential_via_team_credentials_list(post, get, team, org_a @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'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): @@ -385,7 +185,6 @@ def test_create_team_credential_by_urelated_user_xfail(post, team, organization, @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'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): @@ -407,7 +206,7 @@ def test_create_team_credential_by_team_member_xfail(post, team, organization, a @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): credential.organization = organization 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.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): credential.organization = organization 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.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): credential.organization = organization 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.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): credential.organization = organization 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.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob, version): # normal users can't do this 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.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): # org admins can't either 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.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): # but system admins can 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.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob, version): # normal users can't do this 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.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): # org admins can't either 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.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): # but system admins can 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.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): assert org_auditor not in credential.read_role 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.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): assert org_auditor not in credential.read_role 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.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): # 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}), { @@ -551,7 +350,7 @@ def test_sa_grant_private_credential_to_team_through_role_teams(post, credential @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): # 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}), { @@ -567,7 +366,6 @@ def test_sa_grant_private_credential_to_team_through_team_roles(post, credential @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'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): @@ -583,7 +381,6 @@ def test_create_org_credential_as_not_admin(post, organization, org_member, cred @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) 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.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) 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.parametrize('version, params', [ - ['v1', {'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): @@ -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('version', ('v1', 'v2')) @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')): response = post( - reverse('api:credential_list', kwargs={'version': version}), + reverse('api:credential_list', kwargs={'version': 'v2'}), { 'organization': organization.id, 'name': 'C%d' % i, @@ -682,7 +476,7 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin ) response = get( - reverse('api:credential_list', kwargs={'version': version}), + reverse('api:credential_list', kwargs={'version': 'v2'}), org_admin, QUERY_STRING='order_by=%s' % order_by, status=400 @@ -690,22 +484,6 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin 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 def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, credentialtype_ssh): 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] -@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.parametrize('field_value', ['', None, False]) 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 cred = Credential.objects.all()[:1].get() - assert cred.authorize is False + assert cred.inputs['authorize'] is False @pytest.mark.django_db @@ -811,14 +561,6 @@ def test_field_dependencies(get, post, organization, admin, kind, extraneous): # @pytest.mark.django_db @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', { 'credential_type': 1, 'name': 'Best credential ever', @@ -851,12 +593,6 @@ def test_scm_create_ok(post, organization, admin, version, params): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'ssh', - 'name': 'Best credential ever', - 'password': 'secret', - 'vault_password': '', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -882,38 +618,11 @@ def test_ssh_create_ok(post, organization, admin, version, params): 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 # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'ssh', - 'name': 'Best credential ever', - 'vault_password': 'some_password', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -968,16 +677,6 @@ def test_vault_password_required(post, organization, admin): # @pytest.mark.django_db @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', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1017,13 +716,6 @@ def test_net_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'cloudforms', - 'name': 'Best credential ever', - 'host': 'some_host', - 'username': 'some_username', - 'password': 'some_password', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1057,13 +749,6 @@ def test_cloudforms_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @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', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1097,16 +782,6 @@ def test_gce_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @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', { 'credential_type': 1, '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.parametrize('version, params', [ - ['v1', { - 'kind': 'satellite6', - 'name': 'Best credential ever', - 'host': 'some_host', - 'username': 'some_username', - 'password': 'some_password', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1186,13 +854,6 @@ def test_satellite6_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'aws', - 'name': 'Best credential ever', - 'username': 'some_username', - 'password': 'some_password', - 'security_token': 'abc123' - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1223,10 +884,6 @@ def test_aws_create_ok(post, organization, admin, version, params): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'aws', - 'name': 'Best credential ever', - }], ['v2', { 'credential_type': 1, '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.parametrize('version, params', [ - ['v1', { - 'kind': 'vmware', - 'host': 'some_host', - 'name': 'Best credential ever', - 'username': 'some_username', - 'password': 'some_password' - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1295,10 +945,6 @@ def test_vmware_create_ok(post, organization, admin, version, params): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'vmware', - 'name': 'Best credential ever', - }], ['v2', { 'credential_type': 1, '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.parametrize('version, params', [ - ['v1', { - 'username': 'some_user', - 'password': 'some_password', - 'project': 'some_project', - 'host': 'some_host', - }], ['v2', { 'credential_type': 1, 'inputs': { @@ -1396,7 +1036,6 @@ def test_openstack_verify_ssl(get, post, organization, admin, verify_ssl, expect @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {}], ['v2', { 'credential_type': 1, 'inputs': {} @@ -1425,12 +1064,6 @@ def test_openstack_create_fail_required_fields(post, organization, admin, versio @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'name': 'Best credential ever', - 'kind': 'ssh', - 'username': 'joe', - 'password': '', - }], ['v2', { 'name': 'Best credential ever', 'credential_type': 1, @@ -1624,12 +1257,6 @@ def test_cloud_credential_type_mutability(patch, organization, admin, credential @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'name': 'Best credential ever', - 'kind': 'ssh', - 'username': 'joe', - 'ssh_key_data': '$encrypted$', - }], ['v2', { 'name': 'Best credential ever', 'credential_type': 1, @@ -1664,13 +1291,6 @@ def test_ssh_unlock_needed(put, organization, admin, credentialtype_ssh, version @pytest.mark.django_db @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', { 'name': 'Best credential ever', '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.parametrize('version, params', [ - ['v1', { - 'name': 'Best credential ever', - 'kind': 'ssh', - 'username': 'joe', - 'ssh_key_data': '$encrypted$', - 'ssh_key_unlock': 'new-unlock', - }], ['v2', { 'name': 'Best credential ever', '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.parametrize('version, params', [ - ['v1', { - 'kind': 'ssh', - 'username': 'joe', - 'password': 'secret', - }], ['v2', { 'credential_type': 1, 'inputs': { @@ -1783,12 +1391,8 @@ def test_secret_encryption_on_create(get, post, organization, admin, credentialt assert response.status_code == 200 assert response.data['count'] == 1 cred = response.data['results'][0] - if version == 'v1': - assert cred['username'] == 'joe' - assert cred['password'] == '$encrypted$' - elif version == 'v2': - assert cred['inputs']['username'] == 'joe' - assert cred['inputs']['password'] == '$encrypted$' + assert cred['inputs']['username'] == 'joe' + assert cred['inputs']['password'] == '$encrypted$' cred = Credential.objects.all()[:1].get() 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.parametrize('version, params', [ - ['v1', {'password': 'secret'}], ['v2', {'inputs': {'username': 'joe', 'password': 'secret'}}] ]) 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.data['count'] == 1 cred = response.data['results'][0] - if version == 'v1': - assert cred['username'] == 'joe' - assert cred['password'] == '$encrypted$' - elif version == 'v2': - assert cred['inputs']['username'] == 'joe' - assert cred['inputs']['password'] == '$encrypted$' + assert cred['inputs']['username'] == 'joe' + assert cred['inputs']['password'] == '$encrypted$' cred = Credential.objects.all()[:1].get() 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.parametrize('version, params', [ - ['v1', { - 'username': 'joe', - 'password': '$encrypted$', - }], ['v2', { 'inputs': { 'username': 'joe', @@ -1930,7 +1525,6 @@ def test_custom_credential_type_create(get, post, organization, admin): @pytest.mark.parametrize('version, params', [ - ['v1', {'name': 'Some name', 'username': 'someusername'}], ['v2', {'name': 'Some name', 'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) @pytest.mark.django_db diff --git a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py index 37ab829e9e..0c35bfce41 100644 --- a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py +++ b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py @@ -1,4 +1,3 @@ -import json from unittest import mock import pytest @@ -25,74 +24,6 @@ def job_template(job_template, project, inventory): 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 def test_extra_credentials_filtering(get, job_template, admin, 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) -@pytest.mark.django_db -def test_ssh_credential_at_launch(get, post, job_template, admin, machine_credential): - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - pk = post(url, {'credential': machine_credential.pk}, admin, expect=201).data['job'] - summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields'] - - assert len(summary_fields['credentials']) == 1 - - -@pytest.mark.django_db -def test_vault_credential_at_launch(get, post, job_template, admin, vault_credential): - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - pk = post(url, {'vault_credential': vault_credential.pk}, admin, expect=201).data['job'] - summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields'] - - assert len(summary_fields['credentials']) == 1 - - @pytest.mark.django_db def test_extra_credentials_at_launch(get, post, job_template, admin, credential): url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) @@ -236,30 +149,6 @@ def test_extra_credentials_at_launch(get, post, job_template, admin, credential) 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 def test_modify_extra_credentials_at_launch(get, post, job_template, admin, 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 -@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 def test_ssh_password_prompted_at_launch(get, post, job_template, admin, 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) -@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 def test_deprecated_credential_activity_stream(patch, admin_user, machine_credential, job_template): job_template.credentials.add(machine_credential) diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index 7a8f184476..d90f63e180 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -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) job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields) - assert job_obj.credential == machine_credential.pk - assert job_obj.vault_credential == vault_credential.pk + assert job_obj.machine_credential.pk == machine_credential.pk + assert job_obj.vault_credentials[0].pk == vault_credential.pk @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) job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields) - assert job_obj.credential is deploy_jobtemplate.credential - assert job_obj.vault_credential is deploy_jobtemplate.vault_credential + assert job_obj.machine_credential.pk == deploy_jobtemplate.machine_credential.pk + assert job_obj.vault_credentials[0].pk == deploy_jobtemplate.vault_credentials[0].pk @pytest.mark.django_db def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_credential, deploy_jobtemplate, post, rando): - vault_credential.vault_password = 'ASK' + vault_credential.inputs['vault_password'] = 'ASK' vault_credential.save() deploy_jobtemplate.credentials.add(vault_credential) 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 def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_jobtemplate, post, rando): - machine_credential.password = 'ASK' + machine_credential.inputs['password'] = 'ASK' machine_credential.save() deploy_jobtemplate.credentials.add(machine_credential) 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 def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential, vault_credential, deploy_jobtemplate, post, rando): - vault_credential.vault_password = 'ASK' + vault_credential.inputs['vault_password'] = 'ASK' vault_credential.save() - machine_credential.password = 'ASK' + machine_credential.inputs['password'] = 'ASK' machine_credential.save() deploy_jobtemplate.credentials.add(machine_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 def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_credential, deploy_jobtemplate, post, rando): - vault_credential.vault_password = 'ASK' + vault_credential.inputs['vault_password'] = 'ASK' vault_credential.save() deploy_jobtemplate.credentials.add(machine_credential) deploy_jobtemplate.credentials.add(vault_credential) diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index 9d62c70a69..10c978eb06 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -19,148 +19,27 @@ from rest_framework.exceptions import ValidationError @pytest.mark.django_db @pytest.mark.parametrize( - "grant_project, grant_credential, grant_inventory, expect", [ - (True, True, True, 201), - (True, True, False, 403), - (True, False, True, 403), - (False, True, True, 403), + "grant_project, grant_inventory, expect", [ + (True, True, 201), + (True, False, 403), + (False, 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: project.use_role.members.add(alice) - if grant_credential: - machine_credential.use_role.members.add(alice) if grant_inventory: inventory.use_role.members.add(alice) r = post(reverse('api:job_template_list'), { 'name': 'Some name', 'project': project.id, - 'credential': machine_credential.id, # TODO: remove in 3.3 'inventory': inventory.id, 'playbook': 'helloworld.yml', }, 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 -# 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 def test_extra_credential_creation(get, post, organization_factory, job_template_factory, credentialtype_aws): 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 -# 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.parametrize( "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 -@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 def test_jt_without_project(inventory): data = dict(name="Test", job_type="run", diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index 362fcb924a..4180647d44 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -65,7 +65,7 @@ class TestJobTemplateCopyEdit: return objects.job_template def fake_context(self, user): - request = RequestFactory().get('/api/v1/resource/42/') + request = RequestFactory().get('/api/v2/resource/42/') request.user = user class FakeView(object): @@ -151,7 +151,7 @@ def mock_access_method(mocker): class TestAccessListCapabilities: """ 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 """ diff --git a/awx/main/tests/functional/api/test_workflow_node.py b/awx/main/tests/functional/api/test_workflow_node.py index 3e8a756e48..6de3e5b533 100644 --- a/awx/main/tests/functional/api/test_workflow_node.py +++ b/awx/main/tests/functional/api/test_workflow_node.py @@ -55,7 +55,7 @@ def test_node_rejects_unprompted_fields(inventory, project, workflow_job_templat ask_limit_on_launch = False ) 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'}, user=admin_user, expect=400) 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 ) 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'}, user=admin_user, expect=201) diff --git a/awx/main/tests/functional/models/test_unified_job.py b/awx/main/tests/functional/models/test_unified_job.py index b9bb3df523..402e2ac1f6 100644 --- a/awx/main/tests/functional/models/test_unified_job.py +++ b/awx/main/tests/functional/models/test_unified_job.py @@ -63,7 +63,10 @@ class TestCreateUnifiedJob: second_job = job_with_links.copy_unified_job() # 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.limit == 'my_server' assert net_credential in second_job.credentials.all() diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index 4683dcbfde..a131fc1a48 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -99,25 +99,6 @@ def test_default_cred_types(): 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 def test_credential_creation(organization_factory): org = organization_factory('test').organization @@ -141,7 +122,7 @@ def test_credential_creation(organization_factory): cred.full_clean() assert isinstance(cred, Credential) assert cred.name == "Bob's Credential" - assert cred.inputs['username'] == cred.username == 'bob' + assert cred.inputs['username'] == 'bob' @pytest.mark.django_db diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 558e1f41f6..39058d9d8a 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -1,10 +1,7 @@ from unittest import mock import pytest -from rest_framework.exceptions import PermissionDenied - from awx.api.versioning import reverse -from awx.api.serializers import JobTemplateSerializer from awx.main.access import ( BaseAccess, JobTemplateAccess, @@ -29,16 +26,18 @@ def test_job_template_access_superuser(check_license, user, deploy_jobtemplate): @pytest.mark.django_db 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) jt_linked.project.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 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(vault_credential=jt_linked.vault_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=vault_cred.pk, project=proj_pk)) for cred in jt_linked.credentials.all(): 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 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) jt_linked.project.use_role.members.add(rando) jt_linked.inventory.use_role.members.add(rando) - jt_linked.get_deprecated_credential('ssh').use_role.members.add(rando) - jt_linked.get_deprecated_credential('vault').use_role.members.add(rando) + ssh_cred.use_role.members.add(rando) + vault_cred.use_role.members.add(rando) proj_pk = jt_linked.project.pk assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk)) - assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk)) - assert access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk)) + assert access.can_add(dict(credential=ssh_cred.pk, project=proj_pk)) + assert access.can_add(dict(vault_credential=vault_cred.pk, project=proj_pk)) for cred in jt_linked.credentials.all(): 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.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): + ssh_cred = jt_linked.machine_credential + access = JobTemplateAccess(rando) # Appoint this user as admin of the organization #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 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 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(): 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) r = post( 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 @@ -126,57 +129,6 @@ class TestJobTemplateCredentials: assert JobTemplateAccess(rando).can_attach( 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 class TestOrphanJobTemplate: diff --git a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py index 437f6d9404..3e842a0598 100644 --- a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py @@ -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.main.access.JobTemplateAccess.can_change", return_value='foobar'): 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']['edit'] == 'foobar' diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 143e461daf..99136dcc63 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -688,13 +688,19 @@ class TestJobCredentials(TestJobExecution): job.websocket_emit_status = mock.Mock() 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(**{ 'all': lambda: job._credentials, 'add': job._credentials.append, - 'filter.return_value': mock.Mock( - __iter__ = lambda *args: iter(job._credentials), - first = lambda: job._credentials[0] - ), + 'filter.side_effect': _credentials_filter, 'prefetch_related': lambda _: credentials_mock, 'spec_set': ['all', 'add', 'filter', 'prefetch_related'], }) diff --git a/awx/main/tests/unit/test_views.py b/awx/main/tests/unit/test_views.py index 0454b23d7d..b9a96d4344 100644 --- a/awx/main/tests/unit/test_views.py +++ b/awx/main/tests/unit/test_views.py @@ -7,7 +7,7 @@ from rest_framework.generics import ListAPIView # AWX 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 @@ -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) -@pytest.mark.parametrize('version, supports_post', [(1, True), (2, False)]) -def test_disable_post_on_v2_jobs_list(version, supports_post): +def test_disable_post_on_v2_jobs_list(): job_list = JobList() job_list.request = mock.MagicMock() - with mock.patch('awx.api.views.get_request_version', return_value=version): - 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 + assert ('POST' in job_list.allowed_methods) is False def test_views_have_search_fields(all_views): diff --git a/awx/ui/context_processors.py b/awx/ui/context_processors.py index fb231f8c9b..38976a6eaa 100644 --- a/awx/ui/context_processors.py +++ b/awx/ui/context_processors.py @@ -22,5 +22,5 @@ def version(request): context.get('view'), 'deprecated', False - ) or request.path.startswith('/api/v1/') + ) } diff --git a/docs/custom_credential_types.md b/docs/custom_credential_types.md index 64c1143aa2..33cb903b78 100644 --- a/docs/custom_credential_types.md +++ b/docs/custom_credential_types.md @@ -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: , - vault_credential: , - cloud_credential: , - network_credential: , - } - - ...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 ------------------- * Rackspace is being removed from official support in Tower 3.2. Pre-existing diff --git a/docs/notification_system.md b/docs/notification_system.md index e3819f44ef..1415f30c96 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -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//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 The currently defined Notification Types are: diff --git a/tools/data_generators/rbac_dummy_data_generator.py b/tools/data_generators/rbac_dummy_data_generator.py index f6bf8124df..d88320904f 100755 --- a/tools/data_generators/rbac_dummy_data_generator.py +++ b/tools/data_generators/rbac_dummy_data_generator.py @@ -332,7 +332,7 @@ def make_the_data(): name='%s Credential %d User %d' % (prefix, credential_id, user_idx), defaults=dict(created_by=next(creator_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) credentials.append(credential) @@ -355,7 +355,7 @@ def make_the_data(): name='%s Credential %d team %d' % (prefix, credential_id, team_idx), defaults=dict(created_by=next(creator_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) credentials.append(credential) diff --git a/tools/docker-compose/README b/tools/docker-compose/README index 118fe9dad0..96510937ce 100644 --- a/tools/docker-compose/README +++ b/tools/docker-compose/README @@ -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 -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). ``` diff --git a/tools/elastic/README.md b/tools/elastic/README.md index 7681acae69..eb9d4f6531 100644 --- a/tools/elastic/README.md +++ b/tools/elastic/README.md @@ -95,7 +95,7 @@ and } ``` 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 diff --git a/tools/scripts/launch_job.py b/tools/scripts/launch_job.py deleted file mode 100755 index aa9136c639..0000000000 --- a/tools/scripts/launch_job.py +++ /dev/null @@ -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) From 2f57a1ea93cd2f805e552205f769d605ed8a9eee Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 6 Jun 2019 10:16:44 -0400 Subject: [PATCH 2/6] update inventory source credential lookup queries for api v2 --- .../sources/add/sources-add.controller.js | 47 ++++++++++++------- .../sources/edit/sources-edit.controller.js | 47 ++++++++++++------- .../lookup/sources-lookup-credential.route.js | 2 +- 3 files changed, 59 insertions(+), 37 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js index 72bdc53d5e..9e254fd7e5 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js @@ -133,25 +133,36 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars }); $scope.lookupCredential = function(){ - if($scope.source.value !== "scm" && $scope.source.value !== "custom") { - let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; - $state.go('.credential', { - credential_search: { - kind: kind, - page_size: '5', - page: '1' - } - }); + // For most source type selections, we filter for 1-1 matches to credential_type namespace. + let searchKey = 'credential_type__namespace'; + let searchValue = $scope.source.value; + + // SCM and custom source types are more generic in terms of the credentials they + // accept - any cloud or user-defined credential type can be used. We filter for + // 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', { - credential_search: { - credential_type__kind: "cloud", - page_size: '5', - page: '1' - } - }); + + if ($scope.source.value === 'custom') { + searchKey = 'credential_type__kind'; + searchValue = 'cloud'; } + + // 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(){ @@ -169,7 +180,7 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; } 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") { $scope.envParseType = 'yaml'; diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js index 33fa44f301..6096e1c84c 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js @@ -302,25 +302,36 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', }; $scope.lookupCredential = function(){ - if($scope.source.value !== "scm" && $scope.source.value !== "custom") { - let kind = ($scope.source.value === "ec2") ? "aws" : $scope.source.value; - $state.go('.credential', { - credential_search: { - kind: kind, - page_size: '5', - page: '1' - } - }); + // For most source type selections, we filter for 1-1 matches to credential_type namespace. + let searchKey = 'credential_type__namespace'; + let searchValue = $scope.source.value; + + // SCM and custom source types are more generic in terms of the credentials they + // accept - any cloud or user-defined credential type can be used. We filter for + // 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', { - credential_search: { - credential_type__kind: "cloud", - page_size: '5', - page: '1' - } - }); + + if ($scope.source.value === 'custom') { + searchKey = 'credential_type__kind'; + searchValue = 'cloud'; } + + // 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() { @@ -384,7 +395,7 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; } 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") { $scope.envParseType = 'yaml'; diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js index 90879be5f2..a61e96725e 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-credential.route.js @@ -39,7 +39,7 @@ export default { Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$transition$', (list, qs, $stateParams, GetBasePath, $transition$) => { 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); return qs.search(GetBasePath('credentials'), $stateParams[`${list.iterator}_search`]); } From 7a0a2fb54c8ca7a61b13a1ee6ad78b91d510cf32 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 6 Jun 2019 10:20:59 -0400 Subject: [PATCH 3/6] update adhoc command queries for api v2 --- .../src/inventories-hosts/inventories/adhoc/adhoc.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js index 45e18e5f38..f4cc2b447f 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js @@ -28,7 +28,7 @@ function adhocController($q, $scope, $stateParams, return { adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/', inventoryUrl: GetBasePath('inventory') + id + '/', - machineCredentialUrl: GetBasePath('credentials') + '?kind=ssh' + machineCredentialUrl: GetBasePath('credentials') + '?credential_type__namespace=ssh' }; }; From 5987aafb8228ad43257f43971f428ca583cadd92 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 6 Jun 2019 11:04:23 -0400 Subject: [PATCH 4/6] update shared client code for api v2 --- awx/ui/client/src/rest/restServices.factory.js | 2 +- awx/ui/client/src/shared/directives.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/rest/restServices.factory.js b/awx/ui/client/src/rest/restServices.factory.js index e562b68b26..b65a9a1c96 100644 --- a/awx/ui/client/src/rest/restServices.factory.js +++ b/awx/ui/client/src/rest/restServices.factory.js @@ -20,7 +20,7 @@ * * ``` * /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 diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index 9f46002506..6cbd603e4a 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -654,13 +654,13 @@ function(SettingsUtils, i18n, $rootScope) { else { switch(base) { case 'credential': - query += '&kind=ssh&role_level=use_role'; + query += '&credential_type__namespace=ssh&role_level=use_role'; break; case 'scm_credential': - query += '&kind=scm&role_level=use_role'; + query += '&redential_type__namespace=scm&role_level=use_role'; break; case 'network_credential': - query += '&kind=net&role_level=use_role'; + query += '&redential_type__namespace=net&role_level=use_role'; break; case 'cloud_credential': query += '&cloud=true&role_level=use_role'; From 51d7de296f0367f42e982b965da2fe2e6dfc452d Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 6 Jun 2019 12:02:15 -0400 Subject: [PATCH 5/6] remove dead code related to v1 kind field Currently, the credentials list doesn't seem to be returning any options data for 'kind' so this code wasn't being reached. In the future api updates, we'll also be removing the 'kind' field from credentials in general. --- .../list/credentials-list.controller.js | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/awx/ui/client/src/credentials/list/credentials-list.controller.js b/awx/ui/client/src/credentials/list/credentials-list.controller.js index 8fad7b7a71..5fedf2f711 100644 --- a/awx/ui/client/src/credentials/list/credentials-list.controller.js +++ b/awx/ui/client/src/credentials/list/credentials-list.controller.js @@ -36,11 +36,6 @@ export default ['$scope', 'Rest', 'CredentialList', 'Prompt', 'ProcessErrors', ' $scope.$on(`${list.iterator}_options`, function(event, data){ $scope.options = data.data.actions.GET; - optionsRequestDataProcessing(); - }); - - $scope.$watchCollection(`${$scope.list.name}`, function() { - optionsRequestDataProcessing(); }); 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 => { Wait('start'); new Credential('get', credential.id) From 6bc5c4da7474bf069e0312aa14e62273eb5f5002 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 6 Jun 2019 11:33:47 -0400 Subject: [PATCH 6/6] include credential in inventory update detail summary --- awx/api/serializers.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 0614a52e5a..1a2eaaea2d 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2175,14 +2175,25 @@ class InventoryUpdateDetailSerializer(InventoryUpdateSerializer): def get_summary_fields(self, 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'] = {} for field in SUMMARIZABLE_FK_FIELDS['project']: - value = getattr(summary_obj, field, None) + value = getattr(source_project, field, None) if value is not None: 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