scim2-server 0.3.3__tar.gz → 0.5.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.
- {scim2_server-0.3.3 → scim2_server-0.5.0}/PKG-INFO +79 -42
- scim2_server-0.5.0/README.md +99 -0
- {scim2_server-0.3.3 → scim2_server-0.5.0}/pyproject.toml +34 -9
- {scim2_server-0.3.3 → scim2_server-0.5.0}/pyproject.toml.orig +30 -7
- scim2_server-0.5.0/scim2_server/applications/__init__.py +1 -0
- scim2_server-0.5.0/scim2_server/applications/asgi.py +147 -0
- scim2_server-0.5.0/scim2_server/applications/base.py +95 -0
- scim2_server-0.5.0/scim2_server/applications/wsgi.py +234 -0
- scim2_server-0.5.0/scim2_server/bulk.py +232 -0
- scim2_server-0.5.0/scim2_server/conditions.py +94 -0
- scim2_server-0.5.0/scim2_server/errors.py +33 -0
- scim2_server-0.5.0/scim2_server/handler.py +652 -0
- scim2_server-0.5.0/scim2_server/memory.py +269 -0
- scim2_server-0.5.0/scim2_server/requests.py +58 -0
- scim2_server-0.5.0/scim2_server/responses.py +23 -0
- scim2_server-0.5.0/scim2_server/routing.py +128 -0
- scim2_server-0.5.0/scim2_server/service.py +913 -0
- scim2_server-0.5.0/scim2_server/storage.py +172 -0
- scim2_server-0.5.0/scim2_server/testing.py +556 -0
- scim2_server-0.5.0/scim2_server/testserver/__init__.py +1 -0
- scim2_server-0.5.0/scim2_server/testserver/application.py +48 -0
- scim2_server-0.5.0/scim2_server/testserver/cli.py +219 -0
- {scim2_server-0.3.3 → scim2_server-0.5.0}/scim2_server/utils.py +2 -2
- scim2_server-0.3.3/README.md +0 -63
- scim2_server-0.3.3/scim2_server/backend.py +0 -301
- scim2_server-0.3.3/scim2_server/bulk.py +0 -184
- scim2_server-0.3.3/scim2_server/cli.py +0 -147
- scim2_server-0.3.3/scim2_server/provider.py +0 -856
- {scim2_server-0.3.3 → scim2_server-0.5.0}/LICENSE +0 -0
- {scim2_server-0.3.3 → scim2_server-0.5.0}/scim2_server/__init__.py +0 -0
- {scim2_server-0.3.3 → scim2_server-0.5.0}/scim2_server/py.typed +0 -0
- {scim2_server-0.3.3 → scim2_server-0.5.0}/scim2_server/resources/default-resource-types.json +0 -0
- {scim2_server-0.3.3 → scim2_server-0.5.0}/scim2_server/resources/default-schemas.json +0 -0
- {scim2_server-0.3.3 → scim2_server-0.5.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
|
-
Summary:
|
|
3
|
+
Version: 0.5.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,73 +217,110 @@ 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.
|
|
221
|
-
Requires-Dist:
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
-
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
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
|
|
243
266
|
|
|
244
|
-
##
|
|
267
|
+
## Installation
|
|
245
268
|
|
|
246
269
|
```shell
|
|
247
|
-
|
|
270
|
+
pip install scim2-server
|
|
248
271
|
```
|
|
249
272
|
|
|
250
|
-
|
|
251
|
-
- `--reverse-proxy`: Allow using the provider behind a Reverse Proxy (required for URL rewriting).
|
|
252
|
-
- `--schema`: Register schemas from specified JSON file. If not provided, loads the default schemas from RFC 7643.
|
|
253
|
-
- `--resource-type`: Register resource types from specified JSON file. If not provided, loads the default resource types from RFC 7643.
|
|
254
|
-
- `--service-provider-config`: Load the service provider configuration from specified JSON file. If not provided, loads the default configuration.
|
|
255
|
-
- `--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.
|
|
256
|
-
- `--hostname`: The hostname to listen on. Defaults to `127.0.0.1`.
|
|
257
|
-
- `--port`: The port to listen on. Defaults to `8080`.
|
|
258
|
-
- `--dump-resources`: Dump a JSON document containing all resources when the provider exits normally.
|
|
259
|
-
- `--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.
|
|
260
|
-
|
|
261
|
-
### Container
|
|
273
|
+
## Usage
|
|
262
274
|
|
|
263
|
-
|
|
264
|
-
The server listens on `0.0.0.0:8080` inside the container, and the command line arguments are passed to `scim2-server`:
|
|
275
|
+
Serve a test server from the command line:
|
|
265
276
|
|
|
266
277
|
```shell
|
|
267
|
-
|
|
278
|
+
scim2-server --port 8080
|
|
268
279
|
```
|
|
269
280
|
|
|
270
|
-
|
|
281
|
+
Or build the WSGI application in Python:
|
|
271
282
|
|
|
272
|
-
|
|
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
|
|
273
287
|
|
|
274
|
-
|
|
288
|
+
app = WSGIApplication(InMemoryStorage(), load_default_provider())
|
|
289
|
+
```
|
|
275
290
|
|
|
276
|
-
|
|
277
|
-
- Implement your own Backend as a subclass of `scim2_server.backend.Backend`
|
|
278
|
-
- Implement proper authorization with OAuth instead of public access or static bearer tokens
|
|
279
|
-
- Support the `/Me` endpoint, if it applies in your use case
|
|
280
|
-
- Add support for using either a static URL prefix or improve the support for usage behind a reverse proxy
|
|
291
|
+
A container image is published on the GitHub container registry for each release:
|
|
281
292
|
|
|
282
|
-
|
|
293
|
+
```shell
|
|
294
|
+
docker run --publish 8080:8080 ghcr.io/python-scim/scim2-server --bearer-token secret
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The server has been tested against a live
|
|
283
298
|
[Microsoft Entra](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/scim-validator-tutorial)
|
|
284
|
-
system
|
|
299
|
+
system and a live
|
|
285
300
|
[Okta](https://developer.okta.com/docs/guides/scim-provisioning-integration-test/main/) system.
|
|
286
301
|
|
|
302
|
+
## Documentation
|
|
303
|
+
|
|
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.
|
|
312
|
+
|
|
313
|
+
## Contributing
|
|
314
|
+
|
|
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).
|
|
323
|
+
|
|
287
324
|
## Origins
|
|
288
325
|
|
|
289
326
|
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.
|
|
@@ -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.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.5.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.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.5.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.
|
|
33
|
-
|
|
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)
|