[FR] add well-known mixin to let plugins define well-known urls (#12698)

* [FR] add well-known mixin to let plugins define well-known data entry points
Fixes #11741

* add built-in plugin for apis

* add test

* add decorator

* ignore default

* fix tests

* remove erronius decorator

* all well-known paths to security exception paths

* update test setup

* ignore escape paths

* add docs

* add more on security considerations

* add to nav
This commit is contained in:
Matthias Mair
2026-08-28 16:43:20 +10:00
committed by GitHub
parent 0ee2ee9ec2
commit 5244b8a4f2
13 changed files with 246 additions and 4 deletions
@@ -78,6 +78,7 @@ paths_own_security = [
'/anymail/', # Mails - webhooks etc
'/accounts/', # allauth account management - has its own auth model
'/assets/', # Web assets - only used for testing, no security model needed
'/.well-known/',
ensure_slashes(
settings.STATIC_URL
), # Static files - static files are considered safe to serve
+4 -1
View File
@@ -28,7 +28,7 @@ import plugin.api
import report.api
import stock.api
import users.api
from plugin.urls import get_plugin_urls
from plugin.urls import get_plugin_urls, get_wellknown_urls
from web.urls import cui_compatibility_urls
from web.urls import urlpatterns as platform_urls
@@ -167,6 +167,9 @@ urlpatterns += platform_urls
if settings.PLUGINS_ENABLED:
urlpatterns.append(get_plugin_urls())
# Append well-known URLs
urlpatterns.append(get_wellknown_urls())
# Server running in "DEBUG" mode?
if settings.DEBUG:
# Static file access
@@ -0,0 +1,36 @@
"""Plugin mixin class for adding well-known urls."""
from plugin import PluginMixinEnum
from plugin.helpers import MixinNotImplementedError
class WellKnownMixin:
"""Mixin class which provides support for advertising well-known URLs."""
class MixinMeta:
"""Meta options for this mixin class."""
MIXIN_NAME = 'WellKnown'
def __init__(self):
"""Register the mixin."""
super().__init__()
self.add_mixin(PluginMixinEnum.WELLKNOWN, True, __class__)
def get_well_known_urls(self, request=None) -> list[tuple[str, str]]:
"""Get well-known URLs.
This method *must* be implemented by the plugin class.
Arguments:
request: The Django request object (optional)
Returns:
A list of well-known URLs as (name, url) tuples, or None if not available
Raises:
Can raise any exception if the update fails
"""
raise MixinNotImplementedError(
'Plugin must implement get_well_known_urls method'
) # pragma: no cover
@@ -0,0 +1,45 @@
"""Base plugin which defines the built-in well-known entries."""
from django.http import HttpRequest, JsonResponse
from django.urls import path, reverse_lazy
from django.utils.translation import gettext_lazy as _
import InvenTree.helpers
from InvenTree.permissions import auth_exempt
from plugin import InvenTreePlugin
from plugin.mixins import UrlsMixin, WellKnownMixin
class InvenTreeWellKnown(WellKnownMixin, UrlsMixin, InvenTreePlugin):
"""Plugin which provides built-in well-known URLs."""
NAME = 'InvenTreeWellKnown'
SLUG = 'inventree-well-known'
TITLE = _('InvenTree Well-Known URLs')
DESCRIPTION = _('Built-in well-known URLs for InvenTree')
AUTHOR = _('InvenTree contributors')
VERSION = '1.0.0'
def get_well_known_urls(
self, request: 'HttpRequest | None' = None
) -> list[tuple[str, str]]:
"""Return all built-in well-known entries."""
data = []
# See https://www.w3.org/TR/passkey-endpoints/
data.append(('passkey-endpoints', reverse_lazy(f'plugin:{self.slug}:passkey')))
# placeholder for more
return data
@auth_exempt
def view_passkey(self, request, *args, **kwargs):
"""Return the passkey well-known entry."""
passkey_web = request.build_absolute_uri(
InvenTree.helpers.pui_url('/settings/user/security')
)
return JsonResponse({'enroll': passkey_web, 'manage': passkey_web})
def setup_urls(self):
"""Urls that are exposed by this plugin."""
return [path('passkey/', self.view_passkey, name='passkey')]
@@ -0,0 +1,45 @@
"""Test for built-in InvenTree well-known plugin."""
from django.test import override_settings
from django.urls import reverse
from common.models import InvenTreeSetting
from InvenTree.unit_test import InvenTreeAPITestCase
from plugin.registry import registry
class InvenTreeWellKnownTest(InvenTreeAPITestCase):
"""Tests for the InvenTreeWellKnown plugin."""
def setUp(self):
"""Setup some testing drivers/machines."""
InvenTreeSetting.set_setting('ENABLE_PLUGINS_URL', True, None)
registry.reload_plugins()
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_well_known_urls(self):
"""Test that the well-known URLs are returned correctly from the index view."""
url = reverse('well-known:index')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('well_known_urls', data)
# passkey URLs should be present
self.assertIn('passkey-endpoints', data['well_known_urls'])
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_passkey_view(self):
"""Test that the passkey view returns the correct JSON response."""
response = self.client.get(
'/.well-known/passkey-endpoints/', follow=True, accept='application/json'
)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('enroll', data)
self.assertIn('manage', data)
@@ -17,6 +17,7 @@ from plugin.base.integration.SettingsMixin import SettingsMixin
from plugin.base.integration.TransitionMixin import TransitionMixin
from plugin.base.integration.UrlsMixin import UrlsMixin
from plugin.base.integration.ValidationMixin import ValidationMixin
from plugin.base.integration.WellKnownMixin import WellKnownMixin
from plugin.base.label.mixins import LabelPrintingMixin
from plugin.base.locate.mixins import LocateMixin
from plugin.base.mail.mixins import MailMixin
@@ -48,5 +49,6 @@ __all__ = [
'UrlsMixin',
'UserInterfaceMixin',
'ValidationMixin',
'WellKnownMixin',
'supplier',
]
+1
View File
@@ -84,6 +84,7 @@ class PluginMixinEnum(StringEnum):
URLS = 'urls'
USER_INTERFACE = 'ui'
VALIDATION = 'validation'
WELLKNOWN = 'well-known'
class MetaBase:
+5 -1
View File
@@ -100,6 +100,7 @@ class PluginsRegistry:
'inventreelabel',
'inventreelabelmachine',
'parameter-exporter',
'inventree-well-known',
]
ready: bool
@@ -997,7 +998,7 @@ class PluginsRegistry:
as any custom AppMixin plugins require admin integration
"""
from InvenTree.urls import urlpatterns
from plugin.urls import get_plugin_urls
from plugin.urls import get_plugin_urls, get_wellknown_urls
for index, url in enumerate(urlpatterns):
app_name = getattr(url, 'app_name', None)
@@ -1012,6 +1013,9 @@ class PluginsRegistry:
if app_name == 'plugin':
urlpatterns[index] = get_plugin_urls()
if app_name == 'well-known':
urlpatterns[index] = get_wellknown_urls()
# Refresh the URL cache
clear_url_caches()
+1 -1
View File
@@ -571,7 +571,7 @@ class RegistryTests(TestQueryMixin, PluginRegistryMixin, TestCase):
PluginConfig.objects.all().delete()
# Change this value whenever a new mandatory plugin is added
N_MANDATORY_PLUGINS = 10
N_MANDATORY_PLUGINS = 11
registry.reload_plugins(full_reload=True, collect=True)
mandatory = registry.MANDATORY_PLUGINS
+55 -1
View File
@@ -1,7 +1,8 @@
"""URL lookup for plugin app."""
from django.conf import settings
from django.urls import include, re_path
from django.http import JsonResponse
from django.urls import include, path, re_path
from django.urls.exceptions import Resolver404
from django.views.generic.base import RedirectView
@@ -55,3 +56,56 @@ def get_plugin_urls():
)
return re_path(f'^{PLUGIN_BASE}/', include((urls, 'plugin')))
def wellknownindexview(request):
"""Simple view that returns a list of all well-known URLs as JSON."""
from plugin.registry import registry
well_known_urls = {}
if registry.is_ready:
for plugin in registry.with_mixin(PluginMixinEnum.WELLKNOWN):
try:
if urls := plugin.get_well_known_urls(request):
for name, url in urls:
well_known_urls[name] = request.build_absolute_uri(url)
except Exception: # pragma: no cover
log_error('WellKnownView', plugin=plugin.slug)
continue
return JsonResponse({'well_known_urls': well_known_urls})
def get_wellknown_urls():
"""Returns a urlpattern that can be integrated into the global urls (as redirects)."""
from plugin.registry import registry
urls = []
if registry.is_ready:
for plugin in registry.with_mixin(PluginMixinEnum.WELLKNOWN):
try:
if well_known_urls := plugin.get_well_known_urls(request=None):
for name, url in well_known_urls:
urls.append(
path(
name,
RedirectView.as_view(url=url, permanent=False),
name=name,
)
)
urls.append(
re_path(
f'^{name}/.*$',
RedirectView.as_view(url=url, permanent=False),
name=name,
)
)
except Exception: # pragma: no cover
log_error('get_wellknown_urls', plugin=plugin.slug)
continue
# Add index page that lists all well-known URLs
urls.append(path('', wellknownindexview, name='index'))
return path('.well-known/', include((urls, 'well-known')))