diff --git a/pyproject.toml b/pyproject.toml index 69b8e3c810..a17e5c57af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "Django>=4.2,<4.3", "django-filter>=2", "djangorestframework>=3.12", + "drf-spectacular", "pytz", "iso8601", diff --git a/python/nav/django/settings.py b/python/nav/django/settings.py index 75fa799054..cd7c85a0c8 100644 --- a/python/nav/django/settings.py +++ b/python/nav/django/settings.py @@ -221,6 +221,7 @@ 'django.contrib.humanize', 'django_filters', 'rest_framework', + 'drf_spectacular', 'nav.auditlog', 'nav.web.macwatch', 'nav.web.geomap', @@ -235,6 +236,12 @@ 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), 'DEFAULT_PAGINATION_CLASS': 'nav.web.api.v1.NavPageNumberPagination', 'UNAUTHENTICATED_USER': 'nav.django.utils.default_account', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'NAV API', + 'PREPROCESSING_HOOKS': ['nav.web.api.schema.public_schema_filter'], } # Classes that implement a search engine for the web navbar diff --git a/python/nav/web/api/schema.py b/python/nav/web/api/schema.py new file mode 100644 index 0000000000..2cc8192d41 --- /dev/null +++ b/python/nav/web/api/schema.py @@ -0,0 +1,32 @@ +# +# Copyright (C) 2025 Sikt +# +# This file is part of Network Administration Visualized (NAV). +# +# NAV is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 3 as published by +# the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. You should have received a copy of the GNU General Public +# License along with NAV. If not, see . +# +import re + +PUBLIC_SCHEMA_FILTER = re.compile(r'^/api/\d+/.*$') + + +def public_schema_filter(endpoints): + """drf-spectacular preprocessing filter to filter out DRF endpoints we don't want to 'expose' as public API. + + This is mostly because of NAV's weird DRF usage, where the latest API version is bound both to the /api/ and + /api// URL prefixes. Some NAV apps also provide internal APIs that should not be exposed as part of the + public schema. This function basically ensures only endpoints with the /api// prefix are included in + the schema. + """ + for endpoint in endpoints: + path, _path_regex, _method, _callback = endpoint + if PUBLIC_SCHEMA_FILTER.match(path): + yield endpoint diff --git a/python/nav/web/api/v1/urls.py b/python/nav/web/api/v1/urls.py index 04a1fe6e31..916c4cb084 100644 --- a/python/nav/web/api/v1/urls.py +++ b/python/nav/web/api/v1/urls.py @@ -17,7 +17,8 @@ # pylint: disable=E1101 """Urlconf for the NAV REST api""" -from django.urls import re_path, include +from django.urls import path, re_path, include +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from rest_framework import routers from nav.auditlog import api as auditlogapi @@ -54,8 +55,17 @@ router.register(r'auditlog', auditlogapi.LogEntryViewSet, basename='auditlog') router.register(r'module', views.ModuleViewSet, basename='module') +openapi_urls = [ + path('', SpectacularAPIView.as_view(api_version="1"), name='schema'), + path( + "swagger-ui/", + SpectacularSwaggerView.as_view(url_name="api:1:openapi:schema"), + name="swagger-ui", + ), +] urlpatterns = [ + path("schema/", include((openapi_urls, "openapi"))), re_path(r'^$', views.api_root), re_path(r'^token/$', views.get_or_create_token, name="token"), re_path(r'^version/$', views.get_nav_version, name="version"), diff --git a/requirements/base.txt b/requirements/base.txt index 37fdd902fa..b00564c04a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,6 +24,7 @@ dnspython<3.0.0,>=2.1.0 django-filter>=2 djangorestframework>=3.12 +drf-spectacular # REST framework iso8601