From b5728fc548faf194a87c886112a5c0a932eadfd9 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 28 May 2019 13:52:25 -0400 Subject: [PATCH 01/11] Bump Django Rest Framework from 3.7.7 to 3.9.4 --- requirements/requirements.in | 2 +- requirements/requirements.txt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index c7736c0418..3635efa7b5 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -20,7 +20,7 @@ django-radius==1.3.3 django-solo==1.1.3 django-split-settings==0.3.0 django-taggit==0.22.2 -djangorestframework==3.7.7 +djangorestframework==3.9.4 djangorestframework-yaml==1.0.3 irc==16.2 jinja2==2.10.1 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 23b099e65d..2f51d67cbd 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -13,9 +13,9 @@ asgi-amqp==1.1.3 asgiref==1.1.2 # via asgi-amqp, channels, daphne asn1crypto==0.24.0 # via cryptography attrs==19.1.0 # via automat, service-identity, twisted -autobahn==19.3.3 # via daphne +autobahn==19.5.1 # via daphne automat==0.7.0 # via twisted -azure-common==1.1.20 # via azure-keyvault +azure-common==1.1.21 # via azure-keyvault azure-keyvault==1.1.0 azure-nspkg==3.0.2 # via azure-keyvault billiard==3.6.0.0 # via celery @@ -43,7 +43,7 @@ django-split-settings==0.3.0 django-taggit==0.22.2 django==1.11.20 djangorestframework-yaml==1.0.3 -djangorestframework==3.7.7 +djangorestframework==3.9.4 future==0.16.0 # via django-radius hyperlink==19.0.0 # via twisted idna==2.8 # via hyperlink, requests, twisted @@ -59,7 +59,7 @@ jaraco.logging==2.0 # via irc jaraco.stream==2.0 # via irc jaraco.text==3.0 # via irc, jaraco.collections jinja2==2.10.1 -jsonpickle==1.1 # via asgi-amqp +jsonpickle==1.2 # via asgi-amqp jsonschema==2.6.0 kombu==4.5.0 # via asgi-amqp, celery lockfile==0.12.2 # via python-daemon @@ -88,7 +88,7 @@ pyjwt==1.7.1 # via adal, social-auth-core, twilio pyopenssl==19.0.0 # via twisted pyparsing==2.2.0 pyrad==2.1 # via django-radius -pysocks==1.6.8 # via twilio +pysocks==1.7.0 # via twilio python-daemon==2.2.3 # via ansible-runner python-dateutil==2.7.2 python-ldap==3.2.0 # via django-auth-ldap From f94959d120c927446282390ac89b3f604ee7f637 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 28 May 2019 14:34:01 -0400 Subject: [PATCH 02/11] Remove the custom get_view_name function It appeared to not be doing anything that we were making use of that couldn't already be done, slightly differently, using DRF's built-in one. --- awx/api/generics.py | 18 -------- awx/api/views/__init__.py | 94 +++++++++++++++++++------------------- awx/api/views/inventory.py | 2 +- awx/api/views/metrics.py | 2 +- awx/api/views/root.py | 10 ++-- awx/conf/views.py | 6 +-- awx/main/views.py | 6 +-- awx/settings/defaults.py | 1 - 8 files changed, 59 insertions(+), 80 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 0a33b889cd..1ab4674b6d 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -119,24 +119,6 @@ class LoggedLogoutView(auth_views.LogoutView): return ret -def get_view_name(cls, suffix=None): - ''' - Wrapper around REST framework get_view_name() to support get_name() method - and view_name property on a view class. - ''' - name = '' - if hasattr(cls, 'get_name') and callable(cls.get_name): - name = cls().get_name() - elif hasattr(cls, 'view_name'): - if callable(cls.view_name): - name = cls.view_name() - else: - name = cls.view_name - if name: - return ('%s %s' % (name, suffix)) if suffix else name - return views.get_view_name(cls, suffix=None) - - def get_view_description(cls, request, html=False): ''' Wrapper around REST framework get_view_description() to support diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index c6f51ac493..f8cf12f670 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -37,7 +37,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer from rest_framework.response import Response from rest_framework.settings import api_settings -from rest_framework.views import exception_handler +from rest_framework.views import exception_handler, get_view_name from rest_framework import status # Django REST Framework YAML @@ -69,7 +69,7 @@ from awx.api.generics import ( RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, SimpleListAPIView, SubDetailAPIView, SubListAPIView, SubListAttachDetachAPIView, SubListCreateAPIView, SubListCreateAttachDetachAPIView, - SubListDestroyAPIView, get_view_name + SubListDestroyAPIView ) from awx.api.versioning import reverse from awx.conf.license import get_license @@ -168,7 +168,7 @@ class DashboardView(APIView): deprecated = True - view_name = _("Dashboard") + name = _("Dashboard") swagger_topic = 'Dashboard' def get(self, request, format=None): @@ -270,7 +270,7 @@ class DashboardView(APIView): class DashboardJobsGraphView(APIView): - view_name = _("Dashboard Jobs Graphs") + name = _("Dashboard Jobs Graphs") swagger_topic = 'Jobs' def get(self, request, format=None): @@ -320,7 +320,7 @@ class DashboardJobsGraphView(APIView): class InstanceList(ListAPIView): - view_name = _("Instances") + name = _("Instances") model = models.Instance serializer_class = serializers.InstanceSerializer search_fields = ('hostname',) @@ -328,7 +328,7 @@ class InstanceList(ListAPIView): class InstanceDetail(RetrieveUpdateAPIView): - view_name = _("Instance Detail") + name = _("Instance Detail") model = models.Instance serializer_class = serializers.InstanceSerializer @@ -345,7 +345,7 @@ class InstanceDetail(RetrieveUpdateAPIView): class InstanceUnifiedJobsList(SubListAPIView): - view_name = _("Instance Jobs") + name = _("Instance Jobs") model = models.UnifiedJob serializer_class = serializers.UnifiedJobListSerializer parent_model = models.Instance @@ -359,7 +359,7 @@ class InstanceUnifiedJobsList(SubListAPIView): class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAttachDetachAPIView): - view_name = _("Instance's Instance Groups") + name = _("Instance's Instance Groups") model = models.InstanceGroup serializer_class = serializers.InstanceGroupSerializer parent_model = models.Instance @@ -368,7 +368,7 @@ class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAtta class InstanceGroupList(ListCreateAPIView): - view_name = _("Instance Groups") + name = _("Instance Groups") model = models.InstanceGroup serializer_class = serializers.InstanceGroupSerializer @@ -376,7 +376,7 @@ class InstanceGroupList(ListCreateAPIView): class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): always_allow_superuser = False - view_name = _("Instance Group Detail") + name = _("Instance Group Detail") model = models.InstanceGroup serializer_class = serializers.InstanceGroupSerializer permission_classes = (InstanceGroupTowerPermission,) @@ -392,7 +392,7 @@ class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAP class InstanceGroupUnifiedJobsList(SubListAPIView): - view_name = _("Instance Group Running Jobs") + name = _("Instance Group Running Jobs") model = models.UnifiedJob serializer_class = serializers.UnifiedJobListSerializer parent_model = models.InstanceGroup @@ -401,7 +401,7 @@ class InstanceGroupUnifiedJobsList(SubListAPIView): class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetachAPIView): - view_name = _("Instance Group's Instances") + name = _("Instance Group's Instances") model = models.Instance serializer_class = serializers.InstanceSerializer parent_model = models.InstanceGroup @@ -411,7 +411,7 @@ class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetac class ScheduleList(ListCreateAPIView): - view_name = _("Schedules") + name = _("Schedules") model = models.Schedule serializer_class = serializers.ScheduleSerializer @@ -425,7 +425,7 @@ class ScheduleDetail(RetrieveUpdateDestroyAPIView): class SchedulePreview(GenericAPIView): model = models.Schedule - view_name = _('Schedule Recurrence Rule Preview') + name = _('Schedule Recurrence Rule Preview') serializer_class = serializers.SchedulePreviewSerializer permission_classes = (IsAuthenticated,) @@ -508,7 +508,7 @@ class ScheduleUnifiedJobsList(SubListAPIView): serializer_class = serializers.UnifiedJobListSerializer parent_model = models.Schedule relationship = 'unifiedjob_set' - view_name = _('Schedule Jobs List') + name = _('Schedule Jobs List') class AuthView(APIView): @@ -704,7 +704,7 @@ class ProjectTeamsList(ListAPIView): class ProjectSchedulesList(SubListCreateAPIView): - view_name = _("Project Schedules") + name = _("Project Schedules") model = models.Schedule serializer_class = serializers.ScheduleSerializer @@ -715,7 +715,7 @@ class ProjectSchedulesList(SubListCreateAPIView): class ProjectScmInventorySources(SubListAPIView): - view_name = _("Project SCM Inventory Sources") + name = _("Project SCM Inventory Sources") model = models.InventorySource serializer_class = serializers.InventorySourceSerializer parent_model = models.Project @@ -818,7 +818,7 @@ class ProjectUpdateEventsList(SubListAPIView): serializer_class = serializers.ProjectUpdateEventSerializer parent_model = models.ProjectUpdate relationship = 'project_update_events' - view_name = _('Project Update Events List') + name = _('Project Update Events List') search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): @@ -832,7 +832,7 @@ class SystemJobEventsList(SubListAPIView): serializer_class = serializers.SystemJobEventSerializer parent_model = models.SystemJob relationship = 'system_job_events' - view_name = _('System Job Events List') + name = _('System Job Events List') search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): @@ -868,7 +868,7 @@ class ProjectUpdateNotificationsList(SubListAPIView): class ProjectUpdateScmInventoryUpdates(SubListAPIView): - view_name = _("Project Update SCM Inventory Updates") + name = _("Project Update SCM Inventory Updates") model = models.InventoryUpdate serializer_class = serializers.InventoryUpdateListSerializer parent_model = models.ProjectUpdate @@ -912,7 +912,7 @@ class UserMeList(ListAPIView): model = models.User serializer_class = serializers.UserSerializer - view_name = _('Me') + name = _('Me') def get_queryset(self): return self.model.objects.filter(pk=self.request.user.pk) @@ -920,7 +920,7 @@ class UserMeList(ListAPIView): class OAuth2ApplicationList(ListCreateAPIView): - view_name = _("OAuth 2 Applications") + name = _("OAuth 2 Applications") model = models.OAuth2Application serializer_class = serializers.OAuth2ApplicationSerializer @@ -929,7 +929,7 @@ class OAuth2ApplicationList(ListCreateAPIView): class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView): - view_name = _("OAuth 2 Application Detail") + name = _("OAuth 2 Application Detail") model = models.OAuth2Application serializer_class = serializers.OAuth2ApplicationSerializer @@ -942,7 +942,7 @@ class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView): class ApplicationOAuth2TokenList(SubListCreateAPIView): - view_name = _("OAuth 2 Application Tokens") + name = _("OAuth 2 Application Tokens") model = models.OAuth2AccessToken serializer_class = serializers.OAuth2TokenSerializer @@ -964,7 +964,7 @@ class OAuth2ApplicationActivityStreamList(SubListAPIView): class OAuth2TokenList(ListCreateAPIView): - view_name = _("OAuth2 Tokens") + name = _("OAuth2 Tokens") model = models.OAuth2AccessToken serializer_class = serializers.OAuth2TokenSerializer @@ -973,7 +973,7 @@ class OAuth2TokenList(ListCreateAPIView): class OAuth2UserTokenList(SubListCreateAPIView): - view_name = _("OAuth2 User Tokens") + name = _("OAuth2 User Tokens") model = models.OAuth2AccessToken serializer_class = serializers.OAuth2TokenSerializer @@ -985,7 +985,7 @@ class OAuth2UserTokenList(SubListCreateAPIView): class UserAuthorizedTokenList(SubListCreateAPIView): - view_name = _("OAuth2 User Authorized Access Tokens") + name = _("OAuth2 User Authorized Access Tokens") model = models.OAuth2AccessToken serializer_class = serializers.UserAuthorizedTokenSerializer @@ -1000,7 +1000,7 @@ class UserAuthorizedTokenList(SubListCreateAPIView): class OrganizationApplicationList(SubListCreateAPIView): - view_name = _("Organization OAuth2 Applications") + name = _("Organization OAuth2 Applications") model = models.OAuth2Application serializer_class = serializers.OAuth2ApplicationSerializer @@ -1012,7 +1012,7 @@ class OrganizationApplicationList(SubListCreateAPIView): class UserPersonalTokenList(SubListCreateAPIView): - view_name = _("OAuth2 Personal Access Tokens") + name = _("OAuth2 Personal Access Tokens") model = models.OAuth2AccessToken serializer_class = serializers.UserPersonalTokenSerializer @@ -1027,7 +1027,7 @@ class UserPersonalTokenList(SubListCreateAPIView): class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView): - view_name = _("OAuth Token Detail") + name = _("OAuth Token Detail") model = models.OAuth2AccessToken serializer_class = serializers.OAuth2TokenDetailSerializer @@ -1373,7 +1373,7 @@ class CredentialExternalTest(SubDetailAPIView): before saving them. """ - view_name = _('External Credential Test') + name = _('External Credential Test') model = models.Credential serializer_class = serializers.EmptySerializer @@ -1399,7 +1399,7 @@ class CredentialExternalTest(SubDetailAPIView): class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView): - view_name = _("Credential Input Source Detail") + name = _("Credential Input Source Detail") model = models.CredentialInputSource serializer_class = serializers.CredentialInputSourceSerializer @@ -1407,7 +1407,7 @@ class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView): class CredentialInputSourceList(ListCreateAPIView): - view_name = _("Credential Input Sources") + name = _("Credential Input Sources") model = models.CredentialInputSource serializer_class = serializers.CredentialInputSourceSerializer @@ -1415,7 +1415,7 @@ class CredentialInputSourceList(ListCreateAPIView): class CredentialInputSourceSubList(SubListCreateAPIView): - view_name = _("Credential Input Sources") + name = _("Credential Input Sources") model = models.CredentialInputSource serializer_class = serializers.CredentialInputSourceSerializer @@ -1430,7 +1430,7 @@ class CredentialTypeExternalTest(SubDetailAPIView): saving it. """ - view_name = _('External Credential Type Test') + name = _('External Credential Type Test') model = models.CredentialType serializer_class = serializers.EmptySerializer @@ -2004,7 +2004,7 @@ class InventoryTreeView(RetrieveAPIView): class InventoryInventorySourcesList(SubListCreateAPIView): - view_name = _('Inventory Source List') + name = _('Inventory Source List') model = models.InventorySource serializer_class = serializers.InventorySourceSerializer @@ -2016,7 +2016,7 @@ class InventoryInventorySourcesList(SubListCreateAPIView): class InventoryInventorySourcesUpdate(RetrieveAPIView): - view_name = _('Inventory Sources Update') + name = _('Inventory Sources Update') model = models.Inventory obj_permission_type = 'start' @@ -2079,7 +2079,7 @@ class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroy class InventorySourceSchedulesList(SubListCreateAPIView): - view_name = _("Inventory Source Schedules") + name = _("Inventory Source Schedules") model = models.Schedule serializer_class = serializers.ScheduleSerializer @@ -2448,7 +2448,7 @@ class JobTemplateLaunch(RetrieveAPIView): class JobTemplateSchedulesList(SubListCreateAPIView): - view_name = _("Job Template Schedules") + name = _("Job Template Schedules") model = models.Schedule serializer_class = serializers.ScheduleSerializer @@ -3220,7 +3220,7 @@ class WorkflowJobTemplateJobsList(SubListAPIView): class WorkflowJobTemplateSchedulesList(SubListCreateAPIView): - view_name = _("Workflow Job Template Schedules") + name = _("Workflow Job Template Schedules") model = models.Schedule serializer_class = serializers.ScheduleSerializer @@ -3388,7 +3388,7 @@ class SystemJobTemplateLaunch(GenericAPIView): class SystemJobTemplateSchedulesList(SubListCreateAPIView): - view_name = _("System Job Template Schedules") + name = _("System Job Template Schedules") model = models.Schedule serializer_class = serializers.ScheduleSerializer @@ -3659,7 +3659,7 @@ class BaseJobHostSummariesList(SubListAPIView): serializer_class = serializers.JobHostSummarySerializer parent_model = None # Subclasses must define this attribute. relationship = 'job_host_summaries' - view_name = _('Job Host Summaries List') + name = _('Job Host Summaries List') search_fields = ('host_name',) def get_queryset(self): @@ -3708,7 +3708,7 @@ class JobEventChildrenList(SubListAPIView): serializer_class = serializers.JobEventSerializer parent_model = models.JobEvent relationship = 'children' - view_name = _('Job Event Children List') + name = _('Job Event Children List') search_fields = ('stdout',) def get_queryset(self): @@ -3724,7 +3724,7 @@ class JobEventHostsList(HostRelatedSearchMixin, SubListAPIView): serializer_class = serializers.HostSerializer parent_model = models.JobEvent relationship = 'hosts' - view_name = _('Job Event Hosts List') + name = _('Job Event Hosts List') class BaseJobEventsList(SubListAPIView): @@ -3733,7 +3733,7 @@ class BaseJobEventsList(SubListAPIView): serializer_class = serializers.JobEventSerializer parent_model = None # Subclasses must define this attribute. relationship = 'job_events' - view_name = _('Job Events List') + name = _('Job Events List') search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): @@ -3942,7 +3942,7 @@ class BaseAdHocCommandEventsList(SubListAPIView): serializer_class = serializers.AdHocCommandEventSerializer parent_model = None # Subclasses must define this attribute. relationship = 'ad_hoc_command_events' - view_name = _('Ad Hoc Command Events List') + name = _('Ad Hoc Command Events List') search_fields = ('stdout',) @@ -4195,7 +4195,7 @@ class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView): class NotificationTemplateTest(GenericAPIView): '''Test a Notification Template''' - view_name = _('Notification Template Test') + name = _('Notification Template Test') model = models.NotificationTemplate obj_permission_type = 'start' serializer_class = serializers.EmptySerializer diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index 15daa55232..dfa3f6627a 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -60,7 +60,7 @@ class InventoryUpdateEventsList(SubListAPIView): serializer_class = InventoryUpdateEventSerializer parent_model = InventoryUpdate relationship = 'inventory_update_events' - view_name = _('Inventory Update Events List') + name = _('Inventory Update Events List') search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): diff --git a/awx/api/views/metrics.py b/awx/api/views/metrics.py index 5d60747260..2c15c4e8c8 100644 --- a/awx/api/views/metrics.py +++ b/awx/api/views/metrics.py @@ -27,7 +27,7 @@ logger = logging.getLogger('awx.main.analytics') class MetricsView(APIView): - view_name = _('Metrics') + name = _('Metrics') swagger_topic = 'Metrics' renderer_classes = [renderers.PlainTextRenderer, diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 2e4312d613..50e7ada0d6 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -42,7 +42,7 @@ logger = logging.getLogger('awx.api.views.root') class ApiRootView(APIView): permission_classes = (AllowAny,) - view_name = _('REST API') + name = _('REST API') versioning_class = None swagger_topic = 'Versioning' @@ -64,7 +64,7 @@ class ApiRootView(APIView): class ApiOAuthAuthorizationRootView(APIView): permission_classes = (AllowAny,) - view_name = _("API OAuth 2 Authorization Root") + name = _("API OAuth 2 Authorization Root") versioning_class = None swagger_topic = 'Authentication' @@ -130,7 +130,7 @@ class ApiVersionRootView(APIView): class ApiV2RootView(ApiVersionRootView): - view_name = _('Version 2') + name = _('Version 2') class ApiV2PingView(APIView): @@ -139,7 +139,7 @@ class ApiV2PingView(APIView): """ permission_classes = (AllowAny,) authentication_classes = () - view_name = _('Ping') + name = _('Ping') swagger_topic = 'System Configuration' def get(self, request, format=None): @@ -171,7 +171,7 @@ class ApiV2PingView(APIView): class ApiV2ConfigView(APIView): permission_classes = (IsAuthenticated,) - view_name = _('Configuration') + name = _('Configuration') swagger_topic = 'System Configuration' def check_permissions(self, request): diff --git a/awx/conf/views.py b/awx/conf/views.py index bab468e0eb..13b72a926f 100644 --- a/awx/conf/views.py +++ b/awx/conf/views.py @@ -41,7 +41,7 @@ class SettingCategoryList(ListAPIView): model = Setting # Not exactly, but needed for the view. serializer_class = SettingCategorySerializer filter_backends = [] - view_name = _('Setting Categories') + name = _('Setting Categories') def get_queryset(self): setting_categories = [] @@ -63,7 +63,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView): model = Setting # Not exactly, but needed for the view. serializer_class = SettingSingletonSerializer filter_backends = [] - view_name = _('Setting Detail') + name = _('Setting Detail') def get_queryset(self): self.category_slug = self.kwargs.get('category_slug', 'all') @@ -154,7 +154,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView): class SettingLoggingTest(GenericAPIView): - view_name = _('Logging Connectivity Test') + name = _('Logging Connectivity Test') model = Setting serializer_class = SettingSingletonSerializer permission_classes = (IsSuperUser,) diff --git a/awx/main/views.py b/awx/main/views.py index c8bcfa304f..6cf27c0476 100644 --- a/awx/main/views.py +++ b/awx/main/views.py @@ -20,10 +20,8 @@ class ApiErrorView(views.APIView): permission_classes = (permissions.AllowAny,) metadata_class = None exception_class = exceptions.APIException - view_name = _('API Error') - def get_view_name(self): - return self.view_name + name = _('API Error') def finalize_response(self, request, response, *args, **kwargs): response = super(ApiErrorView, self).finalize_response(request, response, *args, **kwargs) @@ -46,7 +44,7 @@ def handle_error(request, status=404, **kwargs): class APIException(exceptions.APIException): status_code = status default_detail = kwargs['content'] - api_error_view = ApiErrorView.as_view(view_name=kwargs['name'], exception_class=APIException) + api_error_view = ApiErrorView.as_view(exception_class=APIException) response = api_error_view(request) if hasattr(response, 'render'): response.render() diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 8341875aa5..b577e86ae5 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -307,7 +307,6 @@ REST_FRAMEWORK = { ), 'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata', 'EXCEPTION_HANDLER': 'awx.api.views.api_exception_handler', - 'VIEW_NAME_FUNCTION': 'awx.api.generics.get_view_name', 'VIEW_DESCRIPTION_FUNCTION': 'awx.api.generics.get_view_description', 'NON_FIELD_ERRORS_KEY': '__all__', 'DEFAULT_VERSION': 'v2', From ed7a7e5f7bd507e7637adaa9e6345ffc6a09228e Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 28 May 2019 14:46:14 -0400 Subject: [PATCH 03/11] Support parse_requirements out of pip < 10 and >= 10 --- awx/main/tests/functional/test_licenses.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/functional/test_licenses.py b/awx/main/tests/functional/test_licenses.py index ec4da1ad91..6c34321f8d 100644 --- a/awx/main/tests/functional/test_licenses.py +++ b/awx/main/tests/functional/test_licenses.py @@ -4,7 +4,11 @@ import json import os from django.conf import settings -from pip._internal.req import parse_requirements + +try: + from pip._internal.req import parse_requirements +except ImportError: + from pip.req import parse_requirements def test_python_and_js_licenses(): From 76d4de24dff90babefa5368abc26b4da793303ce Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 28 May 2019 15:11:16 -0400 Subject: [PATCH 04/11] Handle a change in the error message for BooleanField related encode/django-rest-framework#5881 --- awx/sso/tests/unit/test_fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/sso/tests/unit/test_fields.py b/awx/sso/tests/unit/test_fields.py index fbd3a04781..18afab5d31 100644 --- a/awx/sso/tests/unit/test_fields.py +++ b/awx/sso/tests/unit/test_fields.py @@ -33,7 +33,7 @@ class TestSAMLOrgAttrField(): @pytest.mark.parametrize("data, expected", [ ({'remove': 'blah', 'saml_attr': 'foobar'}, - ValidationError('"blah" is not a valid boolean.')), + ValidationError('Must be a valid boolean.')), ({'remove': True, 'saml_attr': False}, ValidationError('Not a valid string.')), ({'remove': True, 'saml_attr': False, 'foo': 'bar', 'gig': 'ity'}, @@ -41,7 +41,7 @@ class TestSAMLOrgAttrField(): ({'remove_admins': True, 'saml_admin_attr': False}, ValidationError('Not a valid string.')), ({'remove_admins': 'blah', 'saml_admin_attr': 'foobar'}, - ValidationError('"blah" is not a valid boolean.')), + ValidationError('Must be a valid boolean.')), ]) def test_internal_value_invalid(self, data, expected): field = SAMLOrgAttrField() From 2a81643308921d75ec8ffc54bbca27b1e6bbf227 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Thu, 30 May 2019 18:17:37 -0400 Subject: [PATCH 05/11] Refactor the SSO serializer fields to follow the DRF idioms more closely and fix the tests to handle the newer nested validation checks properly. --- awx/sso/fields.py | 313 ++++++++++++++---------------- awx/sso/tests/unit/test_fields.py | 38 ++-- 2 files changed, 167 insertions(+), 184 deletions(-) diff --git a/awx/sso/fields.py b/awx/sso/fields.py index 8220feed5b..09b87fff02 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -1,4 +1,5 @@ import collections +import copy import inspect import json import re @@ -8,8 +9,8 @@ import ldap import awx # Django +from django.utils import six from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ValidationError # Django Auth LDAP import django_auth_ldap.config @@ -18,7 +19,8 @@ from django_auth_ldap.config import ( LDAPSearchUnion, ) -from rest_framework.fields import empty +from rest_framework.exceptions import ValidationError +from rest_framework.fields import empty, Field, SkipField # This must be imported so get_subclasses picks it up from awx.sso.ldap_group_types import PosixUIDGroupType # noqa @@ -74,6 +76,71 @@ class DependsOnMixin(): return res +class _Forbidden(Field): + default_error_messages = { + 'invalid': _('Invalid field.'), + } + + def run_validation(self, value): + self.fail('invalid') + + +class HybridDictField(fields.DictField): + """A DictField, but with defined fixed Fields for certain keys. + """ + + def __init__(self, *args, **kwargs): + self.allow_blank = kwargs.pop('allow_blank', False) + + fields = [ + (field_name, obj) + for field_name, obj in self.__class__.__dict__.items() + if isinstance(obj, Field) and field_name != 'child' + ] + fields.sort(key=lambda x: x[1]._creation_counter) + self._declared_fields = collections.OrderedDict(fields) + + super().__init__(*args, **kwargs) + + def to_representation(self, value): + fields = copy.deepcopy(self._declared_fields) + return { + key: field.to_representation(val) if val is not None else None + for key, val, field in ( + (six.text_type(key), val, fields.get(key, self.child)) + for key, val in value.items() + ) + if not field.write_only + } + + def run_child_validation(self, data): + result = {} + + if not data and self.allow_blank: + return result + + errors = collections.OrderedDict() + fields = copy.deepcopy(self._declared_fields) + keys = set(fields.keys()) | set(data.keys()) + + for key in keys: + value = data.get(key, empty) + key = six.text_type(key) + field = fields.get(key, self.child) + try: + if field.read_only: + continue # Ignore read_only fields, as Serializer seems to do. + result[key] = field.run_validation(value) + except ValidationError as e: + errors[key] = e.detail + except SkipField: + pass + + if not errors: + return result + raise ValidationError(errors) + + class AuthenticationBackendsField(fields.StringListField): # Mapping of settings that must be set in order to enable each @@ -459,70 +526,14 @@ class LDAPDNMapField(fields.StringListBooleanField): child = LDAPDNField() -class BaseDictWithChildField(fields.DictField): +class LDAPSingleOrganizationMapField(HybridDictField): - default_error_messages = { - 'missing_keys': _('Missing key(s): {missing_keys}.'), - 'invalid_keys': _('Invalid key(s): {invalid_keys}.'), - } - child_fields = { - # 'key': fields.ChildField(), - } - allow_unknown_keys = False + admins = LDAPDNMapField(allow_null=True, required=False) + users = LDAPDNMapField(allow_null=True, required=False) + remove_admins = fields.BooleanField(required=False) + remove_users = fields.BooleanField(required=False) - def __init__(self, *args, **kwargs): - self.allow_blank = kwargs.pop('allow_blank', False) - super(BaseDictWithChildField, self).__init__(*args, **kwargs) - - def to_representation(self, value): - value = super(BaseDictWithChildField, self).to_representation(value) - for k, v in value.items(): - child_field = self.child_fields.get(k, None) - if child_field: - value[k] = child_field.to_representation(v) - elif self.allow_unknown_keys: - value[k] = v - return value - - def to_internal_value(self, data): - data = super(BaseDictWithChildField, self).to_internal_value(data) - missing_keys = set() - for key, child_field in self.child_fields.items(): - if not child_field.required: - continue - elif key not in data: - missing_keys.add(key) - missing_keys = sorted(list(missing_keys)) - if missing_keys and (data or not self.allow_blank): - missing_keys = sorted(list(missing_keys)) - keys_display = json.dumps(missing_keys).lstrip('[').rstrip(']') - self.fail('missing_keys', missing_keys=keys_display) - if not self.allow_unknown_keys: - invalid_keys = set(data.keys()) - set(self.child_fields.keys()) - if invalid_keys: - invalid_keys = sorted(list(invalid_keys)) - keys_display = json.dumps(invalid_keys).lstrip('[').rstrip(']') - self.fail('invalid_keys', invalid_keys=keys_display) - for k, v in data.items(): - child_field = self.child_fields.get(k, None) - if child_field: - data[k] = child_field.run_validation(v) - elif self.allow_unknown_keys: - data[k] = v - return data - - -class LDAPSingleOrganizationMapField(BaseDictWithChildField): - - default_error_messages = { - 'invalid_keys': _('Invalid key(s) for organization map: {invalid_keys}.'), - } - child_fields = { - 'admins': LDAPDNMapField(allow_null=True, required=False), - 'users': LDAPDNMapField(allow_null=True, required=False), - 'remove_admins': fields.BooleanField(required=False), - 'remove_users': fields.BooleanField(required=False), - } + child = _Forbidden() class LDAPOrganizationMapField(fields.DictField): @@ -530,17 +541,13 @@ class LDAPOrganizationMapField(fields.DictField): child = LDAPSingleOrganizationMapField() -class LDAPSingleTeamMapField(BaseDictWithChildField): +class LDAPSingleTeamMapField(HybridDictField): - default_error_messages = { - 'missing_keys': _('Missing required key for team map: {invalid_keys}.'), - 'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'), - } - child_fields = { - 'organization': fields.CharField(), - 'users': LDAPDNMapField(allow_null=True, required=False), - 'remove': fields.BooleanField(required=False), - } + organization = fields.CharField() + users = LDAPDNMapField(allow_null=True, required=False) + remove = fields.BooleanField(required=False) + + child = _Forbidden() class LDAPTeamMapField(fields.DictField): @@ -614,17 +621,14 @@ class SocialMapField(fields.ListField): self.fail('type_error', input_type=type(data)) -class SocialSingleOrganizationMapField(BaseDictWithChildField): +class SocialSingleOrganizationMapField(HybridDictField): - default_error_messages = { - 'invalid_keys': _('Invalid key(s) for organization map: {invalid_keys}.'), - } - child_fields = { - 'admins': SocialMapField(allow_null=True, required=False), - 'users': SocialMapField(allow_null=True, required=False), - 'remove_admins': fields.BooleanField(required=False), - 'remove_users': fields.BooleanField(required=False), - } + admins = SocialMapField(allow_null=True, required=False) + users = SocialMapField(allow_null=True, required=False) + remove_admins = fields.BooleanField(required=False) + remove_users = fields.BooleanField(required=False) + + child = _Forbidden() class SocialOrganizationMapField(fields.DictField): @@ -632,17 +636,13 @@ class SocialOrganizationMapField(fields.DictField): child = SocialSingleOrganizationMapField() -class SocialSingleTeamMapField(BaseDictWithChildField): +class SocialSingleTeamMapField(HybridDictField): - default_error_messages = { - 'missing_keys': _('Missing required key for team map: {missing_keys}.'), - 'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'), - } - child_fields = { - 'organization': fields.CharField(), - 'users': SocialMapField(allow_null=True, required=False), - 'remove': fields.BooleanField(required=False), - } + organization = fields.CharField() + users = SocialMapField(allow_null=True, required=False) + remove = fields.BooleanField(required=False) + + child = _Forbidden() class SocialTeamMapField(fields.DictField): @@ -650,17 +650,11 @@ class SocialTeamMapField(fields.DictField): child = SocialSingleTeamMapField() -class SAMLOrgInfoValueField(BaseDictWithChildField): +class SAMLOrgInfoValueField(HybridDictField): - default_error_messages = { - 'missing_keys': _('Missing required key(s) for org info record: {missing_keys}.'), - } - child_fields = { - 'name': fields.CharField(), - 'displayname': fields.CharField(), - 'url': fields.URLField(), - } - allow_unknown_keys = True + name = fields.CharField() + displayname = fields.CharField() + url = fields.URLField() class SAMLOrgInfoField(fields.DictField): @@ -683,34 +677,22 @@ class SAMLOrgInfoField(fields.DictField): return data -class SAMLContactField(BaseDictWithChildField): +class SAMLContactField(HybridDictField): - default_error_messages = { - 'missing_keys': _('Missing required key(s) for contact: {missing_keys}.'), - } - child_fields = { - 'givenName': fields.CharField(), - 'emailAddress': fields.EmailField(), - } - allow_unknown_keys = True + givenName = fields.CharField() + emailAddress = fields.EmailField() -class SAMLIdPField(BaseDictWithChildField): +class SAMLIdPField(HybridDictField): - default_error_messages = { - 'missing_keys': _('Missing required key(s) for IdP: {missing_keys}.'), - } - child_fields = { - 'entity_id': fields.CharField(), - 'url': fields.URLField(), - 'x509cert': fields.CharField(validators=[validate_certificate]), - 'attr_user_permanent_id': fields.CharField(required=False), - 'attr_first_name': fields.CharField(required=False), - 'attr_last_name': fields.CharField(required=False), - 'attr_username': fields.CharField(required=False), - 'attr_email': fields.CharField(required=False), - } - allow_unknown_keys = True + entity_id = fields.CharField() + url = fields.URLField() + x509cert = fields.CharField(validators=[validate_certificate]) + attr_user_permanent_id = fields.CharField(required=False) + attr_first_name = fields.CharField(required=False) + attr_last_name = fields.CharField(required=False) + attr_username = fields.CharField(required=False) + attr_email = fields.CharField(required=False) class SAMLEnabledIdPsField(fields.DictField): @@ -718,52 +700,49 @@ class SAMLEnabledIdPsField(fields.DictField): child = SAMLIdPField() -class SAMLSecurityField(BaseDictWithChildField): +class SAMLSecurityField(HybridDictField): - child_fields = { - 'nameIdEncrypted': fields.BooleanField(required=False), - 'authnRequestsSigned': fields.BooleanField(required=False), - 'logoutRequestSigned': fields.BooleanField(required=False), - 'logoutResponseSigned': fields.BooleanField(required=False), - 'signMetadata': fields.BooleanField(required=False), - 'wantMessagesSigned': fields.BooleanField(required=False), - 'wantAssertionsSigned': fields.BooleanField(required=False), - 'wantAssertionsEncrypted': fields.BooleanField(required=False), - 'wantNameId': fields.BooleanField(required=False), - 'wantNameIdEncrypted': fields.BooleanField(required=False), - 'wantAttributeStatement': fields.BooleanField(required=False), - 'requestedAuthnContext': fields.StringListBooleanField(required=False), - 'requestedAuthnContextComparison': fields.CharField(required=False), - 'metadataValidUntil': fields.CharField(allow_null=True, required=False), - 'metadataCacheDuration': fields.CharField(allow_null=True, required=False), - 'signatureAlgorithm': fields.CharField(allow_null=True, required=False), - 'digestAlgorithm': fields.CharField(allow_null=True, required=False), - } - allow_unknown_keys = True + nameIdEncrypted = fields.BooleanField(required=False) + authnRequestsSigned = fields.BooleanField(required=False) + logoutRequestSigned = fields.BooleanField(required=False) + logoutResponseSigned = fields.BooleanField(required=False) + signMetadata = fields.BooleanField(required=False) + wantMessagesSigned = fields.BooleanField(required=False) + wantAssertionsSigned = fields.BooleanField(required=False) + wantAssertionsEncrypted = fields.BooleanField(required=False) + wantNameId = fields.BooleanField(required=False) + wantNameIdEncrypted = fields.BooleanField(required=False) + wantAttributeStatement = fields.BooleanField(required=False) + requestedAuthnContext = fields.StringListBooleanField(required=False) + requestedAuthnContextComparison = fields.CharField(required=False) + metadataValidUntil = fields.CharField(allow_null=True, required=False) + metadataCacheDuration = fields.CharField(allow_null=True, required=False) + signatureAlgorithm = fields.CharField(allow_null=True, required=False) + digestAlgorithm = fields.CharField(allow_null=True, required=False) -class SAMLOrgAttrField(BaseDictWithChildField): +class SAMLOrgAttrField(HybridDictField): - child_fields = { - 'remove': fields.BooleanField(required=False), - 'saml_attr': fields.CharField(required=False, allow_null=True), - 'remove_admins': fields.BooleanField(required=False), - 'saml_admin_attr': fields.CharField(required=False, allow_null=True), - } + remove = fields.BooleanField(required=False) + saml_attr = fields.CharField(required=False, allow_null=True) + remove_admins = fields.BooleanField(required=False) + saml_admin_attr = fields.CharField(required=False, allow_null=True) + + child = _Forbidden() -class SAMLTeamAttrTeamOrgMapField(BaseDictWithChildField): +class SAMLTeamAttrTeamOrgMapField(HybridDictField): - child_fields = { - 'team': fields.CharField(required=True, allow_null=False), - 'organization': fields.CharField(required=True, allow_null=False), - } + team = fields.CharField(required=True, allow_null=False) + organization = fields.CharField(required=True, allow_null=False) + + child = _Forbidden() -class SAMLTeamAttrField(BaseDictWithChildField): +class SAMLTeamAttrField(HybridDictField): - child_fields = { - 'team_org_map': fields.ListField(required=False, child=SAMLTeamAttrTeamOrgMapField(), allow_null=True), - 'remove': fields.BooleanField(required=False), - 'saml_attr': fields.CharField(required=False, allow_null=True), - } + team_org_map = fields.ListField(required=False, child=SAMLTeamAttrTeamOrgMapField(), allow_null=True) + remove = fields.BooleanField(required=False) + saml_attr = fields.CharField(required=False, allow_null=True) + + child = _Forbidden() diff --git a/awx/sso/tests/unit/test_fields.py b/awx/sso/tests/unit/test_fields.py index 18afab5d31..95cd774b98 100644 --- a/awx/sso/tests/unit/test_fields.py +++ b/awx/sso/tests/unit/test_fields.py @@ -33,21 +33,23 @@ class TestSAMLOrgAttrField(): @pytest.mark.parametrize("data, expected", [ ({'remove': 'blah', 'saml_attr': 'foobar'}, - ValidationError('Must be a valid boolean.')), + {'remove': ['Must be a valid boolean.']}), ({'remove': True, 'saml_attr': False}, - ValidationError('Not a valid string.')), + {'saml_attr': ['Not a valid string.']}), ({'remove': True, 'saml_attr': False, 'foo': 'bar', 'gig': 'ity'}, - ValidationError('Invalid key(s): "foo", "gig".')), + {'saml_attr': ['Not a valid string.'], + 'foo': ['Invalid field.'], + 'gig': ['Invalid field.']}), ({'remove_admins': True, 'saml_admin_attr': False}, - ValidationError('Not a valid string.')), + {'saml_admin_attr': ['Not a valid string.']}), ({'remove_admins': 'blah', 'saml_admin_attr': 'foobar'}, - ValidationError('Must be a valid boolean.')), + {'remove_admins': ['Must be a valid boolean.']}), ]) def test_internal_value_invalid(self, data, expected): field = SAMLOrgAttrField() - with pytest.raises(type(expected)) as e: + with pytest.raises(ValidationError) as e: field.to_internal_value(data) - assert str(e.value) == str(expected) + assert e.value.detail == expected class TestSAMLTeamAttrField(): @@ -77,36 +79,38 @@ class TestSAMLTeamAttrField(): @pytest.mark.parametrize("data, expected", [ ({'remove': True, 'saml_attr': 'foobar', 'team_org_map': [ {'team': 'foobar', 'not_a_valid_key': 'blah', 'organization': 'Ansible'}, - ]}, ValidationError('Invalid key(s): "not_a_valid_key".')), + ]}, {'team_org_map': {0: {'not_a_valid_key': ['Invalid field.']}}}), ({'remove': False, 'saml_attr': 'foobar', 'team_org_map': [ {'organization': 'Ansible'}, - ]}, ValidationError('Missing key(s): "team".')), + ]}, {'team_org_map': {0: {'team': ['This field is required.']}}}), ({'remove': False, 'saml_attr': 'foobar', 'team_org_map': [ {}, - ]}, ValidationError('Missing key(s): "organization", "team".')), + ]}, {'team_org_map': { + 0: {'organization': ['This field is required.'], + 'team': ['This field is required.']}}}), ]) def test_internal_value_invalid(self, data, expected): field = SAMLTeamAttrField() - with pytest.raises(type(expected)) as e: + with pytest.raises(ValidationError) as e: field.to_internal_value(data) - assert str(e.value) == str(expected) + assert e.value.detail == expected class TestLDAPGroupTypeParamsField(): @pytest.mark.parametrize("group_type, data, expected", [ ('LDAPGroupType', {'name_attr': 'user', 'bob': ['a', 'b'], 'scooter': 'hello'}, - ValidationError('Invalid key(s): "bob", "scooter".')), + ['Invalid key(s): "bob", "scooter".']), ('MemberDNGroupType', {'name_attr': 'user', 'member_attr': 'west', 'bob': ['a', 'b'], 'scooter': 'hello'}, - ValidationError('Invalid key(s): "bob", "scooter".')), + ['Invalid key(s): "bob", "scooter".']), ('PosixUIDGroupType', {'name_attr': 'user', 'member_attr': 'west', 'ldap_group_user_attr': 'legacyThing', 'bob': ['a', 'b'], 'scooter': 'hello'}, - ValidationError('Invalid key(s): "bob", "member_attr", "scooter".')), + ['Invalid key(s): "bob", "member_attr", "scooter".']), ]) def test_internal_value_invalid(self, group_type, data, expected): field = LDAPGroupTypeParamsField() field.get_depends_on = mock.MagicMock(return_value=group_type) - with pytest.raises(type(expected)) as e: + with pytest.raises(ValidationError) as e: field.to_internal_value(data) - assert str(e.value) == str(expected) + assert e.value.detail == expected From 34d76422d63fd1044a14a1d692c3096c13cdad0e Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 3 Jun 2019 16:11:34 -0400 Subject: [PATCH 06/11] Move the endpoint deprecation warning out of base.html This is part 1 of the removal of awx/templates/rest_framework/base.html. --- awx/templates/rest_framework/api.html | 9 +++++++++ awx/templates/rest_framework/base.html | 5 ----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/awx/templates/rest_framework/api.html b/awx/templates/rest_framework/api.html index eba78531cc..5d14fea16d 100644 --- a/awx/templates/rest_framework/api.html +++ b/awx/templates/rest_framework/api.html @@ -48,6 +48,15 @@ {% endblock %} +{% block content %} + {% if deprecated %} + + {% endif %} +{{ block.super }} +{% endblock content %} + {% block script %}