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().
scim.discover()
User = scim.get_resource_model("User")
EnterpriseUser = User.get_extension_model("EnterpriseUser")
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.
from scim2_models import User, EnterpriseUserUser, Group, ResourceType
scim = SyncSCIMClient(
client,
resource_models=[User[EnterpriseUser], Group],
resource_types=[ResourceType(id="User", ...), ResourceType(id="Group", ...)],
)
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)
Search¶
search() issues a POST on the /.search endpoint to query across all resource types at once:
response = scim.search(SearchRequest(filter='id co "admin"'))
response = await scim.search(SearchRequest(filter='id co "admin"'))
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: IfTrue(the default) aValidationErrorwill be raised if the input does not respect the SCIM standard. IfFalse, input is expected to be adictthat will be passed as-is in the request.check_response_payload: IfTrue(the default) aValidationErrorwill be raised if the server response does not respect the SCIM standard. IfFalsethe server response is returned as-is.expected_status_codes: The list of expected status codes in the response. IfNoneany status code is accepted. If an unexpected status code is returned, aUnexpectedStatusCodeexception is raised.raise_scim_errors: IfTrue(the default) and the server returned anErrorobject, aSCIMResponseErrorObjectexception will be raised. Theto_error()method gives access to theErrorobject. IfFalsethe 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")