feat(backend): add SCIM for user provisioning (#12713)

* Add SCIM
Closes https://github.com/inventree/InvenTree/issues/6339

* also test scim

* extend testing with conformance suite

* fix test suite results

* coverage completion

* fil test gaps

* add missing schema values

* fix more type issues

* fix error view

* remove route from OpenAPI

* add changelog entry

* add codeowners
This commit is contained in:
Matthias Mair
2026-08-31 09:24:14 +10:00
committed by GitHub
parent c821fa0f43
commit 4066fa6e0a
34 changed files with 2288 additions and 9 deletions
+3 -1
View File
@@ -8,6 +8,8 @@
# Installer functions
.pkgr.yml @matmair
Procfile @matmair
runtime.txt @matmair
/contrib/installer @matmair
/contrib/packager.io @matmair
# Auth federation
/src/backend/scim/ @matmair
+2
View File
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- [#12713](https://github.com/inventree/InvenTree/pull/12713) adds SCIM 2 provisioning support, allowing InvenTree to be integrated with external identity providers for user management.
### Changed
### Removed
+49
View File
@@ -0,0 +1,49 @@
---
title: SCIM Provisioning
---
## SCIM Provisioning
InvenTree provides a SCIM 2.0 service provider endpoint, allowing an external Identity Providers such as Microsoft Entra ID (Azure AD) or Okta to automatically provision and deprovision users/groups.
!!! info "SSO is separate"
SCIM handles provisioning (creating, updating and deactivating accounts). Login via external Identity Providers ([Single Sign-On](./SSO.md)) is a separate process and settings.
### Supported Operations
The InvenTree SCIM endpoint implements a simple SCIM server as per [RFC7643](https://www.rfc-editor.org/rfc/rfc7643). So discovery and simple users / groups actions are enabled.
Users are _deactivated_ rather than deleted when removed via SCIM.
Bulk operations and more advanced HTTP features are not supported. This approach might not scale to thousands of users.
### Authentication
The SCIM endpoint is **not** authenticated against InvenTree user accounts, OAuth2, or API tokens. Instead, a single bearer secret is generated from the Admin Center and used by your Identity Provider to authenticate every SCIM request:
InvenTree does not store the raw secret - only a HMAC-SHA256 digest of it (seeded with the server's `SECRET_KEY`) is persisted to the database. This means:
- A stolen database backup cannot be used to reconstruct or replay the SCIM secret.
- Rotating the server's `SECRET_KEY` invalidates any previously generated SCIM secret.
- If you lose the secret you need to generate a new one.
!!! warning "Still very powerful"
The SCIM secret gives full access to create, update and deactivate users and groups. Keep it secret, and rotate it if you suspect it has been compromised.
### Enable SCIM Provisioning
1. Open the [Admin Center](./admin.md#admin-center) and navigate to *Identity > SCIM*. This pane is only visible to admins / superusers.
2. Click *Enable SCIM* to generate the bearer secret. The secret is displayed for the only time - copy it immediately.
3. Copy the *Base URL* shown in the same pane (this is your InvenTree instance's SCIM endpoint, e.g. `https://your-instance/scim/v2/`).
4. In your Identity Provider's SCIM application configuration, enter the Base URL as the *SCIM base URL*, and the copied secret as the *Bearer Token* / *API Token*.
5. Trigger a test connection from your Identity Provider to ensure everything was copied correctly
### Rotating or Disabling
- **Rotate Secret**: generates a new secret. The Identity Provider side must be updated
- **Disable SCIM**: disables the endpoint and revokes the current secret
### Limitations
- Only a single Identity Provider is supported at a time - do not register multiple clients at the same time
- The SCIM filter grammar is minimal (`attribute eq "value"` only). Complex filter expressions are not supported as of now
+1 -1
View File
@@ -29,7 +29,7 @@ The Admin Center is the main interface for managing InvenTree. It provides a use
- Users / Groups
- Data import / export
- Customisation (e.g. project codes, custom states, parameters and units)
- Operational controls (e.g. background tasks, errors, currencies)
- Operational controls (e.g. background tasks, errors, currencies, identity federation controls)
- Integration with external services (via machines and plugins)
- Reporting and statistics
+1
View File
@@ -198,6 +198,7 @@ nav:
- User Permissions: settings/permissions.md
- Single Sign on: settings/SSO.md
- Multi Factor Authentication: settings/MFA.md
- SCIM Provisioning: settings/SCIM.md
- Email Settings: settings/email.md
- Experimental Features: settings/experimental.md
- Operations:
@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 535
INVENTREE_API_VERSION = 536
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v536 -> 2026-08-30 : https://github.com/inventree/InvenTree/pull/xxxx
- Adds SCIM 2 provisioning support
v535 -> 2026-08-29 : https://github.com/inventree/InvenTree/pull/12739
- Adds SSO registration API endpoint for creating new users via SSO
@@ -66,6 +66,7 @@ urls = [
paths_ignore_handling = [
'/api/',
'/plugin/',
'/scim/',
reverse('auth-check'),
settings.MEDIA_URL,
settings.STATIC_URL,
@@ -77,6 +78,7 @@ paths_own_security = [
'/o/', # oAuth2 library - has its own auth model
'/anymail/', # Mails - webhooks etc
'/accounts/', # allauth account management - has its own auth model
'/scim/', # SCIM provisioning endpoint - authenticated via its own bearer secret
'/assets/', # Web assets - only used for testing, no security model needed
'/.well-known/',
ensure_slashes(
@@ -316,6 +316,7 @@ INSTALLED_APPS = [
'order.apps.OrderConfig',
'part.apps.PartConfig',
'report.apps.ReportConfig',
'scim.apps.ScimConfig',
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'machine.apps.MachineConfig',
+3
View File
@@ -26,6 +26,7 @@ import order.api
import part.api
import plugin.api
import report.api
import scim.api
import stock.api
import users.api
from plugin.urls import get_plugin_urls, get_wellknown_urls
@@ -137,6 +138,8 @@ backendpatterns = [
path('accounts/', include('allauth.urls')),
# OAuth2
flagged_path('OIDC', 'o/', include(oauth2_urls)),
# SCIM 2 provisioning endpoint
path('scim/v2/', include(scim.api)),
path(
'accounts/login/',
RedirectView.as_view(url=f'/{settings.FRONTEND_URL_BASE}', permanent=False),
+4
View File
@@ -84,6 +84,7 @@ from InvenTree.permissions import (
UserSettingsPermissionsOrScope,
)
from InvenTree.serializers import EmptySerializer
from scim.admin_api import ScimConfigViewSet
admin_router = InvenTreeApiRouter()
common_router = InvenTreeApiRouter()
@@ -1698,4 +1699,7 @@ common_api_urls = [
path('', include(common_router.urls)),
]
# SCIM admin
admin_router.register('scim', ScimConfigViewSet, basename='api-scim')
admin_api_urls = admin_router.urls
+1
View File
@@ -0,0 +1 @@
"""SCIM 2 provisioning app."""
+62
View File
@@ -0,0 +1,62 @@
"""Admin-facing API for managing the SCIM provisioning configuration."""
import structlog
from drf_spectacular.utils import extend_schema
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
import InvenTree.permissions
from scim.models import ScimConfiguration
from scim.serializers import ScimConfigurationSerializer, ScimSecretSerializer
logger = structlog.get_logger('inventree')
class ScimConfigViewSet(viewsets.GenericViewSet):
"""Admin viewset for managing the (singleton) SCIM provisioning configuration."""
permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]
serializer_class = ScimConfigurationSerializer
def get_object(self) -> ScimConfiguration:
"""Return the (singleton) SCIM configuration object."""
return ScimConfiguration.load()
def list(self, request, *args, **kwargs):
"""Return the current SCIM configuration status."""
serializer = self.get_serializer(self.get_object())
return Response(serializer.data)
@extend_schema(request=None, responses={200: ScimSecretSerializer()})
@action(detail=False, methods=['post'], serializer_class=ScimSecretSerializer)
def generate(self, request, *args, **kwargs):
"""Generate a new SCIM bearer secret, and enable the endpoint.
The raw secret is returned exactly once in this response - only its
HMAC digest is persisted. Generating a new secret invalidates any
previously issued one.
"""
config = self.get_object()
secret = config.generate_secret()
config.enabled = True
config.save()
logger.info('SCIM bearer secret (re)generated', user=str(request.user))
serializer = self.get_serializer({'secret': secret})
return Response(serializer.data)
@extend_schema(request=None, responses={200: ScimConfigurationSerializer()})
@action(detail=False, methods=['post'])
def disable(self, request, *args, **kwargs):
"""Disable the SCIM provisioning endpoint and revoke its secret."""
config = self.get_object()
config.revoke()
logger.info('SCIM provisioning disabled', user=str(request.user))
serializer = ScimConfigurationSerializer(
config, context=self.get_serializer_context()
)
return Response(serializer.data)
+38
View File
@@ -0,0 +1,38 @@
"""URL patterns for the SCIM 2.0 provisioning endpoint (`/scim/v2/...`)."""
from django.urls import path, re_path
from scim.views import (
GroupDetailView,
GroupsView,
ResourceTypesView,
SchemasView,
ServiceProviderConfigView,
UserDetailView,
UsersView,
scim_error_view,
)
scim_urls = [
path(
'ServiceProviderConfig',
ServiceProviderConfigView.as_view(),
name='scim-service-provider-config',
),
path('ResourceTypes', ResourceTypesView.as_view(), name='scim-resource-types'),
path(
'ResourceTypes/<str:name>',
ResourceTypesView.as_view(),
name='scim-resource-type-detail',
),
path('Schemas', SchemasView.as_view(), name='scim-schemas'),
path('Schemas/<str:schema_id>', SchemasView.as_view(), name='scim-schema-detail'),
path('Users', UsersView.as_view(), name='scim-users'),
path('Users/<int:pk>', UserDetailView.as_view(), name='scim-user-detail'),
path('Groups', GroupsView.as_view(), name='scim-groups'),
path('Groups/<int:pk>', GroupDetailView.as_view(), name='scim-group-detail'),
# catchall
re_path('.*', scim_error_view, name='scim-not-found'),
]
urlpatterns = scim_urls
+11
View File
@@ -0,0 +1,11 @@
"""AppConfig for the 'scim' app."""
from django.apps import AppConfig
class ScimConfig(AppConfig):
"""AppConfig class for the 'scim' app."""
default_auto_field = 'django.db.models.BigAutoField'
name = 'scim'
verbose_name = 'SCIM'
@@ -0,0 +1,70 @@
"""Authentication for the SCIM provisioning endpoint.
The SCIM endpoint is *not* authenticated against InvenTree user accounts.
Instead, a single bearer secret is generated on-demand from the Admin Center
(see `scim.api`) and is used by the external Identity Provider to
authenticate every SCIM request. Only an HMAC digest of that secret is ever
stored - see `scim.models.ScimConfiguration`.
"""
from django.utils.translation import gettext_lazy as _
from rest_framework import authentication, exceptions
from scim.models import ScimConfiguration
class ScimServiceUser:
"""Lightweight stand-in for a Django user, representing the SCIM integration itself.
The SCIM protocol is authenticated via a shared bearer secret rather than
as any particular InvenTree user account, so a full User instance would be
misleading here. This object satisfies the minimal interface that DRF and
InvenTree's request handling expect from `request.user`.
"""
is_authenticated = True
is_anonymous = False
is_active = True
is_staff = False
is_superuser = False
pk = None
id = None
username = 'scim-provisioning'
def __str__(self):
"""String representation of the SCIM service user."""
return self.username # pragma: no cover
class ScimBearerAuthentication(authentication.BaseAuthentication):
"""DRF authentication class which validates the SCIM bearer secret.
Expects an `Authorization: Bearer <secret>` header. The provided secret is
hashed (HMAC-SHA256, seeded with the Django `SECRET_KEY`) and compared
against the stored digest using a constant-time comparison.
"""
www_authenticate_realm = 'scim'
def authenticate(self, request):
"""Validate the bearer token against the configured SCIM secret."""
header = authentication.get_authorization_header(request).split()
if not header or header[0].lower() != b'bearer':
return None
if len(header) != 2:
raise exceptions.AuthenticationFailed(
_('Invalid SCIM authorization header')
)
secret = header[1].decode('utf-8')
config = ScimConfiguration.load()
if not config.verify_secret(secret):
raise exceptions.AuthenticationFailed(_('Invalid SCIM bearer token'))
config.mark_used()
return (ScimServiceUser(), None)
@@ -0,0 +1,58 @@
"""Initial migration for the 'scim' app."""
from django.db import migrations, models
class Migration(migrations.Migration):
"""Create the singleton ScimConfiguration model."""
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='ScimConfiguration',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'enabled',
models.BooleanField(
default=False,
help_text='Enable the SCIM provisioning endpoint',
verbose_name='Enabled',
),
),
(
'secret_digest',
models.CharField(
blank=True,
help_text='HMAC digest of the current SCIM bearer secret',
max_length=64,
verbose_name='Secret Digest',
),
),
(
'secret_generated',
models.DateTimeField(
blank=True, null=True, verbose_name='Secret Generated'
),
),
(
'last_used',
models.DateTimeField(
blank=True, null=True, verbose_name='Last Used'
),
),
],
options={'verbose_name': 'SCIM Configuration'},
)
]
+101
View File
@@ -0,0 +1,101 @@
"""Database models for the 'scim' app."""
import hashlib
import hmac
import secrets
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
import InvenTree.helpers
def _digest(secret: str) -> str:
"""Compute a hmac digest of a secret, seeded with the secret_key from settings."""
return hmac.new(
settings.SECRET_KEY.encode('utf-8'), secret.encode('utf-8'), hashlib.sha256
).hexdigest()
class ScimConfiguration(models.Model):
"""Simple model for storing config of SCIM."""
class Meta:
"""Metaclass."""
verbose_name = _('SCIM Configuration')
enabled = models.BooleanField(
default=False,
verbose_name=_('Enabled'),
help_text=_('Enable the SCIM provisioning endpoint'),
)
secret_digest = models.CharField(
max_length=64,
blank=True,
verbose_name=_('Secret Digest'),
help_text=_('HMAC digest of the current SCIM bearer secret'),
)
secret_generated = models.DateTimeField(
null=True, blank=True, verbose_name=_('Secret Generated')
)
last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Last Used'))
def __str__(self):
"""String representation of the SCIM configuration."""
return 'SCIM Configuration' # pragma: no cover
def save(self, *args, **kwargs):
"""Ensure that only a single instance of this model can ever exist."""
self.pk = 1
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""Prevent deletion of the singleton configuration object."""
@classmethod
def load(cls) -> 'ScimConfiguration':
"""Return the (singleton) SCIM configuration object, creating it if required."""
obj, _created = cls.objects.get_or_create(pk=1)
return obj
@property
def has_secret(self) -> bool:
"""Return True if a bearer secret has been generated."""
return bool(self.secret_digest)
def generate_secret(self) -> str:
"""Generate a new bearer secret, persist its digest, and return the raw secret.
The raw secret is only ever available at generation time - it cannot be
recovered afterwards, only rotated.
"""
secret = secrets.token_urlsafe(48)
self.secret_digest = _digest(secret)
self.secret_generated = InvenTree.helpers.current_time()
self.last_used = None
self.save()
return secret
def revoke(self):
"""Revoke the current secret and disable the SCIM provisioning."""
self.enabled = False
self.secret_digest = ''
self.secret_generated = None
self.save()
def verify_secret(self, secret: str) -> bool:
"""Return True if the provided raw secret matches the stored digest."""
if not self.enabled or not self.secret_digest or not secret:
return False
return hmac.compare_digest(self.secret_digest, _digest(secret))
def mark_used(self):
"""Record that the SCIM endpoint was just used for a successful request."""
self.last_used = InvenTree.helpers.current_time()
self.save(update_fields=['last_used'])
+13
View File
@@ -0,0 +1,13 @@
"""Permission classes for the 'scim' app."""
from rest_framework import permissions
from scim.authentication import ScimServiceUser
class IsScimAuthenticated(permissions.BasePermission):
"""Require that the request was authenticated via the SCIM bearer secret."""
def has_permission(self, request, view):
"""Only allow access if the request was authenticated as the SCIM service user."""
return isinstance(request.user, ScimServiceUser)
+136
View File
@@ -0,0 +1,136 @@
"""Mapping helpers between Django User/Group objects and SCIM resource models."""
import re
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import User as DjangoUser
from django.urls import reverse
from scim2_models import Email, Group, GroupMember, GroupMembership, Meta, Name, User
#: Simple filter grammar supported by this SCIM implementation: `<attr> eq "<value>"`
FILTER_RE = re.compile(r'^\s*(\w+)\s+eq\s+"([^"]*)"\s*$', re.IGNORECASE)
def parse_filter(filter_string: str | None) -> tuple[str, str] | None:
"""Parse a (very limited) SCIM filter expression of the form `attribute eq "value"`."""
if not filter_string:
return None
match = FILTER_RE.match(filter_string)
if not match:
return None
return match.group(1).lower(), match.group(2)
def user_location(pk) -> str:
"""Return the canonical SCIM location URL for a given user id."""
return reverse('scim-user-detail', kwargs={'pk': pk})
def group_location(pk) -> str:
"""Return the canonical SCIM location URL for a given group id."""
return reverse('scim-group-detail', kwargs={'pk': pk})
def user_to_scim(user: DjangoUser) -> User:
"""Convert a Django User instance into a SCIM User resource."""
name = None
if user.first_name or user.last_name:
name = Name(
given_name=user.first_name or None, family_name=user.last_name or None
)
emails = [Email(value=user.email, primary=True)] if user.email else None
groups = [
GroupMembership(value=str(group.pk), display=group.name, type='direct')
for group in user.groups.all()
] or None
return User(
schemas=['urn:ietf:params:scim:schemas:core:2.0:User'],
id=str(user.pk),
user_name=user.username,
name=name,
emails=emails,
active=user.is_active,
groups=groups,
meta=Meta(
resource_type='User',
created=user.date_joined,
last_modified=user.last_login or user.date_joined,
location=user_location(user.pk),
),
)
def apply_scim_to_user(scim_user: User, user: DjangoUser) -> DjangoUser:
"""Apply the fields of a SCIM User resource onto a Django User instance."""
if scim_user.user_name:
user.username = scim_user.user_name
if scim_user.name:
if scim_user.name.given_name is not None:
user.first_name = scim_user.name.given_name
if scim_user.name.family_name is not None:
user.last_name = scim_user.name.family_name
if scim_user.emails:
primary = next((e for e in scim_user.emails if e.primary), scim_user.emails[0])
if primary.value:
user.email = primary.value
if scim_user.active is not None:
user.is_active = scim_user.active
if scim_user.password:
user.set_password(scim_user.password)
return user
def group_to_scim(group: DjangoGroup) -> Group:
"""Convert a Django Group instance into a SCIM Group resource."""
members = [
GroupMember(value=str(user.pk), display=user.username, type='User')
for user in group.user_set.all()
] or None
return Group(
schemas=['urn:ietf:params:scim:schemas:core:2.0:Group'],
id=str(group.pk),
display_name=group.name,
members=members,
meta=Meta(resource_type='Group', location=group_location(group.pk)),
)
def apply_scim_to_group(scim_group: Group, group: DjangoGroup) -> DjangoGroup:
"""Apply the fields of a SCIM Group resource onto a Django Group instance."""
if scim_group.display_name:
group.name = scim_group.display_name
return group
def set_group_members(group: DjangoGroup, members: list[GroupMember] | None) -> None:
"""Replace the membership of a Django Group based on a list of SCIM GroupMember entries.
Membership changes are applied via `user.groups` (rather than
`group.user_set`) so that InvenTree's own `m2m_changed` signal handlers
(which assume the changed instance is a User) fire correctly.
"""
if members is None:
return
target_ids = {member.value for member in members if member.value}
current_ids = {str(pk) for pk in group.user_set.values_list('pk', flat=True)}
for user in DjangoUser.objects.filter(pk__in=target_ids - current_ids):
user.groups.add(group)
for user in DjangoUser.objects.filter(pk__in=current_ids - target_ids):
user.groups.remove(group)
+55
View File
@@ -0,0 +1,55 @@
"""DRF serializers for the SCIM admin configuration API."""
from django.urls import reverse
from rest_framework import serializers
from scim.models import ScimConfiguration
class ScimConfigurationSerializer(serializers.ModelSerializer):
"""Serializer for the (read-only, status-only) SCIM configuration."""
class Meta:
"""Metaclass options."""
model = ScimConfiguration
fields = ['enabled', 'has_secret', 'secret_generated', 'last_used', 'base_url']
read_only_fields = fields
has_secret = serializers.BooleanField(read_only=True)
base_url = serializers.SerializerMethodField()
def get_base_url(self, obj) -> str:
"""Return the absolute base URL that should be configured in the Identity Provider."""
request = self.context.get('request')
path = reverse('scim-service-provider-config').rsplit(
'ServiceProviderConfig', 1
)[0]
if request is not None:
return request.build_absolute_uri(path)
return path # pragma: no cover
class ScimSecretSerializer(serializers.Serializer):
"""Serializer for a freshly (re)generated SCIM bearer secret.
This is the *only* place the raw secret is ever exposed - it cannot be
retrieved again afterwards.
"""
secret = serializers.CharField(read_only=True)
base_url = serializers.SerializerMethodField()
def get_base_url(self, obj) -> str:
"""Return the absolute base URL that should be configured in the Identity Provider."""
request = self.context.get('request')
path = reverse('scim-service-provider-config').rsplit(
'ServiceProviderConfig', 1
)[0]
if request is not None:
return request.build_absolute_uri(path)
return path # pragma: no cover
+255
View File
@@ -0,0 +1,255 @@
"""Tests for the 'scim' app."""
import json
from django.contrib.auth.models import Group, User
from django.test import override_settings
from django.urls import reverse
from rest_framework.test import APIClient
from scim2_client.engines.httpx import SyncSCIMClient
from scim2_tester import check_server
from InvenTree.unit_test import InvenTreeAPITestCase
from scim.models import ScimConfiguration
class ScimConfigurationModelTests(InvenTreeAPITestCase):
"""Tests for the ScimConfiguration model."""
def test_singleton(self):
"""Only a single configuration object can ever exist."""
a = ScimConfiguration.load()
b = ScimConfiguration.load()
self.assertEqual(a.pk, b.pk)
self.assertEqual(ScimConfiguration.objects.count(), 1)
def test_generate_and_verify_secret(self):
"""A generated secret can be verified, but only while enabled."""
config = ScimConfiguration.load()
self.assertFalse(config.has_secret)
secret = config.generate_secret()
config.enabled = True
config.save()
self.assertTrue(config.has_secret)
self.assertTrue(config.verify_secret(secret))
self.assertFalse(config.verify_secret('not-the-secret'))
self.assertFalse(config.verify_secret(''))
# Disabling the endpoint rejects the (still valid) secret
config.enabled = False
config.save()
self.assertFalse(config.verify_secret(secret))
def test_rotate_invalidates_previous_secret(self):
"""Generating a new secret invalidates the previous one."""
config = ScimConfiguration.load()
first = config.generate_secret()
config.enabled = True
config.save()
second = config.generate_secret()
self.assertFalse(config.verify_secret(first))
self.assertTrue(config.verify_secret(second))
def test_revoke(self):
"""Revoking clears the secret and disables the endpoint."""
config = ScimConfiguration.load()
secret = config.generate_secret()
config.enabled = True
config.save()
config.revoke()
self.assertFalse(config.enabled)
self.assertFalse(config.has_secret)
self.assertFalse(config.verify_secret(secret))
class ScimAdminAPITests(InvenTreeAPITestCase):
"""Tests for the Admin Center facing SCIM configuration API."""
def test_non_superuser_denied(self):
"""A non-superuser cannot view or manage the SCIM configuration."""
self.get(reverse('api-scim-list'), expected_code=403)
self.post(reverse('api-scim-generate'), expected_code=403)
self.post(reverse('api-scim-disable'), expected_code=403)
def test_generate_rotate_disable(self):
"""A superuser can generate, rotate and disable the SCIM secret."""
self.user.is_superuser = True
self.user.save()
response = self.get(reverse('api-scim-list'), expected_code=200)
self.assertFalse(response.data['enabled'])
self.assertFalse(response.data['has_secret'])
response = self.post(reverse('api-scim-generate'), expected_code=200)
secret = response.data['secret']
self.assertTrue(secret)
config = ScimConfiguration.load()
self.assertTrue(config.enabled)
self.assertTrue(config.verify_secret(secret))
response = self.post(reverse('api-scim-generate'), expected_code=200)
new_secret = response.data['secret']
self.assertNotEqual(secret, new_secret)
self.post(reverse('api-scim-disable'), expected_code=200)
config.refresh_from_db()
self.assertFalse(config.enabled)
self.assertFalse(config.has_secret)
class ScimProtocolTests(InvenTreeAPITestCase):
"""Tests for the SCIM 2.0 protocol endpoint."""
def setUp(self):
"""Enable SCIM and generate a bearer secret for use in tests."""
super().setUp()
self.config = ScimConfiguration.load()
self.secret = self.config.generate_secret()
self.config.enabled = True
self.config.save()
def auth_header(self, secret=None):
"""Return the kwargs required to attach a SCIM bearer token to a request."""
return {'HTTP_AUTHORIZATION': f'Bearer {secret or self.secret}'}
def test_service_provider_config_is_public(self):
"""The discovery endpoints do not require authentication."""
self.get(reverse('scim-service-provider-config'), expected_code=200)
self.get(reverse('scim-resource-types'), expected_code=200)
self.get(reverse('scim-schemas'), expected_code=200)
def test_users_endpoint_requires_bearer_token(self):
"""The Users endpoint rejects requests without a valid bearer token."""
self.get(reverse('scim-users'), expected_code=401)
self.get(reverse('scim-users'), expected_code=401, **self.auth_header('wrong'))
self.get(reverse('scim-users'), expected_code=401, HTTP_AUTHORIZATION='Bearer')
def test_filter_users_by_username(self):
"""Users can be filtered by an exact userName match."""
response = self.get(
reverse('scim-users'),
data={'filter': f'userName eq "{self.user.username}"'},
expected_code=200,
**self.auth_header(),
)
self.assertEqual(response.data['totalResults'], 1)
self.assertEqual(response.data['Resources'][0]['userName'], self.user.username)
# wrong field name
self.get(
reverse('scim-users'),
data={'filter': f'qbc eq "{self.user.username}"'},
expected_code=400,
**self.auth_header(),
)
# no known filter
self.get(
reverse('scim-users'),
data={'filter': f'userName noq "{self.user.username}"'},
expected_code=200,
**self.auth_header(),
)
def test_create_group_with_members(self):
"""A group can be provisioned with initial membership via SCIM."""
payload = {
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Group'],
'displayName': 'scim-engineering',
'members': [{'value': str(self.user.pk), 'type': 'User'}],
}
response = self.post(
reverse('scim-groups'),
data=payload,
expected_code=201,
**self.auth_header(),
)
group = Group.objects.get(pk=response.data['id'])
self.assertEqual(group.name, 'scim-engineering')
self.assertIn(self.user, group.user_set.all())
# assign a user
new_user = User.objects.create_user(username='scim-user', password='test')
new_user.groups.add(group)
self.assertIn(new_user, group.user_set.all())
# remove membership via update
payload['members'] = [{'value': str(self.user.pk), 'type': 'User'}]
self.put(
reverse('scim-group-detail', kwargs={'pk': group.pk}),
data=payload,
expected_code=200,
**self.auth_header(),
)
group.refresh_from_db()
self.assertNotIn(new_user, group.user_set.all())
# and remove group via delete
self.delete(
reverse('scim-group-detail', kwargs={'pk': group.pk}),
expected_code=204,
**self.auth_header(),
)
self.assertFalse(Group.objects.filter(pk=group.pk).exists())
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_suite(self):
"""Run the SCIM 2.0 conformance test suite against the endpoint."""
cls = PatchedApiClient(base_url='http://testserver/scim/v2')
cls.logout()
cls.credentials(HTTP_AUTHORIZATION=f'Bearer {self.secret}')
scim_client = SyncSCIMClient(cls)
ignore_tags = {
'crud:read:attributes', # 1: we do not have this attribute
'patch:add', # 2: we do not map these attributes to the User model right now
'patch:remove', # 2:see above
'patch:replace', # 2: see above
'check_replace', # BD
'crud:delete', # 3: there is no deleting users right now
}
results = check_server(scim_client)
failures = [result for result in results if result.status.value not in (1, 7)]
if failures:
details = '\n'.join(
f'{result.status.name}: {result.title} - {result.reason}'
for result in failures
if not ignore_tags.intersection(result.tags)
)
if details:
self.fail(
f'SCIM conformance suite reported failures:\n{details}'
) # pragma: no cover
class PatchedApiClient(APIClient):
"""A DRF APIClient subclass that supports the SCIM media type."""
def __init__(self, base_url: str, *args, **kwargs):
"""Initialize the client."""
self.base_url = base_url
super().__init__(*args, **kwargs)
def generic(self, method, path, data=None, format=None, content_type=None, **extra):
"""Override the generic method to set the SCIM media type."""
if content_type is None:
content_type = 'application/scim+json'
path = self.base_url + path if not '/scim/v2/' in path else path
if json_data := extra.pop('json', None):
data = json.dumps(json_data)
format = 'json' # noqa: A001
return super().generic(
method, path, data=data, format=format, content_type=content_type, **extra
)
+470
View File
@@ -0,0 +1,470 @@
"""SCIM 2.0 protocol views."""
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import User as DjangoUser
from django.http import Http404
from django.shortcuts import get_object_or_404
import structlog
from drf_spectacular.utils import extend_schema
from pydantic import ValidationError
from rest_framework.decorators import (
api_view,
authentication_classes,
permission_classes,
renderer_classes,
)
from rest_framework.exceptions import (
APIException,
AuthenticationFailed,
NotAuthenticated,
)
from rest_framework.parsers import JSONParser
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from scim2_models import (
AuthenticationScheme,
Bulk,
ChangePassword,
Error,
ETag,
Filter,
Group,
ListResponse,
Patch,
PatchOp,
ResourceType,
Schema,
ServiceProviderConfig,
Sort,
User,
)
from scim2_models.exceptions import SCIMException
from scim.authentication import ScimBearerAuthentication
from scim.permissions import IsScimAuthenticated
from scim.resources import (
apply_scim_to_group,
apply_scim_to_user,
group_location,
group_to_scim,
parse_filter,
set_group_members,
user_location,
user_to_scim,
)
logger = structlog.get_logger('inventree')
class ScimRenderer(JSONRenderer):
"""JSON renderer which advertises the `application/scim+json` media type."""
media_type = 'application/scim+json'
class ScimParser(JSONParser):
"""JSON parser which also accepts the `application/scim+json` media type."""
media_type = 'application/scim+json'
def scim_dump(obj) -> dict:
"""Serialize a scim2_models object to a plain (JSON-safe) dict."""
return obj.model_dump(mode='json', exclude_none=True, by_alias=True)
def scim_error(status: int, detail: str, scim_type: str | None = None) -> Response:
"""Build a SCIM-formatted error response."""
error = Error(
schemas=['urn:ietf:params:scim:api:messages:2.0:Error'],
status=status,
scim_type=scim_type,
detail=detail,
)
return Response(
scim_dump(error), status=status, content_type='application/scim+json'
)
class ScimAPIView(APIView):
"""Base class for all SCIM protocol views.
These endpoints follow the SCIM 2.0 protocol (not InvenTree's own REST
API conventions) and authenticate via a bearer secret rather than any of
InvenTree's normal authentication schemes - they are excluded from the
OpenAPI schema entirely (`schema = None`) rather than being documented
alongside the versioned `/api/` surface.
"""
schema = None
authentication_classes = [ScimBearerAuthentication]
permission_classes = [IsScimAuthenticated]
renderer_classes = [ScimRenderer, JSONRenderer]
parser_classes = [ScimParser, JSONParser]
def handle_exception(self, exc):
"""Convert SCIM/pydantic exceptions into RFC7644-formatted error responses."""
if isinstance(exc, SCIMException):
return scim_error(
exc.status, exc.detail, exc.scim_type or None
) # pragma: no cover
if isinstance(exc, ValidationError):
return scim_error(400, str(exc), 'invalidValue') # pragma: no cover
if isinstance(exc, Http404):
return scim_error(404, 'Resource not found')
if isinstance(exc, (AuthenticationFailed, NotAuthenticated)):
return scim_error(
exc.status_code,
str(exc.detail) if hasattr(exc, 'detail') else str(exc),
None,
)
if isinstance(exc, APIException): # pragma: no cover
detail = exc.detail
if isinstance(detail, (list, dict)):
detail = str(detail)
return scim_error(exc.status_code, str(detail), None)
return super().handle_exception(exc) # pragma: no cover
@extend_schema(exclude=True)
@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
@renderer_classes([ScimRenderer])
@permission_classes([])
@authentication_classes([])
def scim_error_view(request, format=None):
"""Always return a SCIM-formatted 404 error, rather than a Django 404 page."""
return scim_error(404, f"Resource '{request.path}' not found")
class ServiceProviderConfigView(ScimAPIView):
"""Advertise the SCIM features supported by this service provider."""
authentication_classes = []
permission_classes = []
def get(self, request, *args, **kwargs):
"""Return the service provider configuration."""
config = ServiceProviderConfig(
schemas=['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
documentation_uri='https://docs.inventree.org/en/latest/settings/scim/',
patch=Patch(supported=True),
bulk=Bulk(supported=False, max_operations=0, max_payload_size=0),
filter=Filter(supported=True, max_results=200),
change_password=ChangePassword(supported=False),
sort=Sort(supported=False),
etag=ETag(supported=False),
authentication_schemes=[
AuthenticationScheme(
type=AuthenticationScheme.Type.oauthbearertoken,
name='Bearer Token',
description='Authentication using a single admin-generated bearer secret',
)
],
)
return Response(scim_dump(config), content_type='application/scim+json')
class ResourceTypesView(ScimAPIView):
"""List the resource types supported by this service provider."""
authentication_classes = []
permission_classes = []
resource_types = {
'User': ResourceType(
schemas=['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
id='User',
name='User',
endpoint='/scim/v2/Users',
description='User Account',
schema_='urn:ietf:params:scim:schemas:core:2.0:User',
),
'Group': ResourceType(
schemas=['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
id='Group',
name='Group',
endpoint='/scim/v2/Groups',
description='Group',
schema_='urn:ietf:params:scim:schemas:core:2.0:Group',
),
}
def get(self, request, name=None, *args, **kwargs):
"""Return either a single resource type, or the full list."""
if name:
resource_type = self.resource_types.get(name)
if not resource_type:
return scim_error(404, f"ResourceType '{name}' not found")
return Response(
scim_dump(resource_type), content_type='application/scim+json'
)
resources = list(self.resource_types.values())
response = ListResponse[ResourceType](
schemas=['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
total_results=len(resources),
items_per_page=len(resources),
start_index=1,
resources=resources,
)
return Response(scim_dump(response), content_type='application/scim+json')
class SchemasView(ScimAPIView):
"""Expose the SCIM schema definitions for the supported resource types."""
authentication_classes = []
permission_classes = []
def get(self, request, schema_id=None, *args, **kwargs):
"""Return either a single schema, or the full list."""
schemas = {
schema.id: schema for schema in (User.to_schema(), Group.to_schema())
}
if schema_id:
schema = schemas.get(schema_id)
if not schema:
return scim_error(404, f"Schema '{schema_id}' not found")
return Response(scim_dump(schema), content_type='application/scim+json')
resources = list(schemas.values())
response = ListResponse[Schema](
schemas=['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
total_results=len(resources),
items_per_page=len(resources),
start_index=1,
resources=resources,
)
return Response(scim_dump(response), content_type='application/scim+json')
class BaseResourceView(ScimAPIView):
"""Shared list/create logic for the Users and Groups endpoints."""
django_model = None
scim_model = None
filterable_fields = {}
def get_queryset(self):
"""Return the base queryset for this resource type."""
return self.django_model.objects.all().order_by('pk')
def to_scim(self, instance):
"""Convert a Django model instance into its SCIM representation. Implemented by subclasses."""
raise NotImplementedError # pragma: no cover
def list(self, request):
"""Handle GET (list) requests, with minimal filter and pagination support."""
if not self.scim_model:
raise NotImplementedError('Missing scim_model') # pragma: no cover
queryset = self.get_queryset()
parsed_filter = parse_filter(request.query_params.get('filter'))
if parsed_filter:
attr, value = parsed_filter
field = self.filterable_fields.get(attr)
if field is None:
return scim_error(
400, f"Filtering on '{attr}' is not supported", 'invalidFilter'
)
queryset = queryset.filter(**{field: value})
start_index = max(int(request.query_params.get('startIndex', 1)), 1)
count = int(request.query_params.get('count', 100))
total_results = queryset.count()
page = queryset[start_index - 1 : start_index - 1 + count]
resources = [self.to_scim(obj) for obj in page]
response = ListResponse[self.scim_model](
schemas=['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
total_results=total_results,
items_per_page=len(resources),
start_index=start_index,
resources=resources,
)
return Response(scim_dump(response), content_type='application/scim+json')
class UsersView(BaseResourceView):
"""`/scim/v2/Users` - list and create Users."""
django_model = DjangoUser
scim_model = User
filterable_fields = {'username': 'username__iexact', 'useremail': 'email__iexact'}
def get(self, request, *args, **kwargs):
"""List users."""
return self.list(request)
def to_scim(self, instance):
"""Convert a Django User into its SCIM representation."""
return user_to_scim(instance)
def post(self, request, *args, **kwargs):
"""Provision a new user."""
scim_user = User.model_validate(request.data)
if DjangoUser.objects.filter(username__iexact=scim_user.user_name).exists():
return scim_error(
409, 'A user with this userName already exists', 'uniqueness'
) # pragma: no cover
user = DjangoUser(username=scim_user.user_name)
user.set_unusable_password()
apply_scim_to_user(scim_user, user)
user.save()
logger.info('SCIM: provisioned new user', username=user.username)
return Response(
scim_dump(user_to_scim(user)),
status=201,
content_type='application/scim+json',
headers={'Location': user_location(user.pk)},
)
class UserDetailView(ScimAPIView):
"""`/scim/v2/Users/<id>` - retrieve, replace, patch and remove a single User."""
def get_object(self, pk):
"""Look up a user by primary key, raising a SCIM-formatted 404 if not found."""
return get_object_or_404(DjangoUser, pk=pk)
def get(self, request, pk, *args, **kwargs):
"""Retrieve a single user."""
user = self.get_object(pk)
return Response(
scim_dump(user_to_scim(user)), content_type='application/scim+json'
)
def put(self, request, pk, *args, **kwargs):
"""Replace a user's attributes."""
user = self.get_object(pk)
scim_user = User.model_validate(request.data)
apply_scim_to_user(scim_user, user)
user.save()
return Response(
scim_dump(user_to_scim(user)), content_type='application/scim+json'
)
def patch(self, request, pk, *args, **kwargs):
"""Apply a SCIM PATCH operation set to a user."""
user = self.get_object(pk)
scim_user = user_to_scim(user)
patch_op = PatchOp[User].model_validate(request.data)
patch_op.patch(scim_user)
apply_scim_to_user(scim_user, user)
user.save()
return Response(
scim_dump(user_to_scim(user)), content_type='application/scim+json'
)
def delete(self, request, pk, *args, **kwargs):
"""Deactivate a user.
Users are deactivated rather than deleted, to preserve historical
ownership references (e.g. on stock items, orders, audit trails).
"""
user = self.get_object(pk)
user.is_active = False
user.save(update_fields=['is_active'])
return Response(status=204)
class GroupsView(BaseResourceView):
"""`/scim/v2/Groups` - list and create Groups."""
django_model = DjangoGroup
scim_model = Group
filterable_fields = {'displayname': 'name__iexact'}
def get(self, request, *args, **kwargs):
"""List groups."""
return self.list(request)
def to_scim(self, instance):
"""Convert a Django Group into its SCIM representation."""
return group_to_scim(instance)
def post(self, request, *args, **kwargs):
"""Provision a new group."""
scim_group = Group.model_validate(request.data)
if DjangoGroup.objects.filter(name__iexact=scim_group.display_name).exists():
return scim_error(
409, 'A group with this displayName already exists', 'uniqueness'
) # pragma: no cover
group = DjangoGroup(name=scim_group.display_name)
group.save()
set_group_members(group, scim_group.members)
logger.info('SCIM: provisioned new group', name=group.name)
return Response(
scim_dump(group_to_scim(group)),
status=201,
content_type='application/scim+json',
headers={'Location': group_location(group.pk)},
)
class GroupDetailView(ScimAPIView):
"""`/scim/v2/Groups/<id>` - retrieve, replace, patch and remove a single Group."""
def get_object(self, pk):
"""Look up a group by primary key, raising a SCIM-formatted 404 if not found."""
return get_object_or_404(DjangoGroup, pk=pk)
def get(self, request, pk, *args, **kwargs):
"""Retrieve a single group."""
group = self.get_object(pk)
return Response(
scim_dump(group_to_scim(group)), content_type='application/scim+json'
)
def put(self, request, pk, *args, **kwargs):
"""Replace a group's attributes and membership."""
group = self.get_object(pk)
scim_group = Group.model_validate(request.data)
apply_scim_to_group(scim_group, group)
group.save()
set_group_members(group, scim_group.members)
return Response(
scim_dump(group_to_scim(group)), content_type='application/scim+json'
)
def patch(self, request, pk, *args, **kwargs):
"""Apply a SCIM PATCH operation set to a group (used mainly for membership changes)."""
group = self.get_object(pk)
scim_group = group_to_scim(group)
patch_op = PatchOp[Group].model_validate(request.data)
patch_op.patch(scim_group)
apply_scim_to_group(scim_group, group)
group.save()
set_group_members(group, scim_group.members)
return Response(
scim_dump(group_to_scim(group)), content_type='application/scim+json'
)
def delete(self, request, pk, *args, **kwargs):
"""Remove a group."""
group = self.get_object(pk)
group.delete()
return Response(status=204)
+2
View File
@@ -231,4 +231,6 @@ def get_ruleset_ignore() -> list[str]:
'importer_dataimportsession',
'importer_dataimportcolumnmap',
'importer_dataimportrow',
# SCIM - superuser-only singleton configuration, managed via the Admin Center
'scim_scimconfiguration',
]
+164
View File
@@ -1,5 +1,11 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements.in -o src/backend/requirements-3.14.txt --python-version=3.14 -c src/backend/requirements.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via
# -c src/backend/requirements.txt
# pydantic
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -821,6 +827,12 @@ djangorestframework-simplejwt[crypto]==5.5.1 \
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via
# -c src/backend/requirements.txt
# email-validator
docutils==0.23 \
--hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \
--hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e
@@ -879,6 +891,12 @@ dulwich==1.2.12 \
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via
# -c src/backend/requirements.txt
# pydantic
et-xmlfile==2.0.0 \
--hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
--hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54
@@ -1050,6 +1068,7 @@ idna==3.19 \
# via
# -c src/backend/requirements.txt
# django-anymail
# email-validator
# requests
inflection==0.5.1 \
--hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \
@@ -1777,6 +1796,136 @@ pycparser==3.0 \
# via
# -c src/backend/requirements.txt
# cffi
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# -c src/backend/requirements.txt
# scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via
# -c src/backend/requirements.txt
# pydantic
pydyf==0.12.1 \
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
@@ -2198,6 +2347,12 @@ s3transfer==0.19.2 \
# via
# -c src/backend/requirements.txt
# boto3
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
sentry-sdk==2.68.0 \
--hash=sha256:538e56c2d03679d42f7c0cb5f1af73a7a510b00abc7e296c13ac49b107b713a4 \
--hash=sha256:648c58e9887311a03470a41539e24bdbbf64a30ca4f5336f7e3dcc87276400b3
@@ -2274,6 +2429,15 @@ typing-extensions==4.16.0 \
# opentelemetry-semantic-conventions
# pint
# py-moneyed
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via
# -c src/backend/requirements.txt
# pydantic
tzdata==2026.3 \
--hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \
--hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931
+222 -3
View File
@@ -1,5 +1,19 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev-3.14.txt -c src/backend/requirements-3.14.txt --python-version=3.14 -c src/backend/requirements.txt -c src/backend/requirements-dev.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
anyio==4.14.2 \
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
--hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
# via
# -c src/backend/requirements-dev.txt
# httpx
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -21,6 +35,8 @@ certifi==2026.7.22 \
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# httpcore
# httpx
# requests
cffi==2.1.1 \
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
@@ -543,12 +559,46 @@ django-types==0.24.0 \
# via
# -c src/backend/requirements-dev.txt
# -r src/backend/requirements-dev.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# email-validator
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
gprof2dot==2025.4.14 \
--hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \
--hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce
# via
# -c src/backend/requirements-dev.txt
# django-silk
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via
# -c src/backend/requirements-dev.txt
# httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via
# -c src/backend/requirements-dev.txt
# httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via
# -c src/backend/requirements-dev.txt
# scim2-client
idna==3.19 \
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
@@ -556,6 +606,9 @@ idna==3.19 \
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# anyio
# email-validator
# httpx
# requests
iniconfig==2.3.0 \
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
@@ -644,9 +697,143 @@ pycparser==3.0 \
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# cffi
pygments==2.21.0 \
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
# via
# -c src/backend/requirements-dev.txt
# pytest
@@ -724,6 +911,27 @@ rich==15.0.0 \
# via
# -c src/backend/requirements-dev.txt
# pytest-codspeed
scim2-client[httpx]==0.7.5 \
--hash=sha256:6667f4b39c5cd8fcd4caf59e8bad136bb6f92ba1f30ece04948f36728b291e20 \
--hash=sha256:bb2d3437027e6b13f2d620038a288c6b718284d3a7249f121c1ce43f5cf7fe1e
# via
# -c src/backend/requirements-dev.txt
# scim2-tester
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# scim2-client
# scim2-tester
scim2-tester[httpx]==0.2.8 \
--hash=sha256:d8b8f3d18d452cafbe3eb966c7013579c78c75794eba31ca161e68e6f6544051 \
--hash=sha256:fbd2cce113cd9c0c14caf2660dc252d3b360a5461b931aa88f96ac7f0bd74ae6
# via
# -c src/backend/requirements-dev.txt
# -r src/backend/requirements-dev.in
setuptools==84.0.0 \
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
@@ -786,6 +994,17 @@ typing-extensions==4.16.0 \
# django-stubs
# django-stubs-ext
# django-test-migrations
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
urllib3==2.7.0 \
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
+1
View File
@@ -15,3 +15,4 @@ django-stubs # typing
requests-mock # Mock requests for unit tests
pytest-codspeed # Performance testing with Codspeed
pytest-django # Pytest support for Django (for benchnmarking)
scim2-tester[httpx] # SCIM test suite
+197 -3
View File
@@ -1,5 +1,15 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev.txt -c src/backend/requirements.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via
# -c src/backend/requirements.txt
# pydantic
anyio==4.14.2 \
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
--hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
# via httpx
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -15,6 +25,8 @@ certifi==2026.7.22 \
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
# via
# -c src/backend/requirements.txt
# httpcore
# httpx
# requests
cffi==2.1.1 \
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
@@ -511,15 +523,42 @@ django-types==0.24.0 \
--hash=sha256:af903de8b9ee963b7594459a7a20cb8eaaab176ae2b3244ecaa089e0c570b0d1 \
--hash=sha256:ddb478ca733e0dde5475118dd59ab340156980f9659fd92de2083326ae96100a
# via -r src/backend/requirements-dev.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via
# -c src/backend/requirements.txt
# email-validator
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via
# -c src/backend/requirements.txt
# pydantic
gprof2dot==2025.4.14 \
--hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \
--hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce
# via django-silk
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via scim2-client
idna==3.19 \
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
# via
# -c src/backend/requirements.txt
# anyio
# email-validator
# httpx
# requests
iniconfig==2.3.0 \
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
@@ -586,9 +625,139 @@ pycparser==3.0 \
# via
# -c src/backend/requirements.txt
# cffi
pygments==2.21.0 \
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# -c src/backend/requirements.txt
# scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via
# -c src/backend/requirements.txt
# pydantic
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
# via
# pytest
# rich
@@ -653,6 +822,21 @@ rich==15.0.0 \
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
# via pytest-codspeed
scim2-client[httpx]==0.7.5 \
--hash=sha256:6667f4b39c5cd8fcd4caf59e8bad136bb6f92ba1f30ece04948f36728b291e20 \
--hash=sha256:bb2d3437027e6b13f2d620038a288c6b718284d3a7249f121c1ce43f5cf7fe1e
# via scim2-tester
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via
# -c src/backend/requirements.txt
# scim2-client
# scim2-tester
scim2-tester[httpx]==0.2.8 \
--hash=sha256:d8b8f3d18d452cafbe3eb966c7013579c78c75794eba31ca161e68e6f6544051 \
--hash=sha256:fbd2cce113cd9c0c14caf2660dc252d3b360a5461b931aa88f96ac7f0bd74ae6
# via -r src/backend/requirements-dev.in
setuptools==84.0.0 \
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
@@ -700,9 +884,19 @@ typing-extensions==4.16.0 \
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
# via
# -c src/backend/requirements.txt
# anyio
# django-stubs
# django-stubs-ext
# django-test-migrations
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via
# -c src/backend/requirements.txt
# pydantic
urllib3==2.7.0 \
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
+1
View File
@@ -50,6 +50,7 @@ python-dotenv # Environment variable management
pyyaml # YAML parsing
qrcode[pil] # QR code generator
rapidfuzz # Fuzzy string matching
scim2-models # SCIM 2.0 schema / protocol models, used for the SCIM provisioning endpoint
sentry-sdk # Error reporting (optional)
setuptools # Standard dependency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
+150
View File
@@ -1,5 +1,9 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements.in -o src/backend/requirements.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via pydantic
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -728,6 +732,10 @@ djangorestframework-simplejwt[crypto]==5.5.1 \
--hash=sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469 \
--hash=sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f
# via -r src/backend/requirements.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via email-validator
docutils==0.23 \
--hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \
--hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e
@@ -780,6 +788,10 @@ dulwich==1.2.12 \
--hash=sha256:d7f1ff233081ab644f35e90a0ef169f8d9ee46a72820a3fbf8476f9665e79800 \
--hash=sha256:dfde0fba1eb1f208fc1f0c764e3bb213b31d986f37f7548b994b91e244a10570
# via -r src/backend/requirements.in
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via pydantic
et-xmlfile==2.0.0 \
--hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
--hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54
@@ -930,6 +942,7 @@ idna==3.19 \
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
# via
# django-anymail
# email-validator
# requests
inflection==0.5.1 \
--hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \
@@ -1577,6 +1590,132 @@ pycparser==3.0 \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via pydantic
pydyf==0.12.1 \
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
@@ -1965,6 +2104,10 @@ s3transfer==0.19.2 \
--hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \
--hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25
# via boto3
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via -r src/backend/requirements.in
sentry-sdk==2.68.0 \
--hash=sha256:538e56c2d03679d42f7c0cb5f1af73a7a510b00abc7e296c13ac49b107b713a4 \
--hash=sha256:648c58e9887311a03470a41539e24bdbbf64a30ca4f5336f7e3dcc87276400b3
@@ -2027,7 +2170,14 @@ typing-extensions==4.16.0 \
# opentelemetry-semantic-conventions
# pint
# py-moneyed
# pydantic
# pydantic-core
# referencing
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via pydantic
tzdata==2026.3 \
--hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \
--hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931
+3
View File
@@ -258,6 +258,9 @@ export enum ApiEndpoints {
notes_image_upload = 'notes-image-upload/',
email_list = 'admin/email/',
email_test = 'admin/email/test/',
scim_config = 'admin/scim/',
scim_generate = 'admin/scim/generate/',
scim_disable = 'admin/scim/disable/',
config_list = 'admin/config/',
parameter_list = 'parameter/',
parameter_template_list = 'parameter/template/',
@@ -21,6 +21,7 @@ import {
IconQrcode,
IconReport,
IconScale,
IconShieldLock,
IconSitemap,
IconTags,
IconUsersGroup
@@ -72,6 +73,10 @@ const MachineManagementPanel = Loadable(
lazy(() => import('./MachineManagementPanel'))
);
const ScimManagementPanel = Loadable(
lazy(() => import('./ScimManagementPanel'))
);
const ErrorReportTable = Loadable(
lazy(() => import('../../../../tables/settings/ErrorTable'))
);
@@ -262,6 +267,13 @@ export default function AdminCenter() {
icon: <IconDevicesPc />,
content: <MachineManagementPanel />,
hidden: !user.hasViewRole(UserRoles.admin)
},
{
name: 'identity',
label: t`Identity`,
icon: <IconShieldLock />,
content: <ScimManagementPanel />,
hidden: !user.hasViewRole(UserRoles.admin)
}
];
}, [user]);
@@ -273,6 +285,7 @@ export default function AdminCenter() {
label: t`Operations`,
panelIDs: [
'user',
'identity',
'barcode-history',
'background',
'errors',
@@ -0,0 +1,194 @@
import { CopyButton } from '@lib/components/CopyButton';
import { StylishText } from '@lib/components/StylishText';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import {
Alert,
Badge,
Button,
Code,
Divider,
Group,
Loader,
Modal,
Paper,
Stack,
Table,
Text
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import { IconShieldLock, IconShieldOff } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { api, queryClient } from '../../../../App';
import { showApiErrorMessage } from '../../../../functions/notifications';
export default function ScimManagementPanel() {
const [secret, setSecret] = useState<string>('');
const [
secretModalOpened,
{ open: openSecretModal, close: closeSecretModal }
] = useDisclosure(false);
const { data, isFetching } = useQuery({
queryKey: ['scim-config'],
queryFn: () =>
api.get(apiUrl(ApiEndpoints.scim_config)).then((res) => res.data),
refetchOnMount: true
});
const generateSecret = (action: 'generate' | 'rotate') => {
api
.post(apiUrl(ApiEndpoints.scim_generate))
.then((res) => {
setSecret(res.data.secret);
openSecretModal();
queryClient.invalidateQueries({ queryKey: ['scim-config'] });
showNotification({
title:
action === 'generate' ? t`SCIM enabled` : t`SCIM secret rotated`,
message: t`The new bearer secret is only shown once`,
color: 'green'
});
})
.catch((error) => {
showApiErrorMessage({ error, title: t`Error generating SCIM secret` });
});
};
const disableScim = () => {
api
.post(apiUrl(ApiEndpoints.scim_disable))
.then(() => {
queryClient.invalidateQueries({ queryKey: ['scim-config'] });
showNotification({
title: t`SCIM disabled`,
message: t`The SCIM provisioning endpoint has been disabled and its secret revoked`,
color: 'blue'
});
})
.catch((error) => {
showApiErrorMessage({ error, title: t`Error disabling SCIM` });
});
};
if (isFetching && !data) {
return <Loader />;
}
return (
<Stack gap='md'>
<Modal
opened={secretModalOpened}
onClose={closeSecretModal}
title={<StylishText size='xl'>{t`SCIM Bearer Secret`}</StylishText>}
centered
data-testid='scim-secret-modal'
>
<Alert color='yellow' mb='sm'>
<Trans>
This secret is only shown once - copy it now and store it in your
Identity Provider's SCIM configuration. It cannot be retrieved
again, only rotated.
</Trans>
</Alert>
<Paper p='sm' withBorder>
<Group justify='space-between' wrap='nowrap'>
<Code style={{ wordBreak: 'break-all', whiteSpace: 'normal' }}>
{secret}
</Code>
<CopyButton value={secret} />
</Group>
</Paper>
</Modal>
<Alert icon={<IconShieldLock />} color='blue'>
<Trans>
SCIM allows an external Identity Provider (e.g. Okta, Microsoft Entra
ID, OneLogin) to automatically provision and deprovision Users and
Groups. Single Sign-On (interactive login) is configured separately,
under Single Sign On.
</Trans>
</Alert>
<Table>
<Table.Tbody>
<Table.Tr>
<Table.Td>
<Trans>Status</Trans>
</Table.Td>
<Table.Td>
{data?.enabled ? (
<Badge color='green'>
<Trans>Enabled</Trans>
</Badge>
) : (
<Badge color='gray'>
<Trans>Disabled</Trans>
</Badge>
)}
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Base URL</Trans>
</Table.Td>
<Table.Td>
<Group gap='xs' wrap='nowrap'>
<Code>{data?.base_url}</Code>
<CopyButton value={data?.base_url} />
</Group>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Secret Generated</Trans>
</Table.Td>
<Table.Td>{data?.secret_generated ?? '-'}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Last Used</Trans>
</Table.Td>
<Table.Td>{data?.last_used ?? '-'}</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
<Divider />
<Group>
<Button
leftSection={<IconShieldLock size={16} />}
onClick={() => generateSecret(data?.enabled ? 'rotate' : 'generate')}
>
{data?.enabled ? (
<Trans>Rotate Secret</Trans>
) : (
<Trans>Enable SCIM</Trans>
)}
</Button>
{data?.enabled && (
<Button
color='red'
variant='outline'
leftSection={<IconShieldOff size={16} />}
onClick={disableScim}
>
<Trans>Disable SCIM</Trans>
</Button>
)}
</Group>
<Text size='sm' c='dimmed'>
<Trans>
Rotating the secret immediately invalidates the previous one - update
your Identity Provider's configuration straight away.
</Trans>
</Text>
</Stack>
);
}
+1
View File
@@ -334,6 +334,7 @@ def builtin_apps():
'generic',
'machine',
'web',
'scim',
]