Source code for scim2_server.conditions

import re
from dataclasses import dataclass

from scim2_models import PreconditionFailedException

# A member of an entity tag list: an optional weak prefix, then a quoted or a
# bare tag, up to the next comma. Quoted tags may hold commas, so the header
# cannot be split on commas first.
ENTITY_TAG = re.compile(r'\s*(?:[Ww]/)?(?:"([^"]*)"|([^,]*?))\s*(?:,|$)')


def opaque_tag(entity_tag: str) -> str:
    """Return the opaque part of an entity tag, without its weak prefix and its quotes."""
    entity_tag = entity_tag.strip()
    if entity_tag[:2] in ("W/", "w/"):
        entity_tag = entity_tag[2:]
    if len(entity_tag) >= 2 and entity_tag[0] == entity_tag[-1] == '"':
        entity_tag = entity_tag[1:-1]
    return entity_tag


[docs] @dataclass(frozen=True) class EntityTags: """The entity tags of an :mdn:`If-Match` or an :mdn:`If-None-Match` header.""" tags: frozenset[str] = frozenset() """The opaque part of each listed tag.""" any: bool = False """Whether the header is "*", which matches any current representation."""
[docs] @classmethod def parse(cls, header: str | None) -> "EntityTags | None": """Read the value of a conditional header. :return: :data:`None` when the header is missing or empty. """ if not header or not header.strip(): return None tags = set() for match in ENTITY_TAG.finditer(header): quoted, bare = match.groups() if quoted is None and bare == "*": return cls(any=True) if quoted is not None or bare: tags.add(quoted if quoted is not None else bare) return cls(tags=frozenset(tags))
[docs] def matches(self, version: str | None) -> bool: """Tell whether a resource version matches the header, with the weak comparison. Both :mdn:`If-Match` and :mdn:`If-None-Match` use the weak comparison (:rfc:`RFC 9110 §8.8.3.2 <9110#section-8.8.3.2>`). :param version: The version of the resource, or :data:`None` when the service does not version its resources. """ if self.any: return True return version is not None and opaque_tag(version) in self.tags
[docs] @dataclass(frozen=True) class Conditions: """The raw values of the conditional headers of a request.""" if_match: str | None = None if_none_match: str | None = None
[docs] def check(self, version: str | None, method: str) -> bool: """Evaluate the conditions against the version of a resource. Per :rfc:`RFC 9110 §13.2.2 <9110#section-13.2.2>`, :mdn:`If-Match` is evaluated first. A failed ``If-Match`` answers 412 whatever the method. A failed :mdn:`If-None-Match` answers 304 to a GET and 412 otherwise. :param version: The version of the resource, or :data:`None` when the service does not version its resources. Such a resource matches no listed tag, and "*" still matches it (:rfc:`RFC 9110 §13.1.1 <9110#section-13.1.1>`). :return: :data:`False` when a GET should answer 304 Not Modified. :raises ~scim2_models.PreconditionFailedException: When the method must not be performed. """ if_match = EntityTags.parse(self.if_match) if if_match is not None and not if_match.matches(version): raise PreconditionFailedException if_none_match = EntityTags.parse(self.if_none_match) if if_none_match is not None and if_none_match.matches(version): if method == "GET": return False raise PreconditionFailedException return True