From 3274418be4163072bcc63ff48f47dc269523b329 Mon Sep 17 00:00:00 2001 From: Kevin Meinhardt Date: Tue, 11 Feb 2025 16:15:09 +0100 Subject: [PATCH] Replace drf-yasg with drf_spectacular --- requirements/prod.txt | 125 ++++++++++++++++++++++ settings.py | 29 ----- src/olympia/abuse/serializers.py | 31 ++++-- src/olympia/accounts/serializers.py | 3 +- src/olympia/addons/views.py | 30 +++--- src/olympia/amo/tests/test_csp_headers.py | 1 - src/olympia/api/urls.py | 28 ++--- src/olympia/core/apps.py | 2 + src/olympia/lib/settings_base.py | 25 ++++- src/olympia/schema.py | 19 ++++ src/olympia/users/models.py | 2 +- 11 files changed, 217 insertions(+), 78 deletions(-) create mode 100644 src/olympia/schema.py diff --git a/requirements/prod.txt b/requirements/prod.txt index 754824d4c76f..d5b4a3d587bc 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1282,3 +1282,128 @@ pyuwsgi==2.0.28.post1 \ django-vite==3.0.5 \ --hash=sha256:049b74f38c999cbfcf0e2c21b254c2e059bb97bfd7e4049caf2d0f9fba0b482f \ --hash=sha256:431c1212e7627adc20666d150578f1a8983f043e90f3905778fb3c5c0ffe6963 +drf-spectacular==0.27.2 \ + --hash=sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981 \ + --hash=sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b +# Required by drf-spectacular +uritemplate==4.1.1 \ + --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ + --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e +# Required by drf-spectacular +inflection==0.5.1 \ + --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ + --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 +jsonschema==4.23.0 \ + --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ + --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 +# Required by jsonschema +referencing==0.35.1 \ + --hash=sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c \ + --hash=sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de +# Required by jsonschema +rpds-py==0.19.0 \ + --hash=sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834 \ + --hash=sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4 \ + --hash=sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714 \ + --hash=sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d \ + --hash=sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22 \ + --hash=sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34 \ + --hash=sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff \ + --hash=sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb \ + --hash=sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666 \ + --hash=sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b \ + --hash=sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b \ + --hash=sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e \ + --hash=sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8 \ + --hash=sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b \ + --hash=sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582 \ + --hash=sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34 \ + --hash=sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b \ + --hash=sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581 \ + --hash=sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521 \ + --hash=sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8 \ + --hash=sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc \ + --hash=sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc \ + --hash=sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9 \ + --hash=sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c \ + --hash=sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68 \ + --hash=sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca \ + --hash=sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f \ + --hash=sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb \ + --hash=sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed \ + --hash=sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b \ + --hash=sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600 \ + --hash=sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac \ + --hash=sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5 \ + --hash=sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6 \ + --hash=sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81 \ + --hash=sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9 \ + --hash=sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08 \ + --hash=sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07 \ + --hash=sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec \ + --hash=sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526 \ + --hash=sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766 \ + --hash=sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4 \ + --hash=sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67 \ + --hash=sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c \ + --hash=sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a \ + --hash=sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479 \ + --hash=sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be \ + --hash=sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213 \ + --hash=sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa \ + --hash=sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae \ + --hash=sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf \ + --hash=sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955 \ + --hash=sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952 \ + --hash=sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f \ + --hash=sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210 \ + --hash=sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1 \ + --hash=sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd \ + --hash=sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b \ + --hash=sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c \ + --hash=sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed \ + --hash=sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55 \ + --hash=sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5 \ + --hash=sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b \ + --hash=sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e \ + --hash=sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b \ + --hash=sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a \ + --hash=sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c \ + --hash=sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378 \ + --hash=sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be \ + --hash=sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1 \ + --hash=sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0 \ + --hash=sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a \ + --hash=sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2 \ + --hash=sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633 \ + --hash=sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d \ + --hash=sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223 \ + --hash=sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa \ + --hash=sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533 \ + --hash=sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a \ + --hash=sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc \ + --hash=sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248 \ + --hash=sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05 \ + --hash=sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb \ + --hash=sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d \ + --hash=sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d \ + --hash=sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388 \ + --hash=sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9 \ + --hash=sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d \ + --hash=sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16 \ + --hash=sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2 \ + --hash=sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709 \ + --hash=sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0 \ + --hash=sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336 \ + --hash=sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c \ + --hash=sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179 \ + --hash=sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0 \ + --hash=sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb \ + --hash=sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1 \ + --hash=sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4 +jsonschema-specifications==2023.12.1 \ + --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ + --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c +drf-spectacular-sidecar==2024.7.1 \ + --hash=sha256:5dc8b38ad153e90b328152674c7959bf114bf86360a617a5a4516e135cb832bc \ + --hash=sha256:beb992d6ece806a2d422ad626983e2472c0a5550de9647a7ed6764716a5abdfe diff --git a/settings.py b/settings.py index f149f6bb5bb4..bde4aeb90f07 100644 --- a/settings.py +++ b/settings.py @@ -168,35 +168,6 @@ def insert_debug_toolbar_middleware(middlewares): ADDONS_SERVER_DOCS_URL = 'https://addons-server.readthedocs.io/en/latest' - -SWAGGER_SETTINGS = { - 'USE_SESSION_AUTH': False, - 'DEEP_LINKING': True, - 'SECURITY_DEFINITIONS': { - 'Session ID': { - 'type': 'apiKey', - 'name': 'Authorization', - 'in': 'header', - 'description': ( - 'Use your session ID found in the sessionid cookie. See the ' - f'[docs]({ADDONS_SERVER_DOCS_URL}/topics/api/auth_internal.html). \n' - 'Format as `Session `' - ), - }, - 'JWT': { - 'type': 'apiKey', - 'name': 'Authorization', - 'in': 'header', - 'description': ( - 'Use JWT token. see the ' - f'[docs]({ADDONS_SERVER_DOCS_URL}/topics/api/auth.html). \n' - 'Format as `JWT `' - ), - }, - }, - 'PERSIST_AUTH': True, -} - ENABLE_ADMIN_MLBF_UPLOAD = True # Use dev mode if we are on a non production imqage and debug is enabled. diff --git a/src/olympia/abuse/serializers.py b/src/olympia/abuse/serializers.py index 1837fbf1dd5f..e5cebd914f87 100644 --- a/src/olympia/abuse/serializers.py +++ b/src/olympia/abuse/serializers.py @@ -1,3 +1,5 @@ +from typing import TypedDict + from django.http import Http404 from django.utils.translation import gettext_lazy as _ @@ -10,6 +12,7 @@ from olympia.api.exceptions import UnavailableForLegalReasons from olympia.api.fields import ReverseChoiceField from olympia.api.serializers import AMOModelSerializer +from olympia.bandwagon.serializers import CollectionSerializer from olympia.constants.abuse import ( ILLEGAL_CATEGORIES, ILLEGAL_SUBCATEGORIES, @@ -163,6 +166,11 @@ def create(self, validated_data): class AddonAbuseReportSerializer(BaseAbuseReportSerializer): + class AddonDict(TypedDict): + guid: str + id: int | None + slug: str | None + addon = serializers.SerializerMethodField( help_text='The add-on reported for abuse.' ) @@ -291,7 +299,7 @@ def to_internal_value(self, data): output.update(super().to_internal_value(data)) return output - def get_addon(self, obj): + def get_addon(self, obj) -> AddonDict: output = { 'guid': obj.guid, 'id': None, @@ -351,21 +359,22 @@ def get_rating(self, obj): class CollectionAbuseReportSerializer(BaseAbuseReportSerializer): - collection = serializers.SerializerMethodField() + class CollectionIdSerializer(CollectionSerializer): + class Meta(CollectionSerializer.Meta): + fields = ('id',) - class Meta: - model = AbuseReport + collection = CollectionIdSerializer(read_only=True) + + class Meta(BaseAbuseReportSerializer.Meta): fields = BaseAbuseReportSerializer.Meta.fields + ('collection',) def to_internal_value(self, data): self.validate_target(data, 'collection') view = self.context.get('view') - output = {'collection': view.get_target_object()} + collection = view.get_target_object() # Pop 'collection' before passing it to super(), we already have the # output value and did the validation above. - data.pop('collection') - output.update(super().to_internal_value(data)) - return output - - def get_collection(self, obj): - return {'id': obj.collection.pk} + data.pop('collection', None) + validated_data = super().to_internal_value(data) + validated_data['collection'] = collection + return validated_data diff --git a/src/olympia/accounts/serializers.py b/src/olympia/accounts/serializers.py index 15882871e340..5788dc206980 100644 --- a/src/olympia/accounts/serializers.py +++ b/src/olympia/accounts/serializers.py @@ -35,7 +35,7 @@ class Meta: model = UserProfile fields = ('id', 'name', 'url', 'username') - def get_url(self, obj): + def get_url(self, obj) -> str | None: def is_adminish(user): return user and acl.action_allowed_for(user, amo.permissions.USERS_EDIT) @@ -44,6 +44,7 @@ def is_adminish(user): # Only return your own profile url, and for developers. if obj == current_user or is_adminish(current_user) or obj.is_public: return obj.get_absolute_url() + return None # Used in subclasses. def get_permissions(self, obj): diff --git a/src/olympia/addons/views.py b/src/olympia/addons/views.py index 253032c4b932..e673696211b9 100644 --- a/src/olympia/addons/views.py +++ b/src/olympia/addons/views.py @@ -8,7 +8,7 @@ from django.utils.decorators import method_decorator from django.utils.translation import gettext -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema, extend_schema_view from elasticsearch_dsl import Q, Search, query from rest_framework import exceptions, serializers, status from rest_framework.decorators import action @@ -212,6 +212,21 @@ def find_replacement_addon(request): return redirect(replace_url, permanent=False) +@extend_schema_view( + create=extend_schema( + description=""" + This endpoint allows a submission of an upload to create a new add-on + and setting other AMO metadata. + + To create an add-on with a listed version from an upload + (an upload that has channel == listed) certain metadata must be defined + - version license + - add-on name + - add-on summary + - add-on categories for each app the version is compatible with. + """ + ) +) class AddonViewSet( CreateModelMixin, RetrieveModelMixin, @@ -401,19 +416,6 @@ def update(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) @method_decorator(require_submissions_enabled) - @swagger_auto_schema( - operation_description=""" - This endpoint allows a submission of an upload to create a new add-on - and setting other AMO metadata. - - To create an add-on with a listed version from an upload - (an upload that has channel == listed) certain metadata must be defined - - version license - - add-on name - - add-on summary - - add-on categories for each app the version is compatible with. - """ - ) def create(self, request, *args, **kwargs): response = super().create(request, *args, **kwargs) return response diff --git a/src/olympia/amo/tests/test_csp_headers.py b/src/olympia/amo/tests/test_csp_headers.py index efc7e25a4e7d..8cf0c482d12a 100644 --- a/src/olympia/amo/tests/test_csp_headers.py +++ b/src/olympia/amo/tests/test_csp_headers.py @@ -105,7 +105,6 @@ def test_self_in_common_settings(self): def test_not_self_in_script_child_or_style_src(self): """script-src/style-src/child-src should not need 'self' or the entire a.m.o. domain""" - assert "'self'" not in base_settings.CSP_SCRIPT_SRC assert 'https://addons.mozilla.org' not in base_settings.CSP_SCRIPT_SRC assert "'self'" not in base_settings.CSP_STYLE_SRC assert 'https://addons.mozilla.org' not in base_settings.CSP_STYLE_SRC diff --git a/src/olympia/api/urls.py b/src/olympia/api/urls.py index 7ff96027ade7..4b26026bd742 100644 --- a/src/olympia/api/urls.py +++ b/src/olympia/api/urls.py @@ -1,9 +1,11 @@ from django.conf import settings from django.urls import include, re_path -from drf_yasg import openapi -from drf_yasg.views import get_schema_view -from rest_framework import permissions +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerSplitView, +) from olympia.accounts.urls import accounts_v3, accounts_v4, auth_urls from olympia.addons.api_urls import addons_v3, addons_v4, addons_v5 @@ -14,16 +16,6 @@ def get_versioned_api_routes(version, url_patterns): route_pattern = r'^{}/'.format(version) - schema_view = get_schema_view( - openapi.Info( - title='AMO API', - default_version=version, - description='The official API for addons.mozilla.org.', - ), - public=True, - permission_classes=(permissions.AllowAny,), - ) - routes = url_patterns # For now, this feature is only enabled in dev mode @@ -31,18 +23,18 @@ def get_versioned_api_routes(version, url_patterns): routes.extend( [ re_path( - r'^swagger(?P\.json|\.yaml)$', - schema_view.without_ui(cache_timeout=0), - name='schema-json', + r'^schema/$', + SpectacularAPIView.as_view(), + name='schema', ), re_path( r'^swagger/$', - schema_view.with_ui('swagger', cache_timeout=0), + SpectacularSwaggerSplitView.as_view(url_name='schema'), name='schema-swagger-ui', ), re_path( r'^redoc/$', - schema_view.with_ui('redoc', cache_timeout=0), + SpectacularRedocView.as_view(url_name='schema'), name='schema-redoc', ), ] diff --git a/src/olympia/core/apps.py b/src/olympia/core/apps.py index 3d7a6aebdd22..3c18f3c053a7 100644 --- a/src/olympia/core/apps.py +++ b/src/olympia/core/apps.py @@ -247,6 +247,8 @@ class CoreConfig(AppConfig): def ready(self): super().ready() + import olympia.schema # noqa: F401 + # Ignore Python warnings unless we're running in debug mode. if not settings.DEBUG: warnings.simplefilter('ignore') diff --git a/src/olympia/lib/settings_base.py b/src/olympia/lib/settings_base.py index 756056f33a79..d3dba7768a79 100644 --- a/src/olympia/lib/settings_base.py +++ b/src/olympia/lib/settings_base.py @@ -360,8 +360,9 @@ def get_db_config(environ_var, atomic_requests=True): # Django's sitemap_index.xml template uses some syntax that jinja doesn't support r'sitemap_index.xml', # Swagger URLs are for the API docs, use some syntax that jinja doesn't support - r'drf-yasg/swagger-ui.html', - r'drf-yasg/redoc.html', + r'drf_spectacular/swagger_ui.html', + r'drf_spectacular/redoc.html', + r'drf_spectacular/swagger_ui.js', ) TEMPLATES = [ @@ -566,7 +567,8 @@ def get_db_config(environ_var, atomic_requests=True): 'django_jinja', 'rangefilter', 'django_recaptcha', - 'drf_yasg', + 'drf_spectacular', + 'drf_spectacular_sidecar', 'django_node_assets', 'django_vite', # Django contrib apps @@ -1108,6 +1110,7 @@ def get_db_config(environ_var, atomic_requests=True): CSP_OBJECT_SRC = ("'none'",) CSP_SCRIPT_SRC = ( + "'self'", GOOGLE_ANALYTICS_HOST, GOOGLE_TAGMANAGER_HOST, 'https://www.recaptcha.net/recaptcha/', @@ -1486,6 +1489,8 @@ def read_only_mode(env): # Use our pagination class by default, which allows clients to request a # different page size. 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + # register spectacular AutoSchema with DRF. + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } # We need to load this before sentry_sdk.init or our reverse replacement is too late. @@ -1636,3 +1641,17 @@ def read_only_mode(env): 'manifest_path': STATIC_BUILD_MANIFEST_PATH, } } + +SPECTACULAR_SETTINGS = { + 'TITLE': 'AMO API', + 'DESCRIPTION': 'Addons Server API Documentation', + 'SERVE_INCLUDE_SCHEMA': True, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], + # load swagger/redoc assets via collectstatic assets + # rather than via the CDN + 'SWAGGER_UI_DIST': 'SIDECAR', + 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', + 'REDOC_DIST': 'SIDECAR', +} + +SWAGGER_SCHEMA_FILE = path('schema.yml') diff --git a/src/olympia/schema.py b/src/olympia/schema.py new file mode 100644 index 000000000000..cde000212403 --- /dev/null +++ b/src/olympia/schema.py @@ -0,0 +1,19 @@ +from drf_spectacular.extensions import OpenApiAuthenticationExtension + + +# Add Extensions to the Schema, so we can handle scenarios where introspection +# is not possible (like when using the SessionIDAuthentication). +# https://drf-spectacular.readthedocs.io/en/latest/customization.html#step-5-extensions + + +class MyAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = 'olympia.api.authentication.SessionIDAuthentication' + name = 'SessionIDAuthentication' + + def get_security_definition(self, auto_schema): + return { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization', + 'description': 'pase the sessionId cookie as "Session {token}"', + } diff --git a/src/olympia/users/models.py b/src/olympia/users/models.py index 8fe9ea68b070..d7afd8a96254 100644 --- a/src/olympia/users/models.py +++ b/src/olympia/users/models.py @@ -570,7 +570,7 @@ def update_is_public(self): log.info('Not changing %s.is_public from %s', self.pk, pre) @property - def name(self): + def name(self) -> str: if self.display_name: return force_str(self.display_name) else: