scim2-server 0.6.2__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.6.2 → scim2_server-0.7.1}/PKG-INFO +4 -2
- {scim2_server-0.6.2 → scim2_server-0.7.1}/pyproject.toml +5 -3
- {scim2_server-0.6.2 → scim2_server-0.7.1}/pyproject.toml.orig +7 -3
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/handler.py +7 -2
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/memory.py +27 -6
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/resources/default-service-provider-config.json +1 -2
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/service.py +0 -1
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/storage.py +71 -2
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/testing.py +63 -2
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/testserver/cli.py +20 -1
- scim2_server-0.7.1/scim2_server/testserver/precis.py +29 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/LICENSE +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/README.md +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/__init__.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/applications/__init__.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/applications/asgi.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/applications/base.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/applications/wsgi.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/bulk.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/conditions.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/errors.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/py.typed +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/requests.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/resources/default-resource-types.json +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/resources/default-schemas.json +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/responses.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/routing.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/testserver/__init__.py +0 -0
- {scim2_server-0.6.2 → scim2_server-0.7.1}/scim2_server/testserver/application.py +0 -0
- {scim2_server-0.6.2 → 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.
|
|
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,11 +217,13 @@ 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.
|
|
220
|
+
Requires-Dist: scim2-models>=0.12.1
|
|
221
|
+
Requires-Dist: precis-i18n>=1.1.2 ; extra == 'precis'
|
|
221
222
|
Requires-Dist: pytest>=9.1.1 ; extra == 'testing'
|
|
222
223
|
Requires-Python: >=3.11
|
|
223
224
|
Project-URL: repository, https://github.com/python-scim/scim2-server
|
|
224
225
|
Project-URL: funding, https://github.com/sponsors/python-scim
|
|
226
|
+
Provides-Extra: precis
|
|
225
227
|
Provides-Extra: testing
|
|
226
228
|
Description-Content-Type: text/markdown
|
|
227
229
|
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-server"
|
|
7
|
-
version = "0.
|
|
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.
|
|
31
|
+
dependencies = ["scim2-models>=0.12.1"]
|
|
32
32
|
|
|
33
33
|
[[project.authors]]
|
|
34
34
|
name = "Yaal Coop"
|
|
@@ -43,6 +43,7 @@ file = "LICENSE"
|
|
|
43
43
|
|
|
44
44
|
[project.optional-dependencies]
|
|
45
45
|
testing = ["pytest>=9.1.1"]
|
|
46
|
+
precis = ["precis-i18n>=1.1.2"]
|
|
46
47
|
|
|
47
48
|
[project.urls]
|
|
48
49
|
repository = "https://github.com/python-scim/scim2-server"
|
|
@@ -60,8 +61,9 @@ dev = [
|
|
|
60
61
|
"time-machine>=2.14.2",
|
|
61
62
|
"tox-uv>=1.16.0",
|
|
62
63
|
"aiosqlite>=0.22.1",
|
|
64
|
+
"precis-i18n>=1.1.2",
|
|
63
65
|
"mypy>=2.3.1",
|
|
64
|
-
"scim2-client
|
|
66
|
+
"scim2-client>=0.12.0",
|
|
65
67
|
"scim2-tester>=0.5.1",
|
|
66
68
|
"werkzeug>=3.0.3",
|
|
67
69
|
]
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-server"
|
|
7
|
-
version = "0.
|
|
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,13 +29,16 @@ classifiers = [
|
|
|
29
29
|
|
|
30
30
|
requires-python = ">= 3.11"
|
|
31
31
|
dependencies = [
|
|
32
|
-
"scim2-models>=0.
|
|
32
|
+
"scim2-models>=0.12.1",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
36
|
testing = [
|
|
37
37
|
"pytest>=9.1.1",
|
|
38
38
|
]
|
|
39
|
+
precis = [
|
|
40
|
+
"precis-i18n>=1.1.2",
|
|
41
|
+
]
|
|
39
42
|
|
|
40
43
|
[project.urls]
|
|
41
44
|
repository = "https://github.com/python-scim/scim2-server"
|
|
@@ -53,8 +56,9 @@ dev = [
|
|
|
53
56
|
"time-machine>=2.14.2",
|
|
54
57
|
"tox-uv>=1.16.0",
|
|
55
58
|
"aiosqlite>=0.22.1",
|
|
59
|
+
"precis-i18n>=1.1.2",
|
|
56
60
|
"mypy>=2.3.1",
|
|
57
|
-
"scim2-client
|
|
61
|
+
"scim2-client>=0.12.0",
|
|
58
62
|
"scim2-tester>=0.5.1",
|
|
59
63
|
"werkzeug>=3.0.3",
|
|
60
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
|
|
@@ -203,13 +211,16 @@ class InMemoryStorage(ScimStorage):
|
|
|
203
211
|
"""Return the value a resource holds for a unique attribute, in the form it is compared in.
|
|
204
212
|
|
|
205
213
|
A resource whose schemas do not declare the attribute holds no value.
|
|
214
|
+
A string the policy cannot compare is equal to no other value, so it
|
|
215
|
+
is held as missing too.
|
|
206
216
|
"""
|
|
207
217
|
value = parametrize(Path, type(resource))(attribute.urn).get(
|
|
208
218
|
resource, strict=False
|
|
209
219
|
)
|
|
210
|
-
|
|
211
|
-
return
|
|
212
|
-
|
|
220
|
+
try:
|
|
221
|
+
return attribute.comparable(value)
|
|
222
|
+
except ValueError:
|
|
223
|
+
return None
|
|
213
224
|
|
|
214
225
|
|
|
215
226
|
class AsyncInMemoryStorage(AsyncScimStorage):
|
|
@@ -233,8 +244,18 @@ class AsyncInMemoryStorage(AsyncScimStorage):
|
|
|
233
244
|
"""The stored resources."""
|
|
234
245
|
return self.storage.resources
|
|
235
246
|
|
|
236
|
-
async def get(
|
|
237
|
-
|
|
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
|
+
)
|
|
238
259
|
|
|
239
260
|
async def search(
|
|
240
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]
|
|
@@ -87,6 +87,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
87
87
|
help="Accept a static bearer token, and announce the bearer token scheme. "
|
|
88
88
|
"Can be repeated. Without it, the server accepts every request.",
|
|
89
89
|
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--precis",
|
|
92
|
+
action="store_true",
|
|
93
|
+
help="Compare the usernames and the passwords with the PRECIS profiles of "
|
|
94
|
+
"RFC 8265, and refuse the values they do not allow. Needs the precis extra.",
|
|
95
|
+
)
|
|
90
96
|
parser.add_argument(
|
|
91
97
|
"--hostname",
|
|
92
98
|
default="127.0.0.1",
|
|
@@ -169,7 +175,20 @@ def main() -> None:
|
|
|
169
175
|
BEARER_TOKEN_SCHEME,
|
|
170
176
|
]
|
|
171
177
|
|
|
172
|
-
|
|
178
|
+
policy = None
|
|
179
|
+
if args.precis:
|
|
180
|
+
try:
|
|
181
|
+
# Imported on demand, so that the server runs without the precis extra.
|
|
182
|
+
from scim2_server.testserver.precis import PRECIS_POLICY
|
|
183
|
+
except ModuleNotFoundError:
|
|
184
|
+
parser.error(
|
|
185
|
+
"--precis needs the precis extra: pip install 'scim2-server[precis]'"
|
|
186
|
+
)
|
|
187
|
+
policy = PRECIS_POLICY
|
|
188
|
+
|
|
189
|
+
provider = ScimProvider.from_discovery(
|
|
190
|
+
schemas, resource_types, config=config, policy=policy
|
|
191
|
+
)
|
|
173
192
|
|
|
174
193
|
storages: dict[str | None, InMemoryStorage] = {}
|
|
175
194
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The PRECIS comparison of the ``--precis`` option of the ``scim2-server`` command."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from precis_i18n import get_profile
|
|
6
|
+
from scim2_models import ScimPolicy
|
|
7
|
+
from scim2_models import default_comparison_key
|
|
8
|
+
|
|
9
|
+
USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
|
|
10
|
+
|
|
11
|
+
PROFILES = {
|
|
12
|
+
f"{USER_SCHEMA}:userName": get_profile("UsernameCaseMapped"),
|
|
13
|
+
f"{USER_SCHEMA}:password": get_profile("OpaqueString"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def precis_comparison_key(binding: Any, value: str) -> str:
|
|
18
|
+
"""Compare the usernames and the passwords with the PRECIS profiles of RFC 8265.
|
|
19
|
+
|
|
20
|
+
Per RFC 7644 §5, the usernames and the passwords are prepared with PRECIS
|
|
21
|
+
before they are compared. The other strings are compared as by default.
|
|
22
|
+
"""
|
|
23
|
+
profile = PROFILES.get(binding.urn)
|
|
24
|
+
if profile is None:
|
|
25
|
+
return default_comparison_key(binding, value)
|
|
26
|
+
return str(profile.enforce(value))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
PRECIS_POLICY = ScimPolicy(comparison_key=precis_comparison_key)
|
|
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.6.2 → 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
|