Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ Added
- :meth:`Resource.replace <scim2_models.Resource.replace>` returns whether the replacement
changes the resource, the order of multi-valued entries aside. A server can keep
``meta.version`` and ``meta.lastModified`` when a PUT changes nothing.
- In the :attr:`~scim2_models.Context.BULK_REQUEST` context, an invalid operation of a
:class:`~scim2_models.BulkRequest` no longer fails the whole request. It becomes its own failed
result, with a ``status`` and an :class:`~scim2_models.Error`, as
:rfc:`RFC7644 §3.7.3 <7644#section-3.7.3>` requires. Under a
:class:`~scim2_models.ScimProvider`, each operation is read as the resource type its ``path``
targets, and an unknown endpoint fails the operation with ``invalidPath``.
- :attr:`BulkOperation.endpoint <scim2_models.BulkOperation.endpoint>` and
:attr:`BulkOperation.resource_id <scim2_models.BulkOperation.resource_id>` read the target of
an operation from its ``path``.

Changed
^^^^^^^
Expand Down Expand Up @@ -42,6 +51,11 @@ Security
- Under :attr:`RemoveValue.apply <scim2_models.ScimPolicy.RemoveValue.apply>`, a PATCH
``remove`` whose ``value`` has a key that is not an attribute name, such as
``"value pr or value"``, is refused with ``invalidValue``.
- In a :class:`~scim2_models.BulkRequest`, the ``data`` of a ``PATCH`` operation must be a
:class:`~scim2_models.PatchOp`, and the ``data`` of a ``POST`` or a ``PUT`` must be a resource.
A full resource sent as the ``data`` of a ``PATCH`` used to be accepted with its read-only
attributes, such as ``id`` and ``groups``. An invalid ``data`` only reports the errors of the
type the method expects.

[0.9.0] - 2026-09-27
--------------------
Expand Down
2 changes: 1 addition & 1 deletion doc/integrations/_examples/django_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ class BulkView(SCIMView):
def post(self, request):
try:
bulk_request = BulkRequest[User].model_validate_json(
request.body, scim_ctx=Context.BULK_REQUEST
request.body, scim_ctx=Context.BULK_REQUEST, scim_provider=provider
)
except ValidationError as error:
return scim_validation_error(error)
Expand Down
10 changes: 10 additions & 0 deletions doc/integrations/_examples/fastapi_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ def __init__(self, content: Any = None, **kwargs: Any) -> None:
router = APIRouter(prefix="/scim/v2", default_response_class=SCIMResponse)



# -- provider-middleware-start --
@app.middleware("http")
async def scim_provider(request: Request, call_next):
"""Validate the payloads under the provider, which knows the resource type of each endpoint."""
with provider:
return await call_next(request)
# -- provider-middleware-end --


def resource_location(request, app_record):
"""Return the canonical URL for a user record."""
return str(request.url_for("get_user", user_id=app_record["id"]))
Expand Down
1 change: 1 addition & 0 deletions doc/integrations/_examples/flask_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ def bulk():
bulk_request = BulkRequest[User].model_validate_json(
request.data,
scim_ctx=Context.BULK_REQUEST,
scim_provider=provider,
)
bulk_response = execute_bulk(bulk_request, resource_location)
return bulk_response.model_dump(scim_ctx=Context.BULK_RESPONSE)
Expand Down
37 changes: 19 additions & 18 deletions doc/integrations/_examples/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,6 @@ class PayloadTooLargeException(SCIMException):
"""The status each method answers with when it succeeds."""


def record_id_of(path):
"""Return the resource identifier a bulk operation path designates.

:param path: The ``path`` of the operation, relative to the SCIM root.
"""
return path.rsplit("/", 1)[-1]


def apply_operation(operation, record):
"""Apply one bulk operation to the store and return the record it acted on.

Expand Down Expand Up @@ -290,16 +282,24 @@ def run_operation(operation, location_for):
result = BulkOperation[User](method=operation.method, bulk_id=operation.bulk_id)

record = None
if operation.method != BulkOperation.Method.post:
try:
record = get_record(record_id_of(operation.path))
except KeyError:
result.status = HTTPStatus.NOT_FOUND
result.response = Error(
status=HTTPStatus.NOT_FOUND, detail="Resource does not exist."
)
return result
try:
record = get_record(operation.resource_id)
result.location = location_for(record)
except KeyError:
pass

if isinstance(operation.response, Error):
# scim2-models could not validate the operation, and tells why.
result.status = operation.status
result.response = operation.response
return result

if operation.method != BulkOperation.Method.post and record is None:
result.status = HTTPStatus.NOT_FOUND
result.response = Error(
status=HTTPStatus.NOT_FOUND, detail="Resource does not exist."
)
return result

try:
acted_record = apply_operation(operation, record)
Expand All @@ -318,7 +318,8 @@ def run_operation(operation, location_for):
def execute_bulk(bulk_request, location_for):
"""Apply every operation of a bulk job and describe each outcome.

