Tutorial

Initialization

scim2-client depends on request engines such as httpx to perform network requests. This tutorial demonstrate how to use scim2-client with httpx, and suppose you have installed the httpx extra for example with pip install scim2-client[httpx].

As a start you will need to instantiate a httpx Client (or AsyncClient) object that you can parameter as your will, and then pass it to a SCIMClient object. In addition to your SCIM server root endpoint, you will probably want to provide some authorization headers through the httpx Client headers parameter:

from httpx import Client
from scim2_client.engines.httpx import SyncSCIMClient

client = Client(
    base_url="https://auth.example/scim/v2",
    headers={"Authorization": "Bearer foobar"},
)
scim = SyncSCIMClient(client)
from httpx import AsyncClient
from scim2_client.engines.httpx import AsyncSCIMClient

client = AsyncClient(
    base_url="https://auth.example/scim/v2",
    headers={"Authorization": "Bearer foobar"},
)
scim = AsyncSCIMClient(client)

You need to give to indicate to SCIMClient all the different Resource models that you will need to manipulate, and the matching ResourceType objects to let the client know where to look for resources on the server.

You can either provision those objects manually or automatically.

Automatic provisioning

The easiest way is to let the client discover the server’s configuration and available resources. The discover() method looks for the server ServiceProviderConfig, Schema and ResourceType endpoints, and dynamically generate local Python models based on those schemas. They are then available to use with get_resource_model().

Dynamically discover models from the server
scim.discover()
User = scim.get_resource_model("User")
EnterpriseUser = User.get_extension_model("EnterpriseUser")
Dynamically discover models from the server
await scim.discover()
User = scim.get_resource_model("User")
EnterpriseUser = User.get_extension_model("EnterpriseUser")

Manual provisioning

To manually register models and resource types, you can simply use the resource_models and resource_types arguments.

Manually registering models and resource types
from scim2_models import User, EnterpriseUserUser, Group, ResourceType
scim = SyncSCIMClient(
    client,
    resource_models=[User[EnterpriseUser], Group],
    resource_types=[ResourceType(id="User", ...), ResourceType(id="Group", ...)],
)
Manually registering models and resource types
from scim2_models import User, EnterpriseUserUser, Group, ResourceType
scim = AsyncSCIMClient(
    client,
    resource_models=[User[EnterpriseUser], Group],
    resource_types=[ResourceType(id="User", ...), ResourceType(id="Group", ...)],
)

Tip

If you know that all the resources are hosted at regular server endpoints (for instance /Users for User etc.), you can skip passing the ResourceType objects by hand, and simply call register_naive_resource_types().

Manually registering models and resource types
from scim2_models import User, EnterpriseUserUser, Group, ResourceType
scim = SyncSCIMClient(
    client,
    resource_models=[User[EnterpriseUser], Group],
)
scim.register_naive_resource_types()
Manually registering models and resource types
from scim2_models import User, EnterpriseUserUser, Group, ResourceType
scim = AsyncSCIMClient(
    client,
    resource_models=[User[EnterpriseUser], Group],
)
scim.register_naive_resource_types()

Performing actions

scim2-client allows your application to interact with a SCIM server as described in RFC7644 §3, so you can read and manage the resources. Have a look at the Reference to see the exhaustive set of parameters.

Create

create() issues a POST to provision a new resource:

request = User(user_name="[email protected]")
response = scim.create(request)
print(f"User {response.id} has been created!")
request = User(user_name="[email protected]")
response = await scim.create(request)
print(f"User {response.id} has been created!")

Query

query() issues a GET to read a single resource by its id, or list resources of a given type:

from scim2_models import SearchRequest

user = scim.query(User, "my-user-id")

response = scim.query(User, query_parameters=SearchRequest(filter='userName sw "john"'))
for user in response.resources:
    print(user.user_name)
from scim2_models import SearchRequest

user = await scim.query(User, "my-user-id")

