diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 401578a010..8530756668 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f4bf1f4e..c9845621ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/docs/settings/SCIM.md b/docs/docs/settings/SCIM.md new file mode 100644 index 0000000000..8cc6a5875c --- /dev/null +++ b/docs/docs/settings/SCIM.md @@ -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 diff --git a/docs/docs/settings/admin.md b/docs/docs/settings/admin.md index fc922e70e7..3237172951 100644 --- a/docs/docs/settings/admin.md +++ b/docs/docs/settings/admin.md @@ -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 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ab193f3673..bc9647c8cd 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -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: diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index f4fbaf79c7..102f254444 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -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 diff --git a/src/backend/InvenTree/InvenTree/middleware.py b/src/backend/InvenTree/InvenTree/middleware.py index 32a15ae5b6..93891f1942 100644 --- a/src/backend/InvenTree/InvenTree/middleware.py +++ b/src/backend/InvenTree/InvenTree/middleware.py @@ -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( diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index dd0b73ab9a..200b8f635d 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -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', diff --git a/src/backend/InvenTree/InvenTree/urls.py b/src/backend/InvenTree/InvenTree/urls.py index 285f476b8a..02f0f5110e 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -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), diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 792b7dd79f..81610a34ff 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -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 diff --git a/src/backend/InvenTree/scim/__init__.py b/src/backend/InvenTree/scim/__init__.py new file mode 100644 index 0000000000..7d0790654c --- /dev/null +++ b/src/backend/InvenTree/scim/__init__.py @@ -0,0 +1 @@ +"""SCIM 2 provisioning app.""" diff --git a/src/backend/InvenTree/scim/admin_api.py b/src/backend/InvenTree/scim/admin_api.py new file mode 100644 index 0000000000..6e5728ece0 --- /dev/null +++ b/src/backend/InvenTree/scim/admin_api.py @@ -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) diff --git a/src/backend/InvenTree/scim/api.py b/src/backend/InvenTree/scim/api.py new file mode 100644 index 0000000000..0f1384e4a2 --- /dev/null +++ b/src/backend/InvenTree/scim/api.py @@ -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/', + ResourceTypesView.as_view(), + name='scim-resource-type-detail', + ), + path('Schemas', SchemasView.as_view(), name='scim-schemas'), + path('Schemas/', SchemasView.as_view(), name='scim-schema-detail'), + path('Users', UsersView.as_view(), name='scim-users'), + path('Users/', UserDetailView.as_view(), name='scim-user-detail'), + path('Groups', GroupsView.as_view(), name='scim-groups'), + path('Groups/', GroupDetailView.as_view(), name='scim-group-detail'), + # catchall + re_path('.*', scim_error_view, name='scim-not-found'), +] + +urlpatterns = scim_urls diff --git a/src/backend/InvenTree/scim/apps.py b/src/backend/InvenTree/scim/apps.py new file mode 100644 index 0000000000..447304eeb5 --- /dev/null +++ b/src/backend/InvenTree/scim/apps.py @@ -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' diff --git a/src/backend/InvenTree/scim/authentication.py b/src/backend/InvenTree/scim/authentication.py new file mode 100644 index 0000000000..ef79388c5b --- /dev/null +++ b/src/backend/InvenTree/scim/authentication.py @@ -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 ` 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) diff --git a/src/backend/InvenTree/scim/migrations/0001_initial.py b/src/backend/InvenTree/scim/migrations/0001_initial.py new file mode 100644 index 0000000000..949bdfc831 --- /dev/null +++ b/src/backend/InvenTree/scim/migrations/0001_initial.py @@ -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'}, + ) + ] diff --git a/src/backend/InvenTree/scim/migrations/__init__.py b/src/backend/InvenTree/scim/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/backend/InvenTree/scim/models.py b/src/backend/InvenTree/scim/models.py new file mode 100644 index 0000000000..24276f80ce --- /dev/null +++ b/src/backend/InvenTree/scim/models.py @@ -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']) diff --git a/src/backend/InvenTree/scim/permissions.py b/src/backend/InvenTree/scim/permissions.py new file mode 100644 index 0000000000..56143e2559 --- /dev/null +++ b/src/backend/InvenTree/scim/permissions.py @@ -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) diff --git a/src/backend/InvenTree/scim/resources.py b/src/backend/InvenTree/scim/resources.py new file mode 100644 index 0000000000..3e9d6f43dd --- /dev/null +++ b/src/backend/InvenTree/scim/resources.py @@ -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: ` eq ""` +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) diff --git a/src/backend/InvenTree/scim/serializers.py b/src/backend/InvenTree/scim/serializers.py new file mode 100644 index 0000000000..31d172155c --- /dev/null +++ b/src/backend/InvenTree/scim/serializers.py @@ -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 diff --git a/src/backend/InvenTree/scim/tests.py b/src/backend/InvenTree/scim/tests.py new file mode 100644 index 0000000000..0b611e9034 --- /dev/null +++ b/src/backend/InvenTree/scim/tests.py @@ -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 + ) diff --git a/src/backend/InvenTree/scim/views.py b/src/backend/InvenTree/scim/views.py new file mode 100644 index 0000000000..f0b0da9322 --- /dev/null +++ b/src/backend/InvenTree/scim/views.py @@ -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/` - 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/` - 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) diff --git a/src/backend/InvenTree/users/ruleset.py b/src/backend/InvenTree/users/ruleset.py index 0ed2ddf3fc..c5912e9358 100644 --- a/src/backend/InvenTree/users/ruleset.py +++ b/src/backend/InvenTree/users/ruleset.py @@ -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', ] diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index b9cf88fed1..6c6b289805 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -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 diff --git a/src/backend/requirements-dev-3.14.txt b/src/backend/requirements-dev-3.14.txt index 3ea39287a2..0240222e0e 100644 --- a/src/backend/requirements-dev-3.14.txt +++ b/src/backend/requirements-dev-3.14.txt @@ -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 diff --git a/src/backend/requirements-dev.in b/src/backend/requirements-dev.in index 32c9673837..7a9834bdc9 100644 --- a/src/backend/requirements-dev.in +++ b/src/backend/requirements-dev.in @@ -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 diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index 7dfedf6161..184ea7c5b4 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -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 diff --git a/src/backend/requirements.in b/src/backend/requirements.in index 7d6b0a2cd8..002833d032 100644 --- a/src/backend/requirements.in +++ b/src/backend/requirements.in @@ -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 diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 7e85197112..57ec9e7581 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -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 diff --git a/src/frontend/lib/enums/ApiEndpoints.tsx b/src/frontend/lib/enums/ApiEndpoints.tsx index 844e86399a..fc1e9bf30b 100644 --- a/src/frontend/lib/enums/ApiEndpoints.tsx +++ b/src/frontend/lib/enums/ApiEndpoints.tsx @@ -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/', diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index 454225c5b5..0048ad33f0 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -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: , content: , hidden: !user.hasViewRole(UserRoles.admin) + }, + { + name: 'identity', + label: t`Identity`, + icon: , + content: , + hidden: !user.hasViewRole(UserRoles.admin) } ]; }, [user]); @@ -273,6 +285,7 @@ export default function AdminCenter() { label: t`Operations`, panelIDs: [ 'user', + 'identity', 'barcode-history', 'background', 'errors', diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/ScimManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/ScimManagementPanel.tsx new file mode 100644 index 0000000000..5e11a45956 --- /dev/null +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/ScimManagementPanel.tsx @@ -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(''); + 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 ; + } + + return ( + + {t`SCIM Bearer Secret`}} + centered + data-testid='scim-secret-modal' + > + + + 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. + + + + + + {secret} + + + + + + + } color='blue'> + + 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. + + + + + + + + Status + + + {data?.enabled ? ( + + Enabled + + ) : ( + + Disabled + + )} + + + + + Base URL + + + + {data?.base_url} + + + + + + + Secret Generated + + {data?.secret_generated ?? '-'} + + + + Last Used + + {data?.last_used ?? '-'} + + +
+ + + + + + {data?.enabled && ( + + )} + + + + + Rotating the secret immediately invalidates the previous one - update + your Identity Provider's configuration straight away. + + +
+ ); +} diff --git a/tasks.py b/tasks.py index 7156191e90..cfed52187f 100644 --- a/tasks.py +++ b/tasks.py @@ -334,6 +334,7 @@ def builtin_apps(): 'generic', 'machine', 'web', + 'scim', ]