scim2-server 0.7.0__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scim2_server-0.7.0 → scim2_server-0.7.1}/PKG-INFO +2 -2
- {scim2_server-0.7.0 → scim2_server-0.7.1}/pyproject.toml +3 -3
- {scim2_server-0.7.0 → scim2_server-0.7.1}/pyproject.toml.orig +3 -3
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/handler.py +7 -2
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/memory.py +21 -3
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/storage.py +71 -2
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testing.py +63 -2
- {scim2_server-0.7.0 → scim2_server-0.7.1}/LICENSE +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/README.md +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/__init__.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/__init__.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/asgi.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/base.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/wsgi.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/bulk.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/conditions.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/errors.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/py.typed +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/requests.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/resources/default-resource-types.json +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/resources/default-schemas.json +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/resources/default-service-provider-config.json +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/responses.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/routing.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/service.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/__init__.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/application.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/cli.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/precis.py +0 -0
- {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: scim2-server
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: SCIM server core for any storage and web framework
|
|
5
5
|
Keywords: scim,scim2,provisioning,rfc7643,rfc7644
|
|
6
6
|
Author: Yaal Coop, Christian Friedrich Coors
|
|
@@ -217,7 +217,7 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
217
217
|
Classifier: Environment :: Web Environment
|
|
218
218
|
Classifier: Programming Language :: Python
|
|
219
219
|
Classifier: Operating System :: OS Independent
|
|
220
|
-
Requires-Dist: scim2-models>=0.12.
|
|
220
|
+
Requires-Dist: scim2-models>=0.12.1
|
|
221
221
|
Requires-Dist: precis-i18n>=1.1.2 ; extra == 'precis'
|
|
222
222
|
Requires-Dist: pytest>=9.1.1 ; extra == 'testing'
|
|
223
223
|
Requires-Python: >=3.11
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-server"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.1"
|
|
8
8
|
description = "SCIM server core for any storage and web framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
"Operating System :: OS Independent",
|
|
29
29
|
]
|
|
30
30
|
requires-python = ">= 3.11"
|
|
31
|
-
dependencies = ["scim2-models>=0.12.
|
|
31
|
+
dependencies = ["scim2-models>=0.12.1"]
|
|
32
32
|
|
|
33
33
|
[[project.authors]]
|
|
34
34
|
name = "Yaal Coop"
|
|
@@ -63,7 +63,7 @@ dev = [
|
|
|
63
63
|
"aiosqlite>=0.22.1",
|
|
64
64
|
"precis-i18n>=1.1.2",
|
|
65
65
|
"mypy>=2.3.1",
|
|
66
|
-
"scim2-client
|
|
66
|
+
"scim2-client>=0.12.0",
|
|
67
67
|
"scim2-tester>=0.5.1",
|
|
68
68
|
"werkzeug>=3.0.3",
|
|
69
69
|
]
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-server"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.1"
|
|
8
8
|
description = "SCIM server core for any storage and web framework"
|
|
9
9
|
authors = [
|
|
10
10
|
{name="Yaal Coop", email="contact@yaal.coop"},
|
|
@@ -29,7 +29,7 @@ classifiers = [
|
|
|
29
29
|
|
|
30
30
|
requires-python = ">= 3.11"
|
|
31
31
|
dependencies = [
|
|
32
|
-
"scim2-models>=0.12.
|
|
32
|
+
"scim2-models>=0.12.1",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
@@ -58,7 +58,7 @@ dev = [
|
|
|
58
58
|
"aiosqlite>=0.22.1",
|
|
59
59
|
"precis-i18n>=1.1.2",
|
|
60
60
|
"mypy>=2.3.1",
|
|
61
|
-
"scim2-client
|
|
61
|
+
"scim2-client>=0.12.0",
|
|
62
62
|
"scim2-tester>=0.5.1",
|
|
63
63
|
"werkzeug>=3.0.3",
|
|
64
64
|
]
|
|
@@ -15,6 +15,7 @@ from scim2_server.routing import Operation
|
|
|
15
15
|
from scim2_server.service import ScimService
|
|
16
16
|
from scim2_server.storage import AsyncScimStorage
|
|
17
17
|
from scim2_server.storage import ScimStorage
|
|
18
|
+
from scim2_server.storage import projection
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class ScimHandler:
|
|
@@ -75,7 +76,9 @@ class ScimHandler:
|
|
|
75
76
|
)
|
|
76
77
|
with self.storage.operation():
|
|
77
78
|
resource = self.storage.get(
|
|
78
|
-
resource_type,
|
|
79
|
+
resource_type,
|
|
80
|
+
cast(str, target.resource_id),
|
|
81
|
+
**projection(self.storage, response_parameters),
|
|
79
82
|
)
|
|
80
83
|
response = self.service.query_response(
|
|
81
84
|
request.base_url,
|
|
@@ -398,7 +401,9 @@ class AsyncScimHandler:
|
|
|
398
401
|
)
|
|
399
402
|
async with self.storage.operation():
|
|
400
403
|
resource = await self.storage.get(
|
|
401
|
-
resource_type,
|
|
404
|
+
resource_type,
|
|
405
|
+
cast(str, target.resource_id),
|
|
406
|
+
**projection(self.storage, response_parameters),
|
|
402
407
|
)
|
|
403
408
|
response = self.service.query_response(
|
|
404
409
|
request.base_url,
|
|
@@ -16,6 +16,7 @@ from scim2_models import Path
|
|
|
16
16
|
from scim2_models import PreconditionFailedException
|
|
17
17
|
from scim2_models import Resource
|
|
18
18
|
from scim2_models import ResourceType
|
|
19
|
+
from scim2_models import ResponseParameters
|
|
19
20
|
from scim2_models import ScimFilter
|
|
20
21
|
from scim2_models import SearchRequest
|
|
21
22
|
from scim2_models import Uniqueness
|
|
@@ -23,6 +24,7 @@ from scim2_models import UniquenessException
|
|
|
23
24
|
|
|
24
25
|
from scim2_server.storage import AsyncScimStorage
|
|
25
26
|
from scim2_server.storage import ScimStorage
|
|
27
|
+
from scim2_server.storage import projection
|
|
26
28
|
from scim2_server.utils import parametrize
|
|
27
29
|
|
|
28
30
|
|
|
@@ -68,7 +70,13 @@ class InMemoryStorage(ScimStorage):
|
|
|
68
70
|
with self.lock:
|
|
69
71
|
yield
|
|
70
72
|
|
|
71
|
-
def get(
|
|
73
|
+
def get(
|
|
74
|
+
self,
|
|
75
|
+
resource_type: ResourceType,
|
|
76
|
+
resource_id: str,
|
|
77
|
+
*,
|
|
78
|
+
response_parameters: ResponseParameters[Any] | None = None,
|
|
79
|
+
) -> Resource[Any]:
|
|
72
80
|
with self.lock:
|
|
73
81
|
return self.resources[self._index(resource_type, resource_id)].model_copy(
|
|
74
82
|
deep=True
|
|
@@ -236,8 +244,18 @@ class AsyncInMemoryStorage(AsyncScimStorage):
|
|
|
236
244
|
"""The stored resources."""
|
|
237
245
|
return self.storage.resources
|
|
238
246
|
|
|
239
|
-
async def get(
|
|
240
|
-
|
|
247
|
+
async def get(
|
|
248
|
+
self,
|
|
249
|
+
resource_type: ResourceType,
|
|
250
|
+
resource_id: str,
|
|
251
|
+
*,
|
|
252
|
+
response_parameters: ResponseParameters[Any] | None = None,
|
|
253
|
+
) -> Resource[Any]:
|
|
254
|
+
return self.storage.get(
|
|
255
|
+
resource_type,
|
|
256
|
+
resource_id,
|
|
257
|
+
**projection(self.storage, response_parameters),
|
|
258
|
+
)
|
|
241
259
|
|
|
242
260
|
async def search(
|
|
243
261
|
self, resource_types: list[ResourceType], search_request: SearchRequest[Any]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import warnings
|
|
1
3
|
from abc import ABC
|
|
2
4
|
from abc import abstractmethod
|
|
3
5
|
from contextlib import AbstractAsyncContextManager
|
|
@@ -7,9 +9,39 @@ from typing import Any
|
|
|
7
9
|
|
|
8
10
|
from scim2_models import Resource
|
|
9
11
|
from scim2_models import ResourceType
|
|
12
|
+
from scim2_models import ResponseParameters
|
|
10
13
|
from scim2_models import SearchRequest
|
|
11
14
|
|
|
12
15
|
|
|
16
|
+
def takes_response_parameters(storage_class: type) -> bool:
|
|
17
|
+
"""Tell whether the ``get`` method of a storage accepts ``response_parameters``, and warn when it does not."""
|
|
18
|
+
parameters = inspect.signature(storage_class.get).parameters.values() # type: ignore[attr-defined]
|
|
19
|
+
if any(
|
|
20
|
+
parameter.name == "response_parameters"
|
|
21
|
+
or parameter.kind is inspect.Parameter.VAR_KEYWORD
|
|
22
|
+
for parameter in parameters
|
|
23
|
+
):
|
|
24
|
+
return True
|
|
25
|
+
warnings.warn(
|
|
26
|
+
f"{storage_class.__qualname__}.get should accept a response_parameters "
|
|
27
|
+
"keyword argument. scim2-server 0.9 will require it.",
|
|
28
|
+
DeprecationWarning,
|
|
29
|
+
# takes_response_parameters, __init_subclass__, ABCMeta.__new__, then the class statement
|
|
30
|
+
stacklevel=4,
|
|
31
|
+
)
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def projection(
|
|
36
|
+
storage: "ScimStorage | AsyncScimStorage",
|
|
37
|
+
response_parameters: ResponseParameters[Any] | None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Return the arguments that pass the response parameters to the get method of a storage, if it accepts them."""
|
|
40
|
+
if not storage._get_takes_response_parameters:
|
|
41
|
+
return {}
|
|
42
|
+
return {"response_parameters": response_parameters}
|
|
43
|
+
|
|
44
|
+
|
|
13
45
|
class ScimStorage(ABC):
|
|
14
46
|
"""Where a SCIM server reads and writes its resources.
|
|
15
47
|
|
|
@@ -40,10 +72,35 @@ class ScimStorage(ABC):
|
|
|
40
72
|
calling the storage.
|
|
41
73
|
"""
|
|
42
74
|
|
|
75
|
+
_get_takes_response_parameters = True
|
|
76
|
+
|
|
77
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
78
|
+
super().__init_subclass__(**kwargs)
|
|
79
|
+
cls._get_takes_response_parameters = takes_response_parameters(cls)
|
|
80
|
+
|
|
43
81
|
@abstractmethod
|
|
44
|
-
def get(
|
|
82
|
+
def get(
|
|
83
|
+
self,
|
|
84
|
+
resource_type: ResourceType,
|
|
85
|
+
resource_id: str,
|
|
86
|
+
*,
|
|
87
|
+
response_parameters: ResponseParameters[Any] | None = None,
|
|
88
|
+
) -> Resource[Any]:
|
|
45
89
|
"""Return a resource.
|
|
46
90
|
|
|
91
|
+
The server passes ``response_parameters`` when it returns the resource
|
|
92
|
+
to the client, for a GET request. The storage may then leave out the
|
|
93
|
+
attributes the response does not keep, for instance to avoid loading
|
|
94
|
+
the members of a group. It must return every attribute the response
|
|
95
|
+
keeps. :meth:`Path.iter_paths <scim2_models.Path.iter_paths>` gives
|
|
96
|
+
them. The server applies the parameters to the response anyway.
|
|
97
|
+
Without ``response_parameters``, the storage returns the whole
|
|
98
|
+
resource.
|
|
99
|
+
|
|
100
|
+
A ``get`` method without ``response_parameters`` is deprecated, and
|
|
101
|
+
raises a :class:`DeprecationWarning`. The server then does not pass
|
|
102
|
+
the parameters. scim2-server 0.9 will require it.
|
|
103
|
+
|
|
47
104
|
:raises ~scim2_models.NotFoundException: When no resource of this type
|
|
48
105
|
has this identifier.
|
|
49
106
|
"""
|
|
@@ -135,8 +192,20 @@ class AsyncScimStorage(ABC):
|
|
|
135
192
|
which :class:`~scim2_server.testing.AsyncScimStorageContract` checks.
|
|
136
193
|
"""
|
|
137
194
|
|
|
195
|
+
_get_takes_response_parameters = True
|
|
196
|
+
|
|
197
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
198
|
+
super().__init_subclass__(**kwargs)
|
|
199
|
+
cls._get_takes_response_parameters = takes_response_parameters(cls)
|
|
200
|
+
|
|
138
201
|
@abstractmethod
|
|
139
|
-
async def get(
|
|
202
|
+
async def get(
|
|
203
|
+
self,
|
|
204
|
+
resource_type: ResourceType,
|
|
205
|
+
resource_id: str,
|
|
206
|
+
*,
|
|
207
|
+
response_parameters: ResponseParameters[Any] | None = None,
|
|
208
|
+
) -> Resource[Any]:
|
|
140
209
|
"""Return a resource. See :meth:`ScimStorage.get`."""
|
|
141
210
|
|
|
142
211
|
@abstractmethod
|
|
@@ -29,16 +29,19 @@ from typing import Union
|
|
|
29
29
|
from typing import cast
|
|
30
30
|
|
|
31
31
|
import pytest
|
|
32
|
+
from scim2_models import Context
|
|
32
33
|
from scim2_models import NotFoundException
|
|
33
34
|
from scim2_models import PreconditionFailedException
|
|
34
35
|
from scim2_models import Resource
|
|
35
36
|
from scim2_models import ResourceType
|
|
37
|
+
from scim2_models import ResponseParameters
|
|
36
38
|
from scim2_models import ScimProvider
|
|
37
39
|
from scim2_models import SearchRequest
|
|
38
40
|
from scim2_models import UniquenessException
|
|
39
41
|
|
|
40
42
|
from scim2_server.storage import AsyncScimStorage
|
|
41
43
|
from scim2_server.storage import ScimStorage
|
|
44
|
+
from scim2_server.storage import projection
|
|
42
45
|
from scim2_server.utils import load_default_provider
|
|
43
46
|
from scim2_server.utils import parametrize
|
|
44
47
|
|
|
@@ -198,6 +201,52 @@ class ScimStorageContract:
|
|
|
198
201
|
with pytest.raises(NotFoundException):
|
|
199
202
|
storage.get(group_type, user.id)
|
|
200
203
|
|
|
204
|
+
@pytest.mark.parametrize(
|
|
205
|
+
"parameters",
|
|
206
|
+
[
|
|
207
|
+
{"excludedAttributes": "members"},
|
|
208
|
+
{"excludedAttributes": "members.display"},
|
|
209
|
+
{"attributes": "displayName"},
|
|
210
|
+
{"attributes": "members.value"},
|
|
211
|
+
],
|
|
212
|
+
)
|
|
213
|
+
def test_get_keeps_what_the_response_returns(
|
|
214
|
+
self,
|
|
215
|
+
storage: Any,
|
|
216
|
+
user_type: ResourceType,
|
|
217
|
+
group_type: ResourceType,
|
|
218
|
+
user_model: Any,
|
|
219
|
+
group_model: Any,
|
|
220
|
+
parameters: dict[str, str],
|
|
221
|
+
) -> None:
|
|
222
|
+
"""A resource read with response parameters gives the same response as the whole resource."""
|
|
223
|
+
(user,) = self.create_users(storage, user_type, user_model, "bjensen")
|
|
224
|
+
group = storage.create(
|
|
225
|
+
group_type,
|
|
226
|
+
group_model.model_validate(
|
|
227
|
+
{
|
|
228
|
+
"displayName": "Admins",
|
|
229
|
+
"members": [{"value": user.id, "display": "bjensen"}],
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
response_parameters = parametrize(
|
|
234
|
+
ResponseParameters, group_model
|
|
235
|
+
).model_validate(parameters)
|
|
236
|
+
|
|
237
|
+
whole = storage.get(group_type, group.id)
|
|
238
|
+
projected = storage.get(
|
|
239
|
+
group_type, group.id, **projection(storage, response_parameters)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert projected.model_dump(
|
|
243
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
244
|
+
response_parameters=response_parameters,
|
|
245
|
+
) == whole.model_dump(
|
|
246
|
+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
|
|
247
|
+
response_parameters=response_parameters,
|
|
248
|
+
)
|
|
249
|
+
|
|
201
250
|
def test_update_replaces_the_stored_resource(
|
|
202
251
|
self, storage: Any, user_type: ResourceType, user_model: Any
|
|
203
252
|
) -> None:
|
|
@@ -492,8 +541,20 @@ class BlockingStorage(ScimStorage):
|
|
|
492
541
|
def run(self, coroutine: Coroutine[Any, Any, T]) -> T:
|
|
493
542
|
return self.runner.run(coroutine)
|
|
494
543
|
|
|
495
|
-
def get(
|
|
496
|
-
|
|
544
|
+
def get(
|
|
545
|
+
self,
|
|
546
|
+
resource_type: ResourceType,
|
|
547
|
+
resource_id: str,
|
|
548
|
+
*,
|
|
549
|
+
response_parameters: ResponseParameters[Any] | None = None,
|
|
550
|
+
) -> Resource[Any]:
|
|
551
|
+
return self.run(
|
|
552
|
+
self.storage.get(
|
|
553
|
+
resource_type,
|
|
554
|
+
resource_id,
|
|
555
|
+
**projection(self.storage, response_parameters),
|
|
556
|
+
)
|
|
557
|
+
)
|
|
497
558
|
|
|
498
559
|
def search(
|
|
499
560
|
self, resource_types: list[ResourceType], search_request: SearchRequest[Any]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/resources/default-resource-types.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|