:param bulk_request: The validated bulk request.
:param bulk_request: The bulk request, validated under the provider so that
each operation is read by the resource type of its path.
:param location_for: Builds the canonical URL of a record, which only the
HTTP layer of a framework knows how to spell.
"""
Expand Down
5 changes: 3 additions & 2 deletions doc/integrations/django.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,9 @@ so that the resource converter does not read ``.search`` as an identifier.
POST /Bulk
^^^^^^^^^^

Validate the job with :attr:`~scim2_models.Context.BULK_REQUEST`, apply it with ``execute_bulk``
and serialize the outcome with :attr:`~scim2_models.Context.BULK_RESPONSE`. The view closes over
Validate the job with :attr:`~scim2_models.Context.BULK_REQUEST` under the provider, apply it
with ``execute_bulk`` and serialize the outcome with :attr:`~scim2_models.Context.BULK_RESPONSE`.
The view closes over
its request to build each location, and reports a job beyond ``maxOperations`` through the same
``scim_exception_error`` helper as the other views. See :ref:`helpers-bulk` for what the executor
does with each operation.
Expand Down
8 changes: 8 additions & 0 deletions doc/integrations/fastapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ The job is validated through :class:`~scim2_models.BulkRequestContext`, which ap
with :attr:`~scim2_models.Context.BULK_RESPONSE`. The route closes over its request to build each
location.

FastAPI validates the job before it calls the route, so the provider is opened by a middleware.
Each operation is then read as the resource type its ``path`` targets.

.. literalinclude:: _examples/fastapi_example.py
:language: python
:start-after: # -- provider-middleware-start --
:end-before: # -- provider-middleware-end --

A job that exceeds ``maxOperations`` raises :class:`~scim2_models.SCIMException`, which the
handler registered for it turns into a ``413``. See :ref:`helpers-bulk` for what
the executor does with each operation.
Expand Down
5 changes: 3 additions & 2 deletions doc/integrations/flask.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,9 @@ convert to native and persist, then serialize the created resource with
POST /Bulk
^^^^^^^^^^

Validate the job with :attr:`~scim2_models.Context.BULK_REQUEST`, apply it with ``execute_bulk``
and serialize the outcome with :attr:`~scim2_models.Context.BULK_RESPONSE`. The view hands the
Validate the job with :attr:`~scim2_models.Context.BULK_REQUEST` under the provider, apply it
with ``execute_bulk`` and serialize the outcome with :attr:`~scim2_models.Context.BULK_RESPONSE`.
The view hands the
executor its own ``resource_location``, the only part of a result a framework has to spell.

A job that exceeds ``maxOperations`` raises :class:`~scim2_models.SCIMException`, which the error
Expand Down
17 changes: 11 additions & 6 deletions doc/integrations/helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,18 @@ POST or a PUT therefore hands ``apply_operation`` a :class:`~scim2_models.User`,
:class:`~scim2_models.PatchOp`, both already validated. The dispatch reuses the storage and
mapping helpers of the resource endpoints, and validates nothing again.

An operation that cannot be validated does not fail the request. It arrives as its own failed
result, with a ``status`` and an :class:`~scim2_models.Error` as ``response``, and the other
operations are validated as usual. Under a :class:`~scim2_models.ScimProvider`, each operation is
read as the resource type its ``path`` targets, and an unknown endpoint fails with
``invalidPath``. Without a provider, the model is picked from the ``data``. A request covering
several resource types should therefore be validated under the provider.

``execute_bulk`` follows these rules of §3.7:

- A job performs as many changes as possible and disregards partial failures. ``failOnErrors``
caps the failures a client accepts, and the operations past that cap stay undone.
caps the failures a client accepts, invalid operations included, and the operations past that
cap stay undone.
- Every result carries the location of the resource its operation acted on, except a creation
that failed. ``run_operation`` resolves the target before it applies the operation, so a failure
still knows that location.
Expand All @@ -163,8 +171,5 @@ mapping helpers of the resource endpoints, and validates nothing again.
:start-after: # -- bulk-start --
:end-before: # -- bulk-end --

Some parts of §3.7 stay out of these helpers. Resolving a ``bulkId:`` reference, which lets one
operation point at a resource another operation of the same job creates, is left to the
application. And a payload that no model accepts fails the whole request with a ``400``, where
§3.7.3 reports such an operation with its own ``400`` inside a job that answers ``200``:
scim2-models validates the request in one pass.
Resolving a ``bulkId:`` reference, which lets one operation point at a resource another operation
of the same job creates, stays out of these helpers and is left to the application.
Loading