Deploy the server#
Use this guide to serve the WSGI or the ASGI application of scim2-server with a production server, such as Gunicorn or Uvicorn. It assumes a storage, such as the one of Write a storage.
Build the application#
Build the application in a module of the project, such as myapp/wsgi.py or myapp/asgi.py.
The server imports it from there:
import sqlite3
from scim2_server.applications.wsgi import WSGIApplication
from scim2_server.utils import load_default_provider
from myapp.scim import SQLiteStorage
provider = load_default_provider()
storage = SQLiteStorage(sqlite3.connect("scim.sqlite"), provider)
app = WSGIApplication(storage, provider)
from scim2_server.applications.asgi import ASGIApplication
from scim2_server.utils import load_default_provider
from myapp.scim import AsyncSQLiteStorage
provider = load_default_provider()
app = ASGIApplication(AsyncSQLiteStorage("scim.sqlite", provider), provider)
Serve the application#
Install the server, and start it with the module and the name of the application:
$ pip install gunicorn
$ gunicorn --bind 0.0.0.0:8000 --workers 4 myapp.wsgi:app
$ pip install uvicorn
$ uvicorn --host 0.0.0.0 --port 8000 --workers 4 myapp.asgi:app
The SCIM endpoints are served under /v2, such as http://<HOST>:8000/v2/Users.
Each worker is a process, with its own copy of the application. Use a storage over a shared
database: the workers then serve the same resources. With
InMemoryStorage, each worker keeps its own resources, so run a
single worker.
Serve the application behind a reverse proxy#
Behind a reverse proxy, the server receives the requests from the proxy, not from the client. The
locations of the resources must still use the URL that the client used: its scheme, its host and
its path prefix. The proxy sends them in the X-Forwarded-* headers.
With a WSGI server, wrap the application in ForwardedHeaders. It reads
X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Prefix and
X-Forwarded-For. With Uvicorn, the server reads X-Forwarded-Proto and X-Forwarded-For
itself. The proxy passes the Host header of the client, and --root-path gives the path
prefix:
from scim2_server.applications.wsgi import ForwardedHeaders
app = ForwardedHeaders(WSGIApplication(storage, provider))
$ uvicorn --forwarded-allow-ips 10.0.0.1 --root-path /scim myapp.asgi:app
Only the proxy may send these headers: a client could otherwise choose the URLs of the resources.
Uvicorn only reads them from the addresses of --forwarded-allow-ips.
ForwardedHeaders reads them from any client, so only the proxy must
reach the server.
Publish the resources at other URLs#
Some deployments cannot give the URL of the client in the forwarded headers: the proxy cannot be configured, or an API gateway publishes the resources under another path or another domain. Set the URL of the resources in the service then.
The service builds every URL of a resource in one method:
resource_location(). The meta.location attribute,
the Location header of a creation, and the location of the bulk results all come from
it. Subclass ScimService, and override the method. It receives the
root URL of the SCIM endpoints, the resource type, and the identifier of the resource:
>>> from scim2_server.service import ScimService
>>> class PublicService(ScimService):
... def resource_location(self, base_url, resource_type, resource_id):
... return f"https://scim.example{resource_type.endpoint}/{resource_id}"
Pass the service to the application, built upon the same provider:
>>> from scim2_server.applications.wsgi import WSGIApplication
>>> from scim2_server.memory import InMemoryStorage
>>> from scim2_server.utils import load_default_provider
>>> provider = load_default_provider()
>>> app = WSGIApplication(
... InMemoryStorage(), provider, service=PublicService(provider)
... )
>>> from scim2_server.applications.asgi import ASGIApplication
>>> from scim2_server.memory import AsyncInMemoryStorage
>>> async_app = ASGIApplication(
... AsyncInMemoryStorage(), provider, service=PublicService(provider)
... )
A created resource then carries the URL of the service:
>>> from werkzeug.test import Client
>>> response = Client(app).post(
... "/v2/Users", json={"userName": "bjensen"}, content_type="application/scim+json"
... )
>>> response.headers["Location"] == f"https://scim.example/Users/{response.json['id']}"
True
>>> response.json["meta"]["location"] == response.headers["Location"]
True
The routes of the server do not change: they stay those of RFC 7644 ยง3.2.