scim2-server 0.4.0__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.4.0 → scim2_server-0.5.0}/PKG-INFO +74 -59
- scim2_server-0.5.0/README.md +99 -0
- {scim2_server-0.4.0 → scim2_server-0.5.0}/pyproject.toml +34 -9
- {scim2_server-0.4.0 → 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.4.0 → scim2_server-0.5.0}/scim2_server/utils.py +2 -2
- scim2_server-0.4.0/README.md +0 -85
- scim2_server-0.4.0/scim2_server/backend.py +0 -302
- scim2_server-0.4.0/scim2_server/bulk.py +0 -184
- scim2_server-0.4.0/scim2_server/cli.py +0 -180
- scim2_server-0.4.0/scim2_server/provider.py +0 -856
- scim2_server-0.4.0/scim2_server/tenants.py +0 -87
- {scim2_server-0.4.0 → scim2_server-0.5.0}/LICENSE +0 -0
- {scim2_server-0.4.0 → scim2_server-0.5.0}/scim2_server/__init__.py +0 -0
- {scim2_server-0.4.0 → scim2_server-0.5.0}/scim2_server/py.typed +0 -0
- {scim2_server-0.4.0 → scim2_server-0.5.0}/scim2_server/resources/default-resource-types.json +0 -0
- {scim2_server-0.4.0 → scim2_server-0.5.0}/scim2_server/resources/default-schemas.json +0 -0
- {scim2_server-0.4.0 → 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,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.
|
|
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
|
-
-
|
|
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
|
-
##
|
|
267
|
+
## Installation
|
|
246
268
|
|
|
247
269
|
```shell
|
|
248
|
-
|
|
270
|
+
pip install scim2-server
|
|
249
271
|
```
|
|
250
272
|
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
$ curl http://localhost:8080/a/v2/Users
|
|
278
|
+
scim2-server --port 8080
|
|
274
279
|
```
|
|
275
280
|
|
|
276
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
+
docker run --publish 8080:8080 ghcr.io/python-scim/scim2-server --bearer-token secret
|
|
291
295
|
```
|
|
292
296
|
|
|
293
|
-
|
|
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
|
-
##
|
|
302
|
+
## Documentation
|
|
296
303
|
|
|
297
|
-
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
[
|
|
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.
|
|
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)
|