mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-30 00:46:40 +00:00
[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:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -84,6 +84,7 @@ class PluginMixinEnum(StringEnum):
|
||||
URLS = 'urls'
|
||||
USER_INTERFACE = 'ui'
|
||||
VALIDATION = 'validation'
|
||||
WELLKNOWN = 'well-known'
|
||||
|
||||
|
||||
class MetaBase:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')))
|
||||
|
||||
Reference in New Issue
Block a user