scim2-server 0.8.0__tar.gz → 0.8.2__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.8.0 → scim2_server-0.8.2}/PKG-INFO +4 -1
- {scim2_server-0.8.0 → scim2_server-0.8.2}/README.md +2 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/pyproject.toml +8 -1
- {scim2_server-0.8.0 → scim2_server-0.8.2}/pyproject.toml.orig +5 -1
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/testing.py +214 -7
- {scim2_server-0.8.0 → scim2_server-0.8.2}/LICENSE +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/__init__.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/applications/__init__.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/applications/asgi.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/applications/base.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/applications/wsgi.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/bulk.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/conditions.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/cursor.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/errors.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/handler.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/memory.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/py.typed +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/requests.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/resources/default-resource-types.json +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/resources/default-schemas.json +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/resources/default-service-provider-config.json +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/responses.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/routing.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/service.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/storage.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/testserver/__init__.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/testserver/application.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/testserver/cli.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/testserver/precis.py +0 -0
- {scim2_server-0.8.0 → scim2_server-0.8.2}/scim2_server/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: scim2-server
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
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
|
|
@@ -212,6 +212,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
212
212
|
Classifier: Programming Language :: Python :: 3.12
|
|
213
213
|
Classifier: Programming Language :: Python :: 3.13
|
|
214
214
|
Classifier: Programming Language :: Python :: 3.14
|
|
215
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
215
216
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
216
217
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
217
218
|
Classifier: Environment :: Web Environment
|
|
@@ -229,6 +230,8 @@ Provides-Extra: precis
|
|
|
229
230
|
Provides-Extra: testing
|
|
230
231
|
Description-Content-Type: text/markdown
|
|
231
232
|
|
|
233
|
+
<img alt="python-scim logo" src="https://raw.githubusercontent.com/python-scim/.github/refs/heads/main/python-scim.svg" height="72" align="right">
|
|
234
|
+
|
|
232
235
|
# scim2-server
|
|
233
236
|
|
|
234
237
|
A Python library that serves the SCIM protocol over any storage, built upon
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-server"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.2"
|
|
8
8
|
description = "SCIM server core for any storage and web framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
22
|
"Programming Language :: Python :: 3.13",
|
|
23
23
|
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Programming Language :: Python :: 3.15",
|
|
24
25
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
25
26
|
"License :: OSI Approved :: Apache Software License",
|
|
26
27
|
"Environment :: Web Environment",
|
|
@@ -79,6 +80,7 @@ doc = [
|
|
|
79
80
|
]
|
|
80
81
|
|
|
81
82
|
[tool.uv]
|
|
83
|
+
required-version = ">=0.12.13"
|
|
82
84
|
exclude-newer = "14 days"
|
|
83
85
|
|
|
84
86
|
[tool.uv.exclude-newer-package]
|
|
@@ -166,6 +168,7 @@ env_list = [
|
|
|
166
168
|
"py312",
|
|
167
169
|
"py313",
|
|
168
170
|
"py314",
|
|
171
|
+
"py315",
|
|
169
172
|
"minversions",
|
|
170
173
|
"doc",
|
|
171
174
|
"coverage",
|
|
@@ -196,6 +199,10 @@ uv_resolution = "lowest-direct"
|
|
|
196
199
|
|
|
197
200
|
[tool.tox.env.doc]
|
|
198
201
|
dependency_groups = ["doc"]
|
|
202
|
+
extras = [
|
|
203
|
+
"cursor",
|
|
204
|
+
"testing",
|
|
205
|
+
]
|
|
199
206
|
commands = [[
|
|
200
207
|
"sphinx-build",
|
|
201
208
|
"-W",
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-server"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.2"
|
|
8
8
|
description = "SCIM server core for any storage and web framework"
|
|
9
9
|
authors = [
|
|
10
10
|
{name="Yaal Coop", email="contact@yaal.coop"},
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.12",
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
22
|
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Programming Language :: Python :: 3.15",
|
|
23
24
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
25
|
"License :: OSI Approved :: Apache Software License",
|
|
25
26
|
"Environment :: Web Environment",
|
|
@@ -76,6 +77,7 @@ doc = [
|
|
|
76
77
|
]
|
|
77
78
|
|
|
78
79
|
[tool.uv]
|
|
80
|
+
required-version = ">=0.12.13"
|
|
79
81
|
exclude-newer = "14 days"
|
|
80
82
|
exclude-newer-package = { scim2-models = false, scim2-client = false, scim2-server = false, scim2-cli = false, scim2-tester = false, pytest-scim2-server = false }
|
|
81
83
|
|
|
@@ -155,6 +157,7 @@ env_list = [
|
|
|
155
157
|
"py312",
|
|
156
158
|
"py313",
|
|
157
159
|
"py314",
|
|
160
|
+
"py315",
|
|
158
161
|
"minversions",
|
|
159
162
|
"doc",
|
|
160
163
|
"coverage",
|
|
@@ -179,6 +182,7 @@ uv_resolution = "lowest-direct"
|
|
|
179
182
|
|
|
180
183
|
[tool.tox.env.doc]
|
|
181
184
|
dependency_groups = ["doc"]
|
|
185
|
+
extras = ["cursor", "testing"]
|
|
182
186
|
commands = [
|
|
183
187
|
["sphinx-build", "-W", "--keep-going", "--builder", "html", "doc", "build/sphinx/html"],
|
|
184
188
|
]
|
|
@@ -29,12 +29,16 @@ from typing import Union
|
|
|
29
29
|
from typing import cast
|
|
30
30
|
|
|
31
31
|
import pytest
|
|
32
|
+
from pydantic import ValidationError
|
|
32
33
|
from scim2_models import Context
|
|
34
|
+
from scim2_models import InvalidFilterException
|
|
35
|
+
from scim2_models import InvalidPathException
|
|
33
36
|
from scim2_models import NotFoundException
|
|
34
37
|
from scim2_models import PreconditionFailedException
|
|
35
38
|
from scim2_models import Resource
|
|
36
39
|
from scim2_models import ResourceType
|
|
37
40
|
from scim2_models import ResponseParameters
|
|
41
|
+
from scim2_models import ScimFilter
|
|
38
42
|
from scim2_models import ScimProvider
|
|
39
43
|
from scim2_models import SearchRequest
|
|
40
44
|
from scim2_models import UniquenessException
|
|
@@ -50,6 +54,100 @@ from scim2_server.utils import parametrize
|
|
|
50
54
|
|
|
51
55
|
T = TypeVar("T")
|
|
52
56
|
|
|
57
|
+
ENTERPRISE = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
|
58
|
+
|
|
59
|
+
SEARCH_USERS: list[dict[str, Any]] = [
|
|
60
|
+
{
|
|
61
|
+
"userName": "alice",
|
|
62
|
+
"displayName": "Alice",
|
|
63
|
+
"title": "Boss",
|
|
64
|
+
"name": {"givenName": "Alice", "familyName": "Liddell"},
|
|
65
|
+
"active": True,
|
|
66
|
+
"emails": [
|
|
67
|
+
{"value": "alice@home.example", "type": "home"},
|
|
68
|
+
{"value": "alice@work.example", "type": "work", "primary": True},
|
|
69
|
+
],
|
|
70
|
+
ENTERPRISE: {"employeeNumber": "100"},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"userName": "Bob",
|
|
74
|
+
"active": False,
|
|
75
|
+
"emails": [{"value": "bob@work.example", "type": "work"}],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"userName": "carol",
|
|
79
|
+
"title": "",
|
|
80
|
+
"name": {"familyName": "smith"},
|
|
81
|
+
ENTERPRISE: {"employeeNumber": "2"},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"userName": "dave_%",
|
|
85
|
+
"title": "50% off",
|
|
86
|
+
"emails": [{"value": "dave@home.example"}],
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
"""The users the filter and sort tests search, with the case, missing values and
|
|
90
|
+
``LIKE`` wildcards a translation into a query tends to get wrong."""
|
|
91
|
+
|
|
92
|
+
SEARCH_FILTERS = [
|
|
93
|
+
'userName eq "ALICE"',
|
|
94
|
+
'userName ne "alice"',
|
|
95
|
+
'userName sw "dave_"',
|
|
96
|
+
'userName ew "_%"',
|
|
97
|
+
'userName co "a"',
|
|
98
|
+
'userName gt "b"',
|
|
99
|
+
'userName le "carol"',
|
|
100
|
+
'title eq "boss"',
|
|
101
|
+
'title ne "Boss"',
|
|
102
|
+
'not (title eq "Boss")',
|
|
103
|
+
"title eq null",
|
|
104
|
+
"title ne null",
|
|
105
|
+
"title gt null",
|
|
106
|
+
"title pr",
|
|
107
|
+
'title co "%"',
|
|
108
|
+
'title lt "c"',
|
|
109
|
+
"displayName pr",
|
|
110
|
+
"name pr",
|
|
111
|
+
"name.givenName pr",
|
|
112
|
+
'name.familyName eq "SMITH"',
|
|
113
|
+
"active eq true",
|
|
114
|
+
"active eq false",
|
|
115
|
+
"not (active eq true)",
|
|
116
|
+
"active ne true",
|
|
117
|
+
'meta.created co "20"',
|
|
118
|
+
"active pr",
|
|
119
|
+
"emails pr",
|
|
120
|
+
'emails eq "alice@work.example"',
|
|
121
|
+
'emails ne "alice@work.example"',
|
|
122
|
+
'emails co "WORK"',
|
|
123
|
+
'emails.type eq "work"',
|
|
124
|
+
"emails.type pr",
|
|
125
|
+
"emails.primary eq true",
|
|
126
|
+
'emails[type eq "work" and value co "alice"]',
|
|
127
|
+
'emails[type eq "home" and primary eq true]',
|
|
128
|
+
'emails[value ne "alice@work.example"]',
|
|
129
|
+
'emails[not (type eq "work")]',
|
|
130
|
+
"emails[type pr]",
|
|
131
|
+
'meta.created gt "2000-01-01T00:00:00Z"',
|
|
132
|
+
'meta.lastModified lt "2000-01-01T00:00:00Z"',
|
|
133
|
+
f'{ENTERPRISE}:employeeNumber eq "100"',
|
|
134
|
+
f'{ENTERPRISE}:employeeNumber gt "10"',
|
|
135
|
+
'title eq "Boss" or userName eq "Bob"',
|
|
136
|
+
'title eq "Boss" and active eq false',
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
SEARCH_SORTS = [
|
|
140
|
+
"userName",
|
|
141
|
+
"title",
|
|
142
|
+
"displayName",
|
|
143
|
+
"name.familyName",
|
|
144
|
+
"active",
|
|
145
|
+
"emails",
|
|
146
|
+
"emails.type",
|
|
147
|
+
"meta.created",
|
|
148
|
+
f"{ENTERPRISE}:employeeNumber",
|
|
149
|
+
]
|
|
150
|
+
|
|
53
151
|
|
|
54
152
|
class ScimStorageContract:
|
|
55
153
|
"""The rules every :class:`~scim2_server.storage.ScimStorage` follows.
|
|
@@ -135,6 +233,18 @@ class ScimStorageContract:
|
|
|
135
233
|
for user_name in user_names
|
|
136
234
|
]
|
|
137
235
|
|
|
236
|
+
def create_group(
|
|
237
|
+
self,
|
|
238
|
+
storage: ScimStorage,
|
|
239
|
+
group_type: ResourceType,
|
|
240
|
+
group_model: Any,
|
|
241
|
+
member: Any,
|
|
242
|
+
) -> Any:
|
|
243
|
+
return storage.create(
|
|
244
|
+
group_type,
|
|
245
|
+
group_model(display_name="admins", members=[{"value": member.id}]),
|
|
246
|
+
)
|
|
247
|
+
|
|
138
248
|
def test_create_fills_the_identifier_and_the_meta(
|
|
139
249
|
self, storage: Any, user_type: ResourceType, user_model: Any
|
|
140
250
|
) -> None:
|
|
@@ -400,8 +510,8 @@ class ScimStorageContract:
|
|
|
400
510
|
group_model: Any,
|
|
401
511
|
) -> None:
|
|
402
512
|
"""A search on a resource type returns its resources only, and counts them."""
|
|
403
|
-
self.create_users(storage, user_type, user_model, "alice", "bob")
|
|
404
|
-
|
|
513
|
+
users = self.create_users(storage, user_type, user_model, "alice", "bob")
|
|
514
|
+
self.create_group(storage, group_type, group_model, users[0])
|
|
405
515
|
|
|
406
516
|
page = self.search(storage, [user_type], self.search_request([user_model]))
|
|
407
517
|
|
|
@@ -497,8 +607,8 @@ class ScimStorageContract:
|
|
|
497
607
|
) -> None:
|
|
498
608
|
"""A search on several resource types returns their resources as one collection."""
|
|
499
609
|
self.require(self.supports_root_search, "searching at the root")
|
|
500
|
-
self.create_users(storage, user_type, user_model, "alice")
|
|
501
|
-
|
|
610
|
+
(user,) = self.create_users(storage, user_type, user_model, "alice")
|
|
611
|
+
self.create_group(storage, group_type, group_model, user)
|
|
502
612
|
|
|
503
613
|
page = self.search(
|
|
504
614
|
storage,
|
|
@@ -524,8 +634,8 @@ class ScimStorageContract:
|
|
|
524
634
|
"""An attribute a resource type does not declare matches none of its resources."""
|
|
525
635
|
self.require(self.supports_root_search, "searching at the root")
|
|
526
636
|
self.require(self.announces(provider, "filter"), "filtering")
|
|
527
|
-
self.create_users(storage, user_type, user_model, "alice")
|
|
528
|
-
|
|
637
|
+
(user,) = self.create_users(storage, user_type, user_model, "alice")
|
|
638
|
+
self.create_group(storage, group_type, group_model, user)
|
|
529
639
|
|
|
530
640
|
page = self.search(
|
|
531
641
|
storage,
|
|
@@ -536,6 +646,94 @@ class ScimStorageContract:
|
|
|
536
646
|
assert page.total == 1
|
|
537
647
|
assert page.resources[0].user_name == "alice"
|
|
538
648
|
|
|
649
|
+
def search_users(self) -> list[dict[str, Any]]:
|
|
650
|
+
"""Return the users the filter and sort tests search.
|
|
651
|
+
|
|
652
|
+
Override it to leave out the attributes the storage does not store.
|
|
653
|
+
"""
|
|
654
|
+
return SEARCH_USERS
|
|
655
|
+
|
|
656
|
+
def search_request_or_skip(
|
|
657
|
+
self, models: list[type[Resource[Any]]], **parameters: Any
|
|
658
|
+
) -> SearchRequest[Any]:
|
|
659
|
+
"""Build a search request, and skip the current test when the models do not declare an attribute of the filter or the sort."""
|
|
660
|
+
try:
|
|
661
|
+
return self.search_request(models, **parameters)
|
|
662
|
+
except ValidationError as exception:
|
|
663
|
+
errors = exception.errors()
|
|
664
|
+
if any(
|
|
665
|
+
error["type"] not in ("scim_invalidFilter", "scim_invalidPath")
|
|
666
|
+
for error in errors
|
|
667
|
+
):
|
|
668
|
+
raise
|
|
669
|
+
pytest.skip(f"The models refuse the search: {errors[0]['msg']}")
|
|
670
|
+
|
|
671
|
+
def search_or_skip(
|
|
672
|
+
self,
|
|
673
|
+
storage: Any,
|
|
674
|
+
resource_types: list[ResourceType],
|
|
675
|
+
search_request: SearchRequest[Any],
|
|
676
|
+
) -> Any:
|
|
677
|
+
"""Search a storage, and skip the current test when the storage refuses the filter or the sort."""
|
|
678
|
+
try:
|
|
679
|
+
return self.search(storage, resource_types, search_request)
|
|
680
|
+
except (InvalidFilterException, InvalidPathException) as exception:
|
|
681
|
+
pytest.skip(f"The storage refuses the search: {exception.detail}")
|
|
682
|
+
|
|
683
|
+
@pytest.mark.parametrize("scim_filter", SEARCH_FILTERS)
|
|
684
|
+
def test_search_filters_as_the_filter_evaluator(
|
|
685
|
+
self,
|
|
686
|
+
storage: Any,
|
|
687
|
+
provider: ScimProvider,
|
|
688
|
+
user_type: ResourceType,
|
|
689
|
+
user_model: Any,
|
|
690
|
+
scim_filter: str,
|
|
691
|
+
) -> None:
|
|
692
|
+
"""A filter keeps the resources that ScimFilter.match keeps, and the total counts them."""
|
|
693
|
+
self.require(self.announces(provider, "filter"), "filtering")
|
|
694
|
+
for user in self.search_users():
|
|
695
|
+
storage.create(user_type, user_model.model_validate(user))
|
|
696
|
+
stored = self.search(storage, [user_type], self.search_request([user_model]))
|
|
697
|
+
search_request = self.search_request_or_skip([user_model], filter=scim_filter)
|
|
698
|
+
|
|
699
|
+
page = self.search_or_skip(storage, [user_type], search_request)
|
|
700
|
+
|
|
701
|
+
evaluator = cast(ScimFilter[Any], search_request.filter)
|
|
702
|
+
expected = [r.id for r in stored.resources if evaluator.match(r)]
|
|
703
|
+
assert page.total == len(expected)
|
|
704
|
+
assert sorted(r.id for r in page.resources) == sorted(expected)
|
|
705
|
+
|
|
706
|
+
@pytest.mark.parametrize("sort_order", ["ascending", "descending"])
|
|
707
|
+
@pytest.mark.parametrize("sort_by", SEARCH_SORTS)
|
|
708
|
+
def test_search_sorts_as_search_request_sort(
|
|
709
|
+
self,
|
|
710
|
+
storage: Any,
|
|
711
|
+
provider: ScimProvider,
|
|
712
|
+
user_type: ResourceType,
|
|
713
|
+
user_model: Any,
|
|
714
|
+
sort_by: str,
|
|
715
|
+
sort_order: str,
|
|
716
|
+
) -> None:
|
|
717
|
+
"""A sorted search orders the resources as SearchRequest.sort does.
|
|
718
|
+
|
|
719
|
+
Resources with the same sort value may come in any order.
|
|
720
|
+
"""
|
|
721
|
+
self.require(self.announces(provider, "sort"), "sorting")
|
|
722
|
+
for user in self.search_users():
|
|
723
|
+
storage.create(user_type, user_model.model_validate(user))
|
|
724
|
+
stored = self.search(storage, [user_type], self.search_request([user_model]))
|
|
725
|
+
search_request = self.search_request_or_skip(
|
|
726
|
+
[user_model], sort_by=sort_by, sort_order=sort_order
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
page = self.search_or_skip(storage, [user_type], search_request)
|
|
730
|
+
|
|
731
|
+
expected = search_request.sort(stored.resources)
|
|
732
|
+
assert sorted(r.id for r in page.resources) == sorted(r.id for r in expected)
|
|
733
|
+
assert [search_request.sort_value(r) for r in page.resources] == [
|
|
734
|
+
search_request.sort_value(r) for r in expected
|
|
735
|
+
]
|
|
736
|
+
|
|
539
737
|
def walk(
|
|
540
738
|
self,
|
|
541
739
|
storage: Any,
|
|
@@ -573,6 +771,7 @@ class ScimStorageContract:
|
|
|
573
771
|
self, storage: Any, user_type: ResourceType, user_model: Any
|
|
574
772
|
) -> None:
|
|
575
773
|
"""Following the next cursors returns every resource once, count by count."""
|
|
774
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
576
775
|
names = ["alice", "bob", "carol", "dave", "erin"]
|
|
577
776
|
self.create_users(storage, user_type, user_model, *names)
|
|
578
777
|
|
|
@@ -593,6 +792,7 @@ class ScimStorageContract:
|
|
|
593
792
|
user_model: Any,
|
|
594
793
|
) -> None:
|
|
595
794
|
"""The pages of a sorted search follow each other in the sort order."""
|
|
795
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
596
796
|
self.require(self.announces(provider, "sort"), "sorting")
|
|
597
797
|
self.create_users(
|
|
598
798
|
storage, user_type, user_model, "dave", "alice", "carol", "bob"
|
|
@@ -625,7 +825,9 @@ class ScimStorageContract:
|
|
|
625
825
|
user_model: Any,
|
|
626
826
|
) -> None:
|
|
627
827
|
"""Resources with the same sort value, or without one, are returned once, even across pages."""
|
|
828
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
628
829
|
self.require(self.announces(provider, "sort"), "sorting")
|
|
830
|
+
self.require("nick_name" in user_model.model_fields, "nickName")
|
|
629
831
|
for user_name, nick_name in [
|
|
630
832
|
("alice", "twin"),
|
|
631
833
|
("bob", None),
|
|
@@ -651,6 +853,7 @@ class ScimStorageContract:
|
|
|
651
853
|
self, storage: Any, user_type: ResourceType, user_model: Any
|
|
652
854
|
) -> None:
|
|
653
855
|
"""Following the previous cursors from the last page returns the same pages, back to the first."""
|
|
856
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
654
857
|
self.create_users(
|
|
655
858
|
storage, user_type, user_model, "alice", "bob", "carol", "dave", "erin"
|
|
656
859
|
)
|
|
@@ -669,6 +872,7 @@ class ScimStorageContract:
|
|
|
669
872
|
self, storage: Any, user_type: ResourceType, user_model: Any, deleted: str
|
|
670
873
|
) -> None:
|
|
671
874
|
"""Deleting a resource already returned does not skip nor repeat the next ones."""
|
|
875
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
672
876
|
self.create_users(
|
|
673
877
|
storage, user_type, user_model, "alice", "bob", "carol", "dave", "erin"
|
|
674
878
|
)
|
|
@@ -690,6 +894,7 @@ class ScimStorageContract:
|
|
|
690
894
|
self, storage: Any, user_type: ResourceType, user_model: Any
|
|
691
895
|
) -> None:
|
|
692
896
|
"""Creating a resource between two pages does not skip nor repeat the existing ones."""
|
|
897
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
693
898
|
self.create_users(
|
|
694
899
|
storage, user_type, user_model, "alice", "bob", "carol", "dave", "erin"
|
|
695
900
|
)
|
|
@@ -711,6 +916,7 @@ class ScimStorageContract:
|
|
|
711
916
|
self, storage: Any, user_type: ResourceType, user_model: Any
|
|
712
917
|
) -> None:
|
|
713
918
|
"""Deleting the first resource of a page does not change the page before it."""
|
|
919
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
714
920
|
self.create_users(
|
|
715
921
|
storage, user_type, user_model, "alice", "bob", "carol", "dave", "erin"
|
|
716
922
|
)
|
|
@@ -750,9 +956,10 @@ class ScimStorageContract:
|
|
|
750
956
|
group_model: Any,
|
|
751
957
|
) -> None:
|
|
752
958
|
"""A cursor pages the resources of several resource types as one collection."""
|
|
959
|
+
self.require(storage.supports_cursors, "cursor pagination")
|
|
753
960
|
self.require(self.supports_root_search, "searching at the root")
|
|
754
961
|
users = self.create_users(storage, user_type, user_model, "alice", "bob")
|
|
755
|
-
group =
|
|
962
|
+
group = self.create_group(storage, group_type, group_model, users[0])
|
|
756
963
|
|
|
757
964
|
pages = self.walk(
|
|
758
965
|
storage,
|
|
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
|
|
File without changes
|
|
File without changes
|
{scim2_server-0.8.0 → scim2_server-0.8.2}/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
|
|
File without changes
|