From 7596afd6ceb7177e081efb070dcde541ec938bd8 Mon Sep 17 00:00:00 2001 From: Jan Burgmeier Date: Wed, 11 Jun 2025 12:02:28 +0200 Subject: [PATCH 1/3] Use Union instead of tuple for creating a Resource with multiple extension Using a tuple results in a pydantic error: File "/usr/local/lib/python3.11/site-packages/pydantic/_internal/_generics.py", line 373, in map_generic_model_arguments raise TypeError(f'Too many arguments for {cls}; actual {len(args)}, expected {expected_len}') TypeError: Too many arguments for ; actual 3, expected 1 --- scim2_client/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scim2_client/client.py b/scim2_client/client.py index 41464ed..c0d8fb8 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -552,13 +552,13 @@ def build_resource_models( for schema, resource_type in resource_types_by_schema.items(): schema_obj = schema_objs_by_schema[schema] model = Resource.from_schema(schema_obj) - extensions = [] + extensions = () for ext_schema in resource_type.schema_extensions or []: schema_obj = schema_objs_by_schema[ext_schema.schema_] extension = Extension.from_schema(schema_obj) - extensions.append(extension) + extensions = extensions + (extension,) if extensions: - model = model[tuple(extensions)] + model = model[Union[extensions]] resource_models.append(model) return tuple(resource_models) From 69e4f024c76c02e3d4ba8636a4af3a1705b54f3b Mon Sep 17 00:00:00 2001 From: Jan Burgmeier Date: Mon, 30 Jun 2025 14:33:06 +0200 Subject: [PATCH 2/3] Add unit test for discovering resource types with multiple extensions --- scim2_client/client.py | 3 +- tests/test_discovery.py | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/test_discovery.py diff --git a/scim2_client/client.py b/scim2_client/client.py index c0d8fb8..c9333f0 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -2,6 +2,7 @@ import sys from collections.abc import Collection from dataclasses import dataclass +from typing import Any from typing import Optional from typing import Union @@ -552,7 +553,7 @@ def build_resource_models( for schema, resource_type in resource_types_by_schema.items(): schema_obj = schema_objs_by_schema[schema] model = Resource.from_schema(schema_obj) - extensions = () + extensions: tuple[Any, ...] = () for ext_schema in resource_type.schema_extensions or []: schema_obj = schema_objs_by_schema[ext_schema.schema_] extension = Extension.from_schema(schema_obj) diff --git a/tests/test_discovery.py b/tests/test_discovery.py new file mode 100644 index 0000000..8d6221a --- /dev/null +++ b/tests/test_discovery.py @@ -0,0 +1,90 @@ +import threading +import wsgiref.simple_server +from typing import Annotated +from typing import Union + +import portpicker +import pytest +from httpx import Client +from scim2_models import EnterpriseUser +from scim2_models import Extension +from scim2_models import Group +from scim2_models import Meta +from scim2_models import Required +from scim2_models import ResourceType +from scim2_models import User + +from scim2_client.engines.httpx import SyncSCIMClient + +scim2_server = pytest.importorskip("scim2_server") +from scim2_server.backend import InMemoryBackend # noqa: E402 +from scim2_server.provider import SCIMProvider # noqa: E402 + + +class OtherExtension(Extension): + schemas: Annotated[list[str], Required.true] = [ + "urn:ietf:params:scim:schemas:extension:Other:1.0:User" + ] + + test: str | None = None + test2: list[str] | None = None + + +def get_schemas(): + schemas = [ + User.to_schema(), + Group.to_schema(), + OtherExtension.to_schema(), + EnterpriseUser.to_schema(), + ] + + # SCIMProvider register_schema requires meta object to be set + for schema in schemas: + schema.meta = Meta(resource_type="Schema") + + return schemas + + +def get_resource_types(): + resource_types = [ + ResourceType.from_resource(User[Union[EnterpriseUser, OtherExtension]]), + ResourceType.from_resource(Group), + ] + + # SCIMProvider register_resource_type requires meta object to be set + for resource_type in resource_types: + resource_type.meta = Meta(resource_type="ResourceType") + + return resource_types + + +@pytest.fixture(scope="session") +def server(): + backend = InMemoryBackend() + provider = SCIMProvider(backend) + for schema in get_schemas(): + provider.register_schema(schema) + for resource_type in get_resource_types(): + provider.register_resource_type(resource_type) + + host = "localhost" + port = portpicker.pick_unused_port() + httpd = wsgiref.simple_server.make_server(host, port, provider) + + server_thread = threading.Thread(target=httpd.serve_forever) + server_thread.start() + try: + yield host, port + finally: + httpd.shutdown() + server_thread.join() + + +def test_discovery_resource_types_multiple_extensions(server): + host, port = server + client = Client(base_url=f"http://{host}:{port}") + scim_client = SyncSCIMClient(client) + + scim_client.discover() + assert scim_client.get_resource_model("User") + assert scim_client.get_resource_model("Group") From c6009dac93df38617d7d7c6ed2e1ec63b8120c30 Mon Sep 17 00:00:00 2001 From: Jan Burgmeier Date: Thu, 3 Jul 2025 11:30:18 +0200 Subject: [PATCH 3/3] Fix check_resource_model if resource_models was created from schemas --- scim2_client/client.py | 13 +++++++++++-- tests/test_discovery.py | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/scim2_client/client.py b/scim2_client/client.py index c9333f0..cc47158 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -183,9 +183,18 @@ def get_resource_model(self, name: str) -> Optional[type[Resource]]: def check_resource_model( self, resource_model: type[Resource], payload=None ) -> None: + # We need to check the actual schema names, comapring the class + # types does not work because if the resource_models are + # discovered. The classes might differ: + # vs + schema_to_check = resource_model.model_fields["schemas"].default[0] + for element in self.resource_models: + schema = element.model_fields["schemas"].default[0] + if schema_to_check == schema: + return + if ( - resource_model not in self.resource_models - and resource_model not in CONFIG_RESOURCES + resource_model not in CONFIG_RESOURCES ): raise SCIMRequestError( f"Unknown resource type: '{resource_model}'", source=payload diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 8d6221a..dca5b6f 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -88,3 +88,7 @@ def test_discovery_resource_types_multiple_extensions(server): scim_client.discover() assert scim_client.get_resource_model("User") assert scim_client.get_resource_model("Group") + + # Try to create a user to see if discover filled everything correctly + user_request = User[Union[EnterpriseUser, OtherExtension]](user_name="bjensen@example.com") + scim_client.create(user_request)