scim2-server 0.4.0__tar.gz → 0.6.0__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.
Files changed (35) hide show
  1. {scim2_server-0.4.0 → scim2_server-0.6.0}/PKG-INFO +74 -59
  2. scim2_server-0.6.0/README.md +99 -0
  3. {scim2_server-0.4.0 → scim2_server-0.6.0}/pyproject.toml +34 -9
  4. {scim2_server-0.4.0 → scim2_server-0.6.0}/pyproject.toml.orig +30 -7
  5. scim2_server-0.6.0/scim2_server/applications/__init__.py +1 -0
  6. scim2_server-0.6.0/scim2_server/applications/asgi.py +147 -0
  7. scim2_server-0.6.0/scim2_server/applications/base.py +97 -0
  8. scim2_server-0.6.0/scim2_server/applications/wsgi.py +234 -0
  9. scim2_server-0.6.0/scim2_server/bulk.py +232 -0
  10. scim2_server-0.6.0/scim2_server/conditions.py +94 -0
  11. scim2_server-0.6.0/scim2_server/errors.py +33 -0
  12. scim2_server-0.6.0/scim2_server/handler.py +676 -0
  13. scim2_server-0.6.0/scim2_server/memory.py +269 -0
  14. scim2_server-0.6.0/scim2_server/requests.py +58 -0
  15. scim2_server-0.6.0/scim2_server/responses.py +23 -0
  16. scim2_server-0.6.0/scim2_server/routing.py +128 -0
  17. scim2_server-0.6.0/scim2_server/service.py +1005 -0
  18. scim2_server-0.6.0/scim2_server/storage.py +174 -0
  19. scim2_server-0.6.0/scim2_server/testing.py +558 -0
  20. scim2_server-0.6.0/scim2_server/testserver/__init__.py +1 -0
  21. scim2_server-0.6.0/scim2_server/testserver/application.py +48 -0
  22. scim2_server-0.6.0/scim2_server/testserver/cli.py +219 -0
  23. {scim2_server-0.4.0 → scim2_server-0.6.0}/scim2_server/utils.py +2 -2
  24. scim2_server-0.4.0/README.md +0 -85
  25. scim2_server-0.4.0/scim2_server/backend.py +0 -302
  26. scim2_server-0.4.0/scim2_server/bulk.py +0 -184
  27. scim2_server-0.4.0/scim2_server/cli.py +0 -180
  28. scim2_server-0.4.0/scim2_server/provider.py +0 -856
  29. scim2_server-0.4.0/scim2_server/tenants.py +0 -87
  30. {scim2_server-0.4.0 → scim2_server-0.6.0}/LICENSE +0 -0
  31. {scim2_server-0.4.0 → scim2_server-0.6.0}/scim2_server/__init__.py +0 -0
  32. {scim2_server-0.4.0 → scim2_server-0.6.0}/scim2_server/py.typed +0 -0
  33. {scim2_server-0.4.0 → scim2_server-0.6.0}/scim2_server/resources/default-resource-types.json +0 -0
  34. {scim2_server-0.4.0 → scim2_server-0.6.0}/scim2_server/resources/default-schemas.json +0 -0
  35. {scim2_server-0.4.0 → scim2_server-0.6.0}/scim2_server/resources/default-service-provider-config.json +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-server
3
- Version: 0.4.0
4
- Summary: Lightweight SCIM2 server prototype
3
+ Version: 0.6.0
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
7
7
  Author-email: Yaal Coop <contact@yaal.coop>, Christian Friedrich Coors <me@ccoors.de>
@@ -217,94 +217,109 @@ 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.10.0
221
- Requires-Dist: werkzeug>=3.0.3
220
+ Requires-Dist: scim2-models>=0.10.3
221
+ Requires-Dist: pytest>=9.1.1 ; extra == 'testing'
222
222
  Requires-Python: >=3.11
223
223
  Project-URL: repository, https://github.com/python-scim/scim2-server
224
224
  Project-URL: funding, https://github.com/sponsors/python-scim
225
+ Provides-Extra: testing
225
226
  Description-Content-Type: text/markdown
226
227
 
227
228
  # scim2-server
228
229
 
229
- This is an example WSGI-SCIM server using [scim2-models](https://github.com/python-scim/scim2-models).
230
- It utilizes [werkzeug](https://werkzeug.palletsprojects.com/) and keeps all resources in-memory,
231
- they are lost once the process exits.
230
+ A Python library that serves the SCIM protocol over any storage, built upon
231
+ [scim2-models](https://scim2-models.readthedocs.io), following the
232
+ [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643.html) and
233
+ [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644.html) specifications.
234
+ It validates the requests, applies them to the resources and builds the responses.
235
+ The application only reads and writes the resources.
236
+
237
+ It comes with an in-memory storage, WSGI and ASGI applications with no dependency, and a
238
+ `scim2-server` command that serves a test server.
239
+
240
+ ## Who is it for?
241
+
242
+ scim2-server is for projects that need to:
243
+
244
+ - serve a standalone SCIM server;
245
+ - add SCIM to an existing application, with SCIM models that describe its data;
246
+ - test a SCIM client against a working server, from the `scim2-server` command, a container, or
247
+ the [pytest-scim2-server](https://github.com/pytest-dev/pytest-scim2-server) fixture.
248
+
249
+ ## What's SCIM anyway?
250
+
251
+ SCIM stands for System for Cross-domain Identity Management, and it is a provisioning protocol.
252
+ Provisioning is the action of managing a set of resources across different services, usually users and groups.
253
+ SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
254
+ It allows users and groups creations, modifications and deletions to be synchronized between applications.
232
255
 
233
256
  ## Features
234
257
 
235
- - [x] Discovery endpoints (`/v2/ServiceProviderConfig`, `/v2/ResourceTypes`, `/v2/Schemas`)
236
- - [x] Create/Read/Update/Delete resources (`POST`, `GET`, `PUT`, `DELETE`)
237
- - [x] Searching & Filtering
238
- - [x] Support for ETags
239
- - [x] Unique Constraints
240
- - [x] HTTP PATCH (Add/Remove/Replace)
241
- - [x] Sorting
242
- - [x] Bulk operations
243
- - [x] Multi-tenancy with a URL prefix
258
+ - **Resources**: creation, read, replacement, PATCH and deletion
259
+ - **Search**: filters, sorting, paging and attribute selection, on a resource type or at the root
260
+ - **Bulk requests**: with `bulkId` references and `failOnErrors`
261
+ - **ETags**: conditional requests and protection against concurrent writes
262
+ - **Discovery**: `ServiceProviderConfig`, `ResourceTypes` and `Schemas`
263
+ - **Storages**: an interface to implement, an in-memory storage, and a test suite that checks a storage
264
+ - **Sync & Async**: a synchronous and an asynchronous handler, over the same rules
265
+ - **Multi-tenancy**: one set of resources per URL prefix
244
266
 
245
- ## Usage
267
+ ## Installation
246
268
 
247
269
  ```shell
248
- $ scim2-server [-h] [--schema SCHEMA] [--resource-type RESOURCE_TYPE] [--service-provider-config SERVICE_PROVIDER_CONFIG] [--bearer-token BEARER_TOKEN] [--hostname HOSTNAME] [--port PORT] [--reverse-proxy] [--dump-resources DUMP_RESOURCES] [--tenant TENANT] [--dynamic-tenants] [--debug]
270
+ pip install scim2-server
249
271
  ```
250
272
 
251
- - `-h`/`--help`: Show help message
252
- - `--reverse-proxy`: Allow using the provider behind a Reverse Proxy (required for URL rewriting).
253
- - `--schema`: Register schemas from specified JSON file. If not provided, loads the default schemas from RFC 7643.
254
- - `--resource-type`: Register resource types from specified JSON file. If not provided, loads the default resource types from RFC 7643.
255
- - `--service-provider-config`: Load the service provider configuration from specified JSON file. If not provided, loads the default configuration.
256
- - `--bearer-token`: Registers a bearer token that can be used for accessing the service, and announces the bearer token authentication scheme. If no tokens are provided, anonymous access without authentication is allowed.
257
- - `--hostname`: The hostname to listen on. Defaults to `127.0.0.1`.
258
- - `--port`: The port to listen on. Defaults to `8080`.
259
- - `--dump-resources`: Dump a JSON document containing all resources when the provider exits normally. With tenants, the document has one entry per tenant.
260
- - `--tenant`: Serve a tenant under `/<tenant>`, for example `/<tenant>/v2/Users`. Can be repeated. See [Multi-tenancy](#multi-tenancy).
261
- - `--dynamic-tenants`: Create a tenant on the first request to `/<tenant>`. See [Multi-tenancy](#multi-tenancy).
262
- - `--debug`: Enable the interactive Werkzeug debugger, the reloader and the logging of the WSGI environment of each request. The debugger allows arbitrary code execution and the environment contains the bearer tokens, so never use this option on a server reachable by others.
263
-
264
- ### Multi-tenancy
273
+ ## Usage
265
274
 
266
- With `--tenant` or `--dynamic-tenants`, the first segment of the URL path selects a tenant (RFC 7644 §6.1).
267
- Each tenant has its own resources: `/a/v2/Users` and `/b/v2/Users` are separate, and uniqueness, filters and pagination only consider the resources of the tenant.
268
- The schemas, the resource types and the service provider configuration are the same for every tenant, and so are the bearer tokens.
269
- A request without a known tenant gets a 404 answer, and `v2` cannot be a tenant name.
275
+ Serve a test server from the command line:
270
276
 
271
277
  ```shell
272
- $ scim2-server --tenant a --tenant b
273
- $ curl http://localhost:8080/a/v2/Users
278
+ scim2-server --port 8080
274
279
  ```
275
280
 
276
- With `--dynamic-tenants`, the first request to an unknown tenant creates it with no resources.
277
- This is useful for tests: each test can pick a random tenant and get an empty server.
278
- Any client can then create tenants, even without a valid bearer token, and every tenant stays in memory until the server exits.
279
- Do not use this option on a server reachable by untrusted clients.
281
+ Or build the WSGI application in Python:
280
282
 
281
- In Python, `scim2_server.tenants.TenantDispatcher` builds one `SCIMApplication` per tenant from a factory.
282
- Override its `select_tenant` method to read the tenant from a header or a sub-domain instead.
283
+ ```python
284
+ from scim2_server.applications.wsgi import WSGIApplication
285
+ from scim2_server.memory import InMemoryStorage
286
+ from scim2_server.utils import load_default_provider
283
287
 
284
- ### Container
288
+ app = WSGIApplication(InMemoryStorage(), load_default_provider())
289
+ ```
285
290
 
286
- A container image is published on the GitHub container registry for each release.
287
- The server listens on `0.0.0.0:8080` inside the container, and the command line arguments are passed to `scim2-server`:
291
+ A container image is published on the GitHub container registry for each release:
288
292
 
289
293
  ```shell
290
- $ docker run --publish 8080:8080 ghcr.io/python-scim/scim2-server --bearer-token secret
294
+ docker run --publish 8080:8080 ghcr.io/python-scim/scim2-server --bearer-token secret
291
295
  ```
292
296
 
293
- To build the image yourself, use `docker build --file Containerfile .` or `podman build .`.
297
+ The server has been tested against a live
298
+ [Microsoft Entra](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/scim-validator-tutorial)
299
+ system and a live
300
+ [Okta](https://developer.okta.com/docs/guides/scim-provisioning-integration-test/main/) system.
294
301
 
295
- ## Notes
302
+ ## Documentation
296
303
 
297
- This provider can be used as a starting point if you want to implement a SCIM provider. You should probably change the following things, if you want to use it in production:
304
+ - [Overview](https://scim2-server.readthedocs.io/en/latest/overview.html) introduces the parts of
305
+ a SCIM server, from the description of the service to the test server.
306
+ - [How-to guides](https://scim2-server.readthedocs.io/en/latest/how-to/index.html) cover focused
307
+ tasks, such as writing a storage or integrating a web framework.
308
+ - [Explanation](https://scim2-server.readthedocs.io/en/latest/explanation/index.html) covers the
309
+ layers of the server, the writes of resources and the bulk requests.
310
+ - [Reference](https://scim2-server.readthedocs.io/en/latest/reference.html) lists the public API
311
+ and the options of the `scim2-server` command.
298
312
 
299
- - Use a proper production WSGI server instead of the one provided by Werkzeug
300
- - Implement your own Backend as a subclass of `scim2_server.backend.Backend`
301
- - Implement proper authorization with OAuth instead of public access or static bearer tokens
302
- - Support the `/Me` endpoint, if it applies in your use case
313
+ ## Contributing
303
314
 
304
- The provider in its current state has been tested successfully against a live
305
- [Microsoft Entra](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/scim-validator-tutorial)
306
- system as well as a live
307
- [Okta](https://developer.okta.com/docs/guides/scim-provisioning-integration-test/main/) system.
315
+ The [contribution page](https://scim2-server.readthedocs.io/en/latest/contributing.html)
316
+ describes how to run the tests, the style checks and the documentation build.
317
+
318
+ scim2-server belongs in a collection of SCIM tools developed by [Yaal Coop](https://yaal.coop),
319
+ with [scim2-models](https://github.com/python-scim/scim2-models),
320
+ [scim2-client](https://github.com/python-scim/scim2-client),
321
+ [scim2-tester](https://github.com/python-scim/scim2-tester) and
322
+ [scim2-cli](https://github.com/python-scim/scim2-cli).
308
323
 
309
324
  ## Origins
310
325
 
@@ -0,0 +1,99 @@
1
+ # scim2-server
2
+
3
+ A Python library that serves the SCIM protocol over any storage, built upon
4
+ [scim2-models](https://scim2-models.readthedocs.io), following the
5
+ [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643.html) and
6
+ [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644.html) specifications.
7
+ It validates the requests, applies them to the resources and builds the responses.
8
+ The application only reads and writes the resources.
9
+
10
+ It comes with an in-memory storage, WSGI and ASGI applications with no dependency, and a
11
+ `scim2-server` command that serves a test server.
12
+
13
+ ## Who is it for?
14
+
15
+ scim2-server is for projects that need to:
16
+
17
+ - serve a standalone SCIM server;
18
+ - add SCIM to an existing application, with SCIM models that describe its data;
19
+ - test a SCIM client against a working server, from the `scim2-server` command, a container, or
20
+ the [pytest-scim2-server](https://github.com/pytest-dev/pytest-scim2-server) fixture.
21
+
22
+ ## What's SCIM anyway?
23
+
24
+ SCIM stands for System for Cross-domain Identity Management, and it is a provisioning protocol.
25
+ Provisioning is the action of managing a set of resources across different services, usually users and groups.
26
+ SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
27
+ It allows users and groups creations, modifications and deletions to be synchronized between applications.
28
+
29
+ ## Features
30
+
31
+ - **Resources**: creation, read, replacement, PATCH and deletion
32
+ - **Search**: filters, sorting, paging and attribute selection, on a resource type or at the root
33
+ - **Bulk requests**: with `bulkId` references and `failOnErrors`
34
+ - **ETags**: conditional requests and protection against concurrent writes
35
+ - **Discovery**: `ServiceProviderConfig`, `ResourceTypes` and `Schemas`
36
+ - **Storages**: an interface to implement, an in-memory storage, and a test suite that checks a storage
37
+ - **Sync & Async**: a synchronous and an asynchronous handler, over the same rules
38
+ - **Multi-tenancy**: one set of resources per URL prefix
39
+
40
+ ## Installation
41
+
42
+ ```shell
43
+ pip install scim2-server
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ Serve a test server from the command line:
49
+
50
+ ```shell
51
+ scim2-server --port 8080
52
+ ```
53
+
54
+ Or build the WSGI application in Python:
55
+
56
+ ```python
57
+ from scim2_server.applications.wsgi import WSGIApplication
58
+ from scim2_server.memory import InMemoryStorage
59
+ from scim2_server.utils import load_default_provider
60
+
61
+ app = WSGIApplication(InMemoryStorage(), load_default_provider())
62
+ ```
63
+
64
+ A container image is published on the GitHub container registry for each release:
65
+
66
+ ```shell
67
+ docker run --publish 8080:8080 ghcr.io/python-scim/scim2-server --bearer-token secret
68
+ ```
69
+
70
+ The server has been tested against a live
71
+ [Microsoft Entra](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/scim-validator-tutorial)
72
+ system and a live
73
+ [Okta](https://developer.okta.com/docs/guides/scim-provisioning-integration-test/main/) system.
74
+
75
+ ## Documentation
76
+
77
+ - [Overview](https://scim2-server.readthedocs.io/en/latest/overview.html) introduces the parts of
78
+ a SCIM server, from the description of the service to the test server.
79
+ - [How-to guides](https://scim2-server.readthedocs.io/en/latest/how-to/index.html) cover focused
80
+ tasks, such as writing a storage or integrating a web framework.
81
+ - [Explanation](https://scim2-server.readthedocs.io/en/latest/explanation/index.html) covers the
82
+ layers of the server, the writes of resources and the bulk requests.
83
+ - [Reference](https://scim2-server.readthedocs.io/en/latest/reference.html) lists the public API
84
+ and the options of the `scim2-server` command.
85
+
86
+ ## Contributing
87
+
88
+ The [contribution page](https://scim2-server.readthedocs.io/en/latest/contributing.html)
89
+ describes how to run the tests, the style checks and the documentation build.
90
+
91
+ scim2-server belongs in a collection of SCIM tools developed by [Yaal Coop](https://yaal.coop),
92
+ with [scim2-models](https://github.com/python-scim/scim2-models),
93
+ [scim2-client](https://github.com/python-scim/scim2-client),
94
+ [scim2-tester](https://github.com/python-scim/scim2-tester) and
95
+ [scim2-cli](https://github.com/python-scim/scim2-cli).
96
+
97
+ ## Origins
98
+
99
+ Parts of this software were initially developed at [CONTACT Software](https://www.contact-software.com/) ([GitHub](https://github.com/cslab)) and subsequently made available under the Apache License Version 2.0.
@@ -4,8 +4,8 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-server"
7
- version = "0.4.0"
8
- description = "Lightweight SCIM2 server prototype"
7
+ version = "0.6.0"
8
+ description = "SCIM server core for any storage and web framework"
9
9
  readme = "README.md"
10
10
  keywords = [
11
11
  "scim",
@@ -28,10 +28,7 @@ classifiers = [
28
28
  "Operating System :: OS Independent",
29
29
  ]
30
30
  requires-python = ">= 3.11"
31
- dependencies = [
32
- "scim2-models>=0.10.0",
33
- "werkzeug>=3.0.3",
34
- ]
31
+ dependencies = ["scim2-models>=0.10.3"]
35
32
 
36
33
  [[project.authors]]
37
34
  name = "Yaal Coop"
@@ -44,12 +41,15 @@ email = "me@ccoors.de"
44
41
  [project.license]
45
42
  file = "LICENSE"
46
43
 
44
+ [project.optional-dependencies]
45
+ testing = ["pytest>=9.1.1"]
46
+
47
47
  [project.urls]
48
48
  repository = "https://github.com/python-scim/scim2-server"
49
49
  funding = "https://github.com/sponsors/python-scim"
50
50
 
51
51
  [project.scripts]
52
- scim2-server = "scim2_server.cli:main"
52
+ scim2-server = "scim2_server.testserver.cli:main"
53
53
 
54
54
  [dependency-groups]
55
55
  dev = [
@@ -59,7 +59,18 @@ dev = [
59
59
  "prek>=0.1.0",
60
60
  "time-machine>=2.14.2",
61
61
  "tox-uv>=1.16.0",
62
+ "aiosqlite>=0.22.1",
62
63
  "mypy>=2.3.1",
64
+ "scim2-client[werkzeug]>=0.11.2",
65
+ "scim2-tester>=0.5.1",
66
+ "werkzeug>=3.0.3",
67
+ ]
68
+ doc = [
69
+ "shibuya>=2024.5.15",
70
+ "sphinx>=7.3.7",
71
+ "sphinx-argparse>=0.6.1",
72
+ "sphinx-design>=0.6.1",
73
+ "sphinx-issues>=5.0.0",
63
74
  ]
64
75
 
65
76
  [tool.uv]
@@ -93,7 +104,7 @@ source = [
93
104
  omit = [
94
105
  ".tox/*",
95
106
  "examples/*",
96
- "scim2_server/cli.py",
107
+ "scim2_server/testserver/cli.py",
97
108
  ]
98
109
  branch = true
99
110
 
@@ -138,7 +149,8 @@ force-single-line = true
138
149
  docstring-code-format = true
139
150
 
140
151
  [tool.pytest.ini_options]
141
- addopts = "--doctest-modules --doctest-glob='*.rst'"
152
+ addopts = "--doctest-modules --doctest-glob='*.rst' --ignore=doc/conf.py"
153
+ pythonpath = ["doc/_examples"]
142
154
  doctest_optionflags = "ALLOW_UNICODE IGNORE_EXCEPTION_DETAIL ELLIPSIS"
143
155
 
144
156
  [tool.tox]
@@ -150,6 +162,7 @@ env_list = [
150
162
  "py313",
151
163
  "py314",
152
164
  "minversions",
165
+ "doc",
153
166
  "coverage",
154
167
  ]
155
168
 
@@ -176,6 +189,18 @@ commands = [[
176
189
  runner = "uv-venv-runner"
177
190
  uv_resolution = "lowest-direct"
178
191
 
192
+ [tool.tox.env.doc]
193
+ dependency_groups = ["doc"]
194
+ commands = [[
195
+ "sphinx-build",
196
+ "-W",
197
+ "--keep-going",
198
+ "--builder",
199
+ "html",
200
+ "doc",
201
+ "build/sphinx/html",
202
+ ]]
203
+
179
204
  [tool.tox.env.coverage]
180
205
  commands = [
181
206
  [
@@ -4,8 +4,8 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-server"
7
- version = "0.4.0"
8
- description = "Lightweight SCIM2 server prototype"
7
+ version = "0.6.0"
8
+ description = "SCIM server core for any storage and web framework"
9
9
  authors = [
10
10
  {name="Yaal Coop", email="contact@yaal.coop"},
11
11
  {name="Christian Friedrich Coors", email="me@ccoors.de"},
@@ -29,8 +29,12 @@ classifiers = [
29
29
 
30
30
  requires-python = ">= 3.11"
31
31
  dependencies = [
32
- "scim2-models>=0.10.0",
33
- "werkzeug>=3.0.3",
32
+ "scim2-models>=0.10.3",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ testing = [
37
+ "pytest>=9.1.1",
34
38
  ]
35
39
 
36
40
  [project.urls]
@@ -38,7 +42,7 @@ repository = "https://github.com/python-scim/scim2-server"
38
42
  funding = "https://github.com/sponsors/python-scim"
39
43
 
40
44
  [project.scripts]
41
- scim2-server = "scim2_server.cli:main"
45
+ scim2-server = "scim2_server.testserver.cli:main"
42
46
 
43
47
  [dependency-groups]
44
48
  dev = [
@@ -48,7 +52,18 @@ dev = [
48
52
  "prek>=0.1.0",
49
53
  "time-machine>=2.14.2",
50
54
  "tox-uv>=1.16.0",
55
+ "aiosqlite>=0.22.1",
51
56
  "mypy>=2.3.1",
57
+ "scim2-client[werkzeug]>=0.11.2",
58
+ "scim2-tester>=0.5.1",
59
+ "werkzeug>=3.0.3",
60
+ ]
61
+ doc = [
62
+ "shibuya>=2024.5.15",
63
+ "sphinx>=7.3.7",
64
+ "sphinx-argparse>=0.6.1",
65
+ "sphinx-design>=0.6.1",
66
+ "sphinx-issues>=5.0.0",
52
67
  ]
53
68
 
54
69
  [tool.uv]
@@ -74,7 +89,7 @@ source = [
74
89
  "scim2_server",
75
90
  "tests",
76
91
  ]
77
- omit = [".tox/*", "examples/*", "scim2_server/cli.py"]
92
+ omit = [".tox/*", "examples/*", "scim2_server/testserver/cli.py"]
78
93
  branch = true
79
94
 
80
95
  [tool.coverage.report]
@@ -119,7 +134,8 @@ docstring-code-format = true
119
134
 
120
135
 
121
136
  [tool.pytest.ini_options]
122
- addopts = "--doctest-modules --doctest-glob='*.rst'"
137
+ addopts = "--doctest-modules --doctest-glob='*.rst' --ignore=doc/conf.py"
138
+ pythonpath = ["doc/_examples"]
123
139
  doctest_optionflags= "ALLOW_UNICODE IGNORE_EXCEPTION_DETAIL ELLIPSIS"
124
140
 
125
141
  [tool.tox]
@@ -131,6 +147,7 @@ env_list = [
131
147
  "py313",
132
148
  "py314",
133
149
  "minversions",
150
+ "doc",
134
151
  "coverage",
135
152
  ]
136
153
 
@@ -151,6 +168,12 @@ commands = [
151
168
  runner = "uv-venv-runner"
152
169
  uv_resolution = "lowest-direct"
153
170
 
171
+ [tool.tox.env.doc]
172
+ dependency_groups = ["doc"]
173
+ commands = [
174
+ ["sphinx-build", "-W", "--keep-going", "--builder", "html", "doc", "build/sphinx/html"],
175
+ ]
176
+
154
177
  [tool.tox.env.coverage]
155
178
  commands = [
156
179
  ["pytest", "--cov", "--cov-fail-under=100", "--cov-report", "term:skip-covered", "{posargs}"],
@@ -0,0 +1 @@
1
+ """The WSGI and ASGI applications of the SCIM server."""
@@ -0,0 +1,147 @@
1
+ """The ASGI application of the SCIM server, with no dependency."""
2
+
3
+ import json
4
+ from collections.abc import Awaitable
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+ from urllib.parse import parse_qsl
8
+
9
+ from scim2_models import SCIMException
10
+ from scim2_models import ScimProvider
11
+
12
+ from scim2_server.applications.base import VERSION_PREFIX
13
+ from scim2_server.applications.base import BaseApplication
14
+ from scim2_server.handler import AsyncScimHandler
15
+ from scim2_server.requests import ScimRequest
16
+ from scim2_server.responses import ScimResponse
17
+ from scim2_server.service import ScimService
18
+ from scim2_server.storage import AsyncScimStorage
19
+
20
+ Scope = dict[str, Any]
21
+ Message = dict[str, Any]
22
+ Receive = Callable[[], Awaitable[Message]]
23
+ Send = Callable[[Message], Awaitable[None]]
24
+
25
+
26
+ class ASGIApplication(BaseApplication):
27
+ """An ASGI application serving the SCIM protocol over an asynchronous storage.
28
+
29
+ It reads the ASGI requests, serves them with an
30
+ :class:`~scim2_server.handler.AsyncScimHandler`, and writes the responses.
31
+ It answers the ``lifespan`` messages of the server.
32
+
33
+ :param storage: The storage of the resources.
34
+ :param provider: The description of the service.
35
+ :param service: The service serving the requests, built upon ``provider``.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ storage: AsyncScimStorage,
41
+ provider: ScimProvider,
42
+ service: ScimService | None = None,
43
+ ):
44
+ super().__init__(provider, service)
45
+ self.storage = storage
46
+ self.handler = AsyncScimHandler(self.service, storage)
47
+
48
+ @staticmethod
49
+ def get_base_url(scope: Scope) -> str:
50
+ """Return the root URL of the SCIM endpoints, as the client sees it."""
51
+ headers = dict(scope["headers"])
52
+ host = headers.get(b"host", b"").decode()
53
+ if not host and scope.get("server"):
54
+ host = "{}:{}".format(*scope["server"])
55
+ return f"{scope['scheme']}://{host}{scope.get('root_path', '')}{VERSION_PREFIX}"
56
+
57
+ @staticmethod
58
+ def get_path(scope: Scope) -> str:
59
+ """Return the path of a request, without the root path of the application.
60
+
61
+ The ``path`` of an ASGI scope includes its ``root_path``.
62
+ """
63
+ path: str = scope["path"]
64
+ root_path: str = scope.get("root_path", "")
65
+ return path.removeprefix(root_path) if root_path else path
66
+
67
+ async def read_request(self, scope: Scope, receive: Receive) -> ScimRequest:
68
+ """Return the SCIM request of an ASGI request.
69
+
70
+ The body is read up to one byte more than the service accepts, so that
71
+ the service answers 413 to a larger body.
72
+ """
73
+ request = ScimRequest(
74
+ method=scope["method"],
75
+ base_url=self.get_base_url(scope),
76
+ path=self.split_path(self.get_path(scope)),
77
+ query=dict(parse_qsl(scope["query_string"].decode())),
78
+ headers=[
79
+ (name.decode(), value.decode()) for name, value in scope["headers"]
80
+ ],
81
+ )
82
+ try:
83
+ limit = self.service.max_body_size(request)
84
+ except SCIMException:
85
+ limit = None
86
+ body = b""
87
+ more_body = True
88
+ while more_body:
89
+ message = await receive()
90
+ if limit is None or len(body) <= limit:
91
+ body += message.get("body", b"")
92
+ more_body = message.get("more_body", False)
93
+ request.body = body if limit is None else body[: limit + 1]
94
+ return request
95
+
96
+ async def dispatch_request(self, request: ScimRequest) -> ScimResponse:
97
+ """Authenticate a request and serve it.
98
+
99
+ Override this method to act before or after a request is served.
100
+ """
101
+ if self.needs_auth(request):
102
+ self.check_auth(request)
103
+ request.subject = self.get_subject(request)
104
+ return await self.handler.handle(request)
105
+
106
+ async def serve(self, request: ScimRequest) -> ScimResponse:
107
+ """Serve a request and return the response sent to the client."""
108
+ try:
109
+ response = await self.dispatch_request(request)
110
+ except Exception as exception:
111
+ response = self.handle_exception(request, exception)
112
+ return self.finalize_response(request, response)
113
+
114
+ @staticmethod
115
+ async def send_response(response: ScimResponse, send: Send) -> None:
116
+ """Send a SCIM response as an ASGI response."""
117
+ body = b"" if response.body is None else json.dumps(response.body).encode()
118
+ await send(
119
+ {
120
+ "type": "http.response.start",
121
+ "status": int(response.status),
122
+ "headers": [
123
+ (name.encode(), value.encode())
124
+ for name, value in response.headers.items()
125
+ ],
126
+ }
127
+ )
128
+ await send({"type": "http.response.body", "body": body})
129
+
130
+ @staticmethod
131
+ async def serve_lifespan(receive: Receive, send: Send) -> None:
132
+ """Answer the startup and the shutdown of the server."""
133
+ while True:
134
+ message = await receive()
135
+ if message["type"] == "lifespan.startup":
136
+ await send({"type": "lifespan.startup.complete"})
137
+ else: # lifespan.shutdown
138
+ await send({"type": "lifespan.shutdown.complete"})
139
+ return
140
+
141
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
142
+ """Serve an ASGI request."""
143
+ if scope["type"] == "lifespan":
144
+ await self.serve_lifespan(receive, send)
145
+ return
146
+ request = await self.read_request(scope, receive)
147
+ await self.send_response(await self.serve(request), send)