Overview#
scim2-server serves the System for Cross-domain Identity Management (SCIM) protocol. It validates the requests, applies them to the resources, and builds the responses, with scim2-models.
It does not keep the resources, authenticate the clients, or depend on a web framework: a storage, the application and an integration do.
The SCIM data model and SCIM protocol specifications define the vocabulary used here.
Install scim2-server:
pip install scim2-server
This page introduces the parts of a SCIM server, in the order an application meets them. Follow it in order for a first tour. The how-to guides cover focused tasks, the explanations cover the design of the server, and the reference lists the complete API.
Describe the service#
A ScimProvider describes the service: the schemas it knows, the resources
it serves, and the features it supports. load_default_provider()
serves the users and the groups of RFC 7643, with every feature:
>>> from scim2_server.utils import load_default_provider
>>> provider = load_default_provider()
Build a provider of its own to serve other attributes. The following service serves the members of a library. A schema extension adds a card number to the users (RFC 7643 ยง3.3). The service refuses the bulk requests, and returns at most 100 users per search:
>>> from scim2_models import URN
>>> from scim2_models import Extension
>>> from scim2_models import ResourceType
>>> from scim2_models import ScimProvider
>>> from scim2_models import User
>>> from scim2_server.utils import load_default_service_provider_config
>>> class LibraryUser(Extension):
... __schema__ = URN("urn:example:params:scim:schemas:extension:library:2.0:User")
... card_number: str | None = None
>>> config = load_default_service_provider_config()
>>> config.bulk.supported = False
>>> config.filter.max_results = 100
>>> provider = ScimProvider(
... models=[User, LibraryUser],
... resource_types=[ResourceType.from_resource(User[LibraryUser])],
... config=config,
... )
The Python name card_number becomes the SCIM attribute cardNumber.
Serve a request#
A ScimHandler serves the SCIM requests with a
ScimService and a storage.
InMemoryStorage keeps the resources in memory. A
ScimRequest holds a request, independent of any web framework:
>>> import json
>>> from scim2_server.handler import ScimHandler
>>> from scim2_server.memory import InMemoryStorage
>>> from scim2_server.requests import ScimRequest
>>> from scim2_server.service import ScimService
>>> handler = ScimHandler(ScimService(provider), InMemoryStorage())
>>> member = {
... "schemas": [
... "urn:ietf:params:scim:schemas:core:2.0:User",
... "urn:example:params:scim:schemas:extension:library:2.0:User",
... ],
... "userName": "bjensen",
... "urn:example:params:scim:schemas:extension:library:2.0:User": {
... "cardNumber": "42-1337"
... },
... }
>>> response = handler.handle(
... ScimRequest(
... "POST",
... "https://scim.example/v2",
... "/Users",
... headers={"Content-Type": "application/scim+json"},
... body=json.dumps(member).encode(),
... )
... )
>>> response.status, response.headers["Location"]
(<HTTPStatus.CREATED: 201>, 'https://scim.example/v2/Users/...')
The server validated the user against the schemas of the provider, and filled its id and its
meta attribute. Read it back:
>>> user_id = response.body["id"]
>>> response = handler.handle(
... ScimRequest("GET", "https://scim.example/v2", f"/Users/{user_id}")
... )
>>> response.body["urn:example:params:scim:schemas:extension:library:2.0:User"]
{'cardNumber': '42-1337'}
A failed request raises a SCIMException.
error_response() turns it into the error response. A bulk
request fails, as the configuration announces:
>>> from scim2_models import SCIMException
>>> try:
... handler.handle(
... ScimRequest(
... "POST",
... "https://scim.example/v2",
... "/Bulk",
... headers={"Content-Type": "application/scim+json"},
... body=b'{"schemas": ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"]}',
... )
... )
... except SCIMException as exception:
... response = handler.service.error_response(exception)
>>> response.status, response.body["detail"]
(<HTTPStatus.NOT_IMPLEMENTED: 501>, 'Bulk is not supported')
Serve over HTTP#
WSGIApplication and
ASGIApplication serve a storage and a provider over
HTTP, with no other dependency. The ASGI application takes an asynchronous storage, such as
AsyncInMemoryStorage:
>>> from scim2_server.applications.wsgi import WSGIApplication
>>> app = WSGIApplication(InMemoryStorage(), provider)
>>> from scim2_server.applications.asgi import ASGIApplication
>>> from scim2_server.memory import AsyncInMemoryStorage
>>> async_app = ASGIApplication(AsyncInMemoryStorage(), provider)
Serve the WSGI application on the local machine with wsgiref.simple_server:
>>> from wsgiref.simple_server import make_server
>>> make_server("127.0.0.1", 8080, app).serve_forever()
In another terminal, read the configuration of the service:
$ curl -s http://127.0.0.1:8080/v2/ServiceProviderConfig | python -m json.tool
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
],
"meta": {
"resourceType": "ServiceProviderConfig",
"location": "http://127.0.0.1:8080/v2/ServiceProviderConfig"
},
"patch": {
"supported": true
},
"bulk": {
"supported": false,
"maxOperations": 1000,
"maxPayloadSize": 1048576
},
"filter": {
"supported": true,
"maxResults": 100
},
"changePassword": {
"supported": true
},
"sort": {
"supported": true
},
"etag": {
"supported": true
},
"authenticationSchemes": []
}
$ pip install scim2-cli
$ scim2 --url http://127.0.0.1:8080/v2 query serviceproviderconfig
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
],
"meta": {
"resourceType": "ServiceProviderConfig",
"location": "http://127.0.0.1:8080/v2/ServiceProviderConfig"
},
"patch": {
"supported": true
},
"bulk": {
"supported": false,
"maxOperations": 1000,
"maxPayloadSize": 1048576
},
"filter": {
"supported": true,
"maxResults": 100
},
"changePassword": {
"supported": true
},
"sort": {
"supported": true
},
"etag": {
"supported": true
},
"authenticationSchemes": []
}
Integrate a web framework serves SCIM from the web framework of an application, and Deploy the server serves the applications in production.
Keep the resources in a database#
A storage reads and writes the resources. Subclass ScimStorage, or
AsyncScimStorage for an asynchronous server, and write its five
methods. The server validates the requests and applies them before it calls the storage:
from scim2_server.storage import ScimStorage
class DatabaseStorage(ScimStorage):
def get(self, resource_type, resource_id): ...
def create(self, resource_type, resource): ...
def update(self, resource_type, resource, *, expected_version=None): ...
def delete(self, resource_type, resource_id, *, expected_version=None): ...
def search(self, resource_types, search_request): ...
Write a storage writes a complete storage over a SQLite table, and Serve an existing data model serves the tables that an application already has.
Check a server#
scim2_tester.check_server() sends the requests an identity provider would send, and checks
each response against the RFCs. Install it with the Werkzeug engine of scim2-client, which calls
the application directly:
$ pip install scim2-tester "scim2-client[werkzeug]"
>>> from scim2_client.engines.werkzeug import TestSCIMClient
>>> from scim2_tester import check_server
>>> from werkzeug.test import Client
>>> scim_client = TestSCIMClient(Client(app), scim_prefix="/v2")
>>> scim_client.discover()
>>> results = check_server(scim_client)
>>> [result.title for result in results if result.status.name in ("ERROR", "CRITICAL")]
[]
A storage has its own test suite. Subclass
ScimStorageContract in the tests of the storage, and give it a
storage fixture:
import pytest
from scim2_server.testing import ScimStorageContract
class TestDatabaseStorage(ScimStorageContract):
@pytest.fixture
def storage(self, provider):
return DatabaseStorage(provider)
Check a storage runs this suite.
Run a test server#
The scim2-server command serves an in-memory SCIM server, for the tests of a SCIM client in
any language:
$ scim2-server --port 8080
Each release also publishes an image on the GitHub container registry:
$ docker run --publish 8080:8080 ghcr.io/python-scim/scim2-server
pytest-scim2-server serves the same server as a pytest fixture. Test server lists the options of the command.