mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-01 17:51:23 +00:00
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:
+3
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""SCIM 2 provisioning app."""
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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'},
|
||||
)
|
||||
]
|
||||
@@ -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'])
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user