scim2-server 0.3.2__tar.gz → 0.4.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.2 → scim2_server-0.4.0}/PKG-INFO +26 -4
- {scim2_server-0.3.2 → scim2_server-0.4.0}/README.md +25 -3
- {scim2_server-0.3.2 → scim2_server-0.4.0}/pyproject.toml +1 -1
- {scim2_server-0.3.2 → scim2_server-0.4.0}/pyproject.toml.orig +1 -1
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/backend.py +3 -2
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/cli.py +41 -8
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/provider.py +29 -7
- scim2_server-0.4.0/scim2_server/tenants.py +87 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/LICENSE +0 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/__init__.py +0 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/bulk.py +0 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/py.typed +0 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/resources/default-resource-types.json +0 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/resources/default-schemas.json +0 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/resources/default-service-provider-config.json +0 -0
- {scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: scim2-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Lightweight SCIM2 server prototype
|
|
5
5
|
Keywords: scim,scim2,provisioning,rfc7643,rfc7644
|
|
6
6
|
Author: Yaal Coop, Christian Friedrich Coors
|
|
@@ -240,11 +240,12 @@ they are lost once the process exits.
|
|
|
240
240
|
- [x] HTTP PATCH (Add/Remove/Replace)
|
|
241
241
|
- [x] Sorting
|
|
242
242
|
- [x] Bulk operations
|
|
243
|
+
- [x] Multi-tenancy with a URL prefix
|
|
243
244
|
|
|
244
245
|
## Usage
|
|
245
246
|
|
|
246
247
|
```shell
|
|
247
|
-
$ 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] [--debug]
|
|
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]
|
|
248
249
|
```
|
|
249
250
|
|
|
250
251
|
- `-h`/`--help`: Show help message
|
|
@@ -255,9 +256,31 @@ $ scim2-server [-h] [--schema SCHEMA] [--resource-type RESOURCE_TYPE] [--service
|
|
|
255
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.
|
|
256
257
|
- `--hostname`: The hostname to listen on. Defaults to `127.0.0.1`.
|
|
257
258
|
- `--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
|
+
- `--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).
|
|
259
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.
|
|
260
263
|
|
|
264
|
+
### Multi-tenancy
|
|
265
|
+
|
|
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.
|
|
270
|
+
|
|
271
|
+
```shell
|
|
272
|
+
$ scim2-server --tenant a --tenant b
|
|
273
|
+
$ curl http://localhost:8080/a/v2/Users
|
|
274
|
+
```
|
|
275
|
+
|
|
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.
|
|
280
|
+
|
|
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
|
+
|
|
261
284
|
### Container
|
|
262
285
|
|
|
263
286
|
A container image is published on the GitHub container registry for each release.
|
|
@@ -277,7 +300,6 @@ This provider can be used as a starting point if you want to implement a SCIM pr
|
|
|
277
300
|
- Implement your own Backend as a subclass of `scim2_server.backend.Backend`
|
|
278
301
|
- Implement proper authorization with OAuth instead of public access or static bearer tokens
|
|
279
302
|
- 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
|
|
281
303
|
|
|
282
304
|
The provider in its current state has been tested successfully against a live
|
|
283
305
|
[Microsoft Entra](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/scim-validator-tutorial)
|
|
@@ -14,11 +14,12 @@ they are lost once the process exits.
|
|
|
14
14
|
- [x] HTTP PATCH (Add/Remove/Replace)
|
|
15
15
|
- [x] Sorting
|
|
16
16
|
- [x] Bulk operations
|
|
17
|
+
- [x] Multi-tenancy with a URL prefix
|
|
17
18
|
|
|
18
19
|
## Usage
|
|
19
20
|
|
|
20
21
|
```shell
|
|
21
|
-
$ 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] [--debug]
|
|
22
|
+
$ 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]
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
- `-h`/`--help`: Show help message
|
|
@@ -29,9 +30,31 @@ $ scim2-server [-h] [--schema SCHEMA] [--resource-type RESOURCE_TYPE] [--service
|
|
|
29
30
|
- `--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.
|
|
30
31
|
- `--hostname`: The hostname to listen on. Defaults to `127.0.0.1`.
|
|
31
32
|
- `--port`: The port to listen on. Defaults to `8080`.
|
|
32
|
-
- `--dump-resources`: Dump a JSON document containing all resources when the provider exits normally.
|
|
33
|
+
- `--dump-resources`: Dump a JSON document containing all resources when the provider exits normally. With tenants, the document has one entry per tenant.
|
|
34
|
+
- `--tenant`: Serve a tenant under `/<tenant>`, for example `/<tenant>/v2/Users`. Can be repeated. See [Multi-tenancy](#multi-tenancy).
|
|
35
|
+
- `--dynamic-tenants`: Create a tenant on the first request to `/<tenant>`. See [Multi-tenancy](#multi-tenancy).
|
|
33
36
|
- `--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.
|
|
34
37
|
|
|
38
|
+
### Multi-tenancy
|
|
39
|
+
|
|
40
|
+
With `--tenant` or `--dynamic-tenants`, the first segment of the URL path selects a tenant (RFC 7644 §6.1).
|
|
41
|
+
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.
|
|
42
|
+
The schemas, the resource types and the service provider configuration are the same for every tenant, and so are the bearer tokens.
|
|
43
|
+
A request without a known tenant gets a 404 answer, and `v2` cannot be a tenant name.
|
|
44
|
+
|
|
45
|
+
```shell
|
|
46
|
+
$ scim2-server --tenant a --tenant b
|
|
47
|
+
$ curl http://localhost:8080/a/v2/Users
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
With `--dynamic-tenants`, the first request to an unknown tenant creates it with no resources.
|
|
51
|
+
This is useful for tests: each test can pick a random tenant and get an empty server.
|
|
52
|
+
Any client can then create tenants, even without a valid bearer token, and every tenant stays in memory until the server exits.
|
|
53
|
+
Do not use this option on a server reachable by untrusted clients.
|
|
54
|
+
|
|
55
|
+
In Python, `scim2_server.tenants.TenantDispatcher` builds one `SCIMApplication` per tenant from a factory.
|
|
56
|
+
Override its `select_tenant` method to read the tenant from a header or a sub-domain instead.
|
|
57
|
+
|
|
35
58
|
### Container
|
|
36
59
|
|
|
37
60
|
A container image is published on the GitHub container registry for each release.
|
|
@@ -51,7 +74,6 @@ This provider can be used as a starting point if you want to implement a SCIM pr
|
|
|
51
74
|
- Implement your own Backend as a subclass of `scim2_server.backend.Backend`
|
|
52
75
|
- Implement proper authorization with OAuth instead of public access or static bearer tokens
|
|
53
76
|
- Support the `/Me` endpoint, if it applies in your use case
|
|
54
|
-
- Add support for using either a static URL prefix or improve the support for usage behind a reverse proxy
|
|
55
77
|
|
|
56
78
|
The provider in its current state has been tested successfully against a live
|
|
57
79
|
[Microsoft Entra](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/scim-validator-tutorial)
|
|
@@ -98,7 +98,8 @@ class Backend:
|
|
|
98
98
|
:param resource: Resource to create.
|
|
99
99
|
:return: The created resource. Creation should set system-
|
|
100
100
|
defined attributes (ID, Metadata). May be the same object
|
|
101
|
-
that is passed in.
|
|
101
|
+
that is passed in. A relative ``meta.location`` is relative to
|
|
102
|
+
the root of the application.
|
|
102
103
|
"""
|
|
103
104
|
raise NotImplementedError
|
|
104
105
|
|
|
@@ -230,7 +231,7 @@ class InMemoryBackend(Backend):
|
|
|
230
231
|
resource_type=resource_type.name,
|
|
231
232
|
created=utcnow,
|
|
232
233
|
last_modified=utcnow,
|
|
233
|
-
location="/
|
|
234
|
+
location=f"v2/{resource_type.endpoint.strip('/')}/{resource.id}",
|
|
234
235
|
)
|
|
235
236
|
self._touch_resource(resource, utcnow)
|
|
236
237
|
self._check_uniqueness(resource)
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import pprint
|
|
5
5
|
from collections.abc import Iterable
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
from scim2_models import AuthenticationScheme
|
|
9
10
|
from scim2_models import External
|
|
@@ -16,6 +17,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
|
|
16
17
|
|
|
17
18
|
from scim2_server.backend import InMemoryBackend
|
|
18
19
|
from scim2_server.provider import SCIMApplication
|
|
20
|
+
from scim2_server.tenants import TenantDispatcher
|
|
19
21
|
from scim2_server.utils import load_default_resource_types
|
|
20
22
|
from scim2_server.utils import load_default_schemas
|
|
21
23
|
from scim2_server.utils import load_default_service_provider_config
|
|
@@ -45,6 +47,11 @@ def log_environ(handler: "WSGIApplication") -> "WSGIApplication":
|
|
|
45
47
|
return _inner
|
|
46
48
|
|
|
47
49
|
|
|
50
|
+
def dump_resources(backend: InMemoryBackend) -> list[dict[str, Any]]:
|
|
51
|
+
"""Return the JSON representation of the resources of a backend."""
|
|
52
|
+
return [r.model_dump() for r in backend.resources]
|
|
53
|
+
|
|
54
|
+
|
|
48
55
|
def main() -> None:
|
|
49
56
|
parser = argparse.ArgumentParser()
|
|
50
57
|
parser.add_argument(
|
|
@@ -71,6 +78,17 @@ def main() -> None:
|
|
|
71
78
|
type=argparse.FileType("w"),
|
|
72
79
|
help="Dump resources to a JSON file on exit",
|
|
73
80
|
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--tenant",
|
|
83
|
+
action="append",
|
|
84
|
+
help="Serve a tenant under /TENANT, with its own resources",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--dynamic-tenants",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Create a tenant on the first request to /TENANT. "
|
|
90
|
+
"Any client can then create tenants, and they stay in memory until exit",
|
|
91
|
+
)
|
|
74
92
|
parser.add_argument(
|
|
75
93
|
"--debug",
|
|
76
94
|
action="store_true",
|
|
@@ -112,16 +130,26 @@ def main() -> None:
|
|
|
112
130
|
BEARER_TOKEN_SCHEME,
|
|
113
131
|
]
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
app = SCIMApplication(
|
|
117
|
-
backend, ScimProvider.from_discovery(schemas, resource_types, config=config)
|
|
118
|
-
)
|
|
133
|
+
provider = ScimProvider.from_discovery(schemas, resource_types, config=config)
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
backends: dict[str | None, InMemoryBackend] = {}
|
|
136
|
+
|
|
137
|
+
def make_application(tenant: str | None = None) -> SCIMApplication:
|
|
138
|
+
backends[tenant] = InMemoryBackend()
|
|
139
|
+
app = SCIMApplication(backends[tenant], provider)
|
|
140
|
+
for bearer_token in args.bearer_token or []:
|
|
122
141
|
app.register_bearer_token(bearer_token)
|
|
142
|
+
return app
|
|
143
|
+
|
|
144
|
+
use_tenants = bool(args.tenant or args.dynamic_tenants)
|
|
145
|
+
wsgi_app: WSGIApplication
|
|
146
|
+
if use_tenants:
|
|
147
|
+
wsgi_app = TenantDispatcher(
|
|
148
|
+
make_application, args.tenant or [], dynamic=args.dynamic_tenants
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
wsgi_app = make_application()
|
|
123
152
|
|
|
124
|
-
wsgi_app: WSGIApplication = app
|
|
125
153
|
if args.debug:
|
|
126
154
|
wsgi_app = log_environ(wsgi_app)
|
|
127
155
|
if args.reverse_proxy:
|
|
@@ -139,8 +167,13 @@ def main() -> None:
|
|
|
139
167
|
)
|
|
140
168
|
|
|
141
169
|
if args.dump_resources:
|
|
170
|
+
dump: Any = (
|
|
171
|
+
{tenant: dump_resources(backend) for tenant, backend in backends.items()}
|
|
172
|
+
if use_tenants
|
|
173
|
+
else dump_resources(backends[None])
|
|
174
|
+
)
|
|
142
175
|
with args.dump_resources as f:
|
|
143
|
-
f.write(json.dumps(
|
|
176
|
+
f.write(json.dumps(dump, indent=2))
|
|
144
177
|
|
|
145
178
|
|
|
146
179
|
if __name__ == "__main__":
|
|
@@ -36,6 +36,7 @@ from werkzeug import Response
|
|
|
36
36
|
from werkzeug.datastructures import ETags
|
|
37
37
|
from werkzeug.exceptions import Forbidden
|
|
38
38
|
from werkzeug.exceptions import HTTPException
|
|
39
|
+
from werkzeug.exceptions import MethodNotAllowed
|
|
39
40
|
from werkzeug.exceptions import NotFound
|
|
40
41
|
from werkzeug.exceptions import NotImplemented as WerkzeugNotImplemented
|
|
41
42
|
from werkzeug.exceptions import PreconditionFailed
|
|
@@ -43,6 +44,7 @@ from werkzeug.exceptions import RequestEntityTooLarge
|
|
|
43
44
|
from werkzeug.exceptions import Unauthorized
|
|
44
45
|
from werkzeug.http import parse_etags
|
|
45
46
|
from werkzeug.http import unquote_etag
|
|
47
|
+
from werkzeug.routing import BaseConverter
|
|
46
48
|
from werkzeug.routing import Map
|
|
47
49
|
from werkzeug.routing import Rule
|
|
48
50
|
from werkzeug.routing.exceptions import RequestRedirect
|
|
@@ -79,6 +81,20 @@ BULK_SUCCESS_STATUS = {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
|
|
84
|
+
class ResourceEndpointConverter(BaseConverter):
|
|
85
|
+
"""Match a resource endpoint, but not the endpoints RFC 7644 reserves nor the version prefix.
|
|
86
|
+
|
|
87
|
+
A request with a method a reserved endpoint does not support then gets a
|
|
88
|
+
405 answer, instead of being routed to a resource type of that name.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# A reserved name followed by the end of the path segment is refused.
|
|
92
|
+
regex = (
|
|
93
|
+
r"(?!(?:ServiceProviderConfig|ResourceTypes|Schemas|Bulk|Me|v2)(?![^/]))[^/]+"
|
|
94
|
+
)
|
|
95
|
+
part_isolating = True
|
|
96
|
+
|
|
97
|
+
|
|
82
98
|
class SCIMApplication:
|
|
83
99
|
"""A WSGI application implementing a SCIM provider (server)."""
|
|
84
100
|
|
|
@@ -123,17 +139,17 @@ class SCIMApplication:
|
|
|
123
139
|
methods=("GET", "POST", "PUT", "PATCH", "DELETE"),
|
|
124
140
|
),
|
|
125
141
|
Rule(
|
|
126
|
-
f"{prefix}/<
|
|
142
|
+
f"{prefix}/<resource_endpoint:resource_endpoint>",
|
|
127
143
|
endpoint="resource",
|
|
128
144
|
methods=("GET", "POST"),
|
|
129
145
|
),
|
|
130
146
|
Rule(
|
|
131
|
-
f"{prefix}/<
|
|
147
|
+
f"{prefix}/<resource_endpoint:resource_endpoint>/.search",
|
|
132
148
|
endpoint="resource_search",
|
|
133
149
|
methods=("POST",),
|
|
134
150
|
),
|
|
135
151
|
Rule(
|
|
136
|
-
f"{prefix}/<
|
|
152
|
+
f"{prefix}/<resource_endpoint:resource_endpoint>/<string:resource_id>",
|
|
137
153
|
endpoint="single_resource",
|
|
138
154
|
methods=("GET", "PUT", "PATCH", "DELETE"),
|
|
139
155
|
),
|
|
@@ -148,7 +164,9 @@ class SCIMApplication:
|
|
|
148
164
|
for prefix in ("", "/v2")
|
|
149
165
|
)
|
|
150
166
|
|
|
151
|
-
self.url_map = Map(
|
|
167
|
+
self.url_map = Map(
|
|
168
|
+
rules, converters={"resource_endpoint": ResourceEndpointConverter}
|
|
169
|
+
)
|
|
152
170
|
|
|
153
171
|
def get_model(self, resource_type: ResourceType) -> type[Resource[Any]]:
|
|
154
172
|
"""Return the model of a resource type, its extensions included."""
|
|
@@ -178,12 +196,12 @@ class SCIMApplication:
|
|
|
178
196
|
def publish(self, request: Request, resource: Resource[Any]) -> Resource[Any]:
|
|
179
197
|
"""Return a copy of a resource in the form sent to the client.
|
|
180
198
|
|
|
181
|
-
Its location is made absolute from the URL the
|
|
199
|
+
Its location is made absolute from the root URL of the application, and
|
|
182
200
|
its version is left out when the service does not support ETags.
|
|
183
201
|
"""
|
|
184
202
|
assert resource.meta is not None
|
|
185
203
|
update: dict[str, Any] = {
|
|
186
|
-
"location": urljoin(request.
|
|
204
|
+
"location": urljoin(request.url_root, resource.meta.location)
|
|
187
205
|
}
|
|
188
206
|
if not self.etag_supported:
|
|
189
207
|
update["version"] = None
|
|
@@ -811,7 +829,11 @@ class SCIMApplication:
|
|
|
811
829
|
self.log.exception(e)
|
|
812
830
|
return e.get_response(environ)
|
|
813
831
|
except Exception as e:
|
|
814
|
-
|
|
832
|
+
response = self.make_error(self.error_from(e))
|
|
833
|
+
if isinstance(e, MethodNotAllowed) and e.valid_methods:
|
|
834
|
+
# RFC 9110 §15.5.6: a 405 answer lists the supported methods.
|
|
835
|
+
response.headers["Allow"] = ", ".join(sorted(e.valid_methods))
|
|
836
|
+
return response
|
|
815
837
|
|
|
816
838
|
def __call__(
|
|
817
839
|
self, environ: "WSGIEnvironment", start_response: "StartResponse"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from threading import Lock
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from scim2_models import Error
|
|
7
|
+
|
|
8
|
+
from scim2_server.provider import SCIMApplication
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from _typeshed.wsgi import StartResponse
|
|
12
|
+
from _typeshed.wsgi import WSGIEnvironment
|
|
13
|
+
|
|
14
|
+
RESERVED_TENANTS = frozenset({"v2"})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TenantDispatcher:
|
|
18
|
+
"""A WSGI application serving each tenant with its own SCIM application.
|
|
19
|
+
|
|
20
|
+
The tenant is the first segment of the request path, as in the URL prefix
|
|
21
|
+
method of RFC 7644 §6.1: a request to ``/<tenant>/v2/Users`` is served by
|
|
22
|
+
the application of ``<tenant>``, mounted under ``/<tenant>``.
|
|
23
|
+
|
|
24
|
+
:param factory: Build the application of a tenant from its name.
|
|
25
|
+
:param tenants: The tenants created at startup.
|
|
26
|
+
:param dynamic: Whether a request to an unknown tenant creates it. Any
|
|
27
|
+
client can then create tenants, and each one stays in memory.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
factory: Callable[[str], SCIMApplication],
|
|
33
|
+
tenants: Iterable[str] = (),
|
|
34
|
+
dynamic: bool = False,
|
|
35
|
+
):
|
|
36
|
+
self.factory = factory
|
|
37
|
+
self.dynamic = dynamic
|
|
38
|
+
self.applications: dict[str, SCIMApplication] = {}
|
|
39
|
+
self.lock = Lock()
|
|
40
|
+
for tenant in tenants:
|
|
41
|
+
if not self.is_valid_tenant(tenant):
|
|
42
|
+
raise ValueError(f"Invalid tenant name: {tenant!r}")
|
|
43
|
+
self.applications[tenant] = factory(tenant)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def is_valid_tenant(tenant: str) -> bool:
|
|
47
|
+
"""Tell whether a name can identify a tenant.
|
|
48
|
+
|
|
49
|
+
The version segment is refused, so that a request without a tenant is
|
|
50
|
+
not served by a tenant named ``v2``.
|
|
51
|
+
"""
|
|
52
|
+
return bool(tenant) and "/" not in tenant and tenant not in RESERVED_TENANTS
|
|
53
|
+
|
|
54
|
+
def select_tenant(self, environ: "WSGIEnvironment") -> str | None:
|
|
55
|
+
"""Return the tenant of a request, and move it from the path to the mount prefix.
|
|
56
|
+
|
|
57
|
+
Override this method to read the tenant from somewhere else, such as
|
|
58
|
+
a header or a sub-domain (RFC 7644 §6.1).
|
|
59
|
+
"""
|
|
60
|
+
path_info: str = environ.get("PATH_INFO", "")
|
|
61
|
+
_, _, path = path_info.partition("/")
|
|
62
|
+
tenant, separator, rest = path.partition("/")
|
|
63
|
+
if not self.is_valid_tenant(tenant):
|
|
64
|
+
return None
|
|
65
|
+
environ["SCRIPT_NAME"] = f"{environ.get('SCRIPT_NAME', '')}/{tenant}"
|
|
66
|
+
environ["PATH_INFO"] = separator + rest
|
|
67
|
+
return tenant
|
|
68
|
+
|
|
69
|
+
def get_application(self, tenant: str) -> SCIMApplication | None:
|
|
70
|
+
"""Return the application of a tenant, creating it if tenants are dynamic."""
|
|
71
|
+
with self.lock:
|
|
72
|
+
if tenant not in self.applications and self.dynamic:
|
|
73
|
+
self.applications[tenant] = self.factory(tenant)
|
|
74
|
+
return self.applications.get(tenant)
|
|
75
|
+
|
|
76
|
+
def __call__(
|
|
77
|
+
self, environ: "WSGIEnvironment", start_response: "StartResponse"
|
|
78
|
+
) -> Iterable[bytes]:
|
|
79
|
+
"""Dispatch a request to the application of its tenant."""
|
|
80
|
+
tenant = self.select_tenant(environ)
|
|
81
|
+
application = self.get_application(tenant) if tenant is not None else None
|
|
82
|
+
if application is None:
|
|
83
|
+
response = SCIMApplication.make_response(
|
|
84
|
+
Error(status=404, detail="Unknown tenant").model_dump(), status=404
|
|
85
|
+
)
|
|
86
|
+
return response(environ, start_response)
|
|
87
|
+
return application(environ, start_response)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scim2_server-0.3.2 → scim2_server-0.4.0}/scim2_server/resources/default-resource-types.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|