response = await scim.query(User, query_parameters=SearchRequest(filter='userName sw "john"'))
for user in response.resources:
    print(user.user_name)

Replace

replace() issues a PUT to fully overwrite an existing resource:

user = scim.query(User, "my-user-id")
user.display_name = "Fancy New Name"
updated_user = scim.replace(user)
user = await scim.query(User, "my-user-id")
user.display_name = "Fancy New Name"
updated_user = await scim.replace(user)

Delete

delete() issues a DELETE and returns None on success:

scim.delete(User, "my-user-id")
await scim.delete(User, "my-user-id")

Modify

modify() issues a PATCH to apply partial updates as defined in RFC7644 §3.5.2:

from scim2_models import PatchOp, PatchOperation

patch = PatchOp[User](operations=[
    PatchOperation(op=PatchOperation.Op.replace_, path="displayName", value="New Name"),
    PatchOperation(op=PatchOperation.Op.add, path="emails", value=[{"value": "[email protected]"}]),
])
response = scim.modify(User, "my-user-id", patch)
from scim2_models import PatchOp, PatchOperation

patch = PatchOp[User](operations=[
    PatchOperation(op=PatchOperation.Op.replace_, path="displayName", value="New Name"),
    PatchOperation(op=PatchOperation.Op.add, path="emails", value=[{"value": "[email protected]"}]),
])
response = await scim.modify(User, "my-user-id", patch)

Bulk

Note

Bulk operation requests are not yet implemented, but any help is welcome!

Error handling

By default, if the server returns an error, a SCIMResponseErrorObject exception is raised. The to_error() method gives access to the Error object:

from scim2_client import SCIMResponseErrorObject

try:
    response = scim.create(request)
except SCIMResponseErrorObject as exc:
    error = exc.to_error()
    print(f"SCIM error [{error.status}] {error.scim_type}: {error.detail}")
from scim2_client import SCIMResponseErrorObject

try:
    response = await scim.create(request)
except SCIMResponseErrorObject as exc:
    error = exc.to_error()
    print(f"SCIM error [{error.status}] {error.scim_type}: {error.detail}")

Request and response validation

By default, scim2-client validates both request payloads and server responses against the SCIM specifications, raising an error on non-compliance. However sometimes you want to accept invalid inputs and outputs. To achieve this, all the methods provide the following parameters, all are True by default:

  • check_request_payload: If True (the default) a ValidationError will be raised if the input does not respect the SCIM standard. If False, input is expected to be a dict that will be passed as-is in the request.

  • check_response_payload: If True (the default) a ValidationError will be raised if the server response does not respect the SCIM standard. If False the server response is returned as-is.

  • expected_status_codes: The list of expected status codes in the response. If None any status code is accepted. If an unexpected status code is returned, a UnexpectedStatusCode exception is raised.

  • raise_scim_errors: If True (the default) and the server returned an Error object, a SCIMResponseErrorObject exception will be raised. The to_error() method gives access to the Error object. If False the error object is returned directly.

Tip

Check the request Contexts to understand which value will excluded from the request payload, and which values are expected in the response payload.

Engines

scim2-client comes with a light abstraction layers that allows for different requests engines. Currently those engines are shipped:

  • SyncSCIMClient: A synchronous engine using httpx to perform the HTTP requests.

  • AsyncSCIMClient: An asynchronous engine using httpx to perform the HTTP requests. It has the very same API than its synchronous version, except it is asynchronous.

  • TestSCIMClient: A test engine for development purposes. It takes a WSGI app and directly execute the server code instead of performing real HTTP requests. This is faster in unit test suites, and helpful to catch the server exceptions.

You can easily implement your own engine by inheriting from SCIMClient.

Additional request parameters

Pass additional parameters directly to the underlying engine methods. This can be useful if you need to explicitly pass a certain URL for example:

scim.query(url="/User/i-know-what-im-doing")
await scim.query(url="/User/i-know-what-im-doing")