SCIM clients can filter list responses (GET /Users?filter=…) and search endpoints (POST /Users/.search) using the SCIM 2.0 filter grammar defined in RFC 7644 §3.4.2.2. AshScim parses these filters into the map-shaped input format consumed by Ash.Query.filter_input/2, with the security-critical attribute resolution done in the same pass.

Operator support

SCIMAshExample
eqequserName eq "alice"
nenot_eqactive ne true
cocontainsuserName co "alice"
swstring_starts_with?userName sw "alice"
ewstring_ends_with?userName ew "@example.com"
pris_nil: falseexternalId pr
gtgreater_thancreated gt "2024-01-01"
gegreater_than_or_equalcreated ge "2024-01-01"
ltless_thancreated lt "2024-12-31"
leless_than_or_equalcreated le "2024-12-31"

Boolean composition

and, or, and parenthesized not (...) are supported with the standard precedence (not > and > or):

userName eq "alice" or userName eq "bob"
active eq true and externalId pr
not (active eq true)
(userName co "alice" or userName co "bob") and active eq true

Chained ands and ors flatten into a single list:

  • a or b or c parses to %{or: [a, b, c]}
  • a and b and c parses to %{and: [a, b, c]}

Dotted paths

Sub-attributes of complex and multivalued mappings are addressable via dotted paths:

name.familyName co "Anderson"
emails.value eq "alice@example.com"

For complex and single-attribute multivalueds, the dotted path resolves through the sub-maps to the underlying scalar attribute and emits a flat filter (%{last_name: %{contains: "Anderson"}}).

For relationship-backed multivalueds, the dotted path emits Ash's native nested filter form:

# members.value eq "u1" against a Group
%{memberships: %{user_id: %{eq: "u1"}}}

This traverses the :memberships relationship, so a query like ?filter=members.value eq "u1" finds groups whose memberships include the matching user.

Security: closed-world resolution

The filter resolver is the security boundary between IdP-supplied input and your data layer. The rule it enforces:

Every attribute path in the filter must be declared in the resource's SCIM mappings. Anything else fails closed.

If an IdP sends ?filter=password eq "hunter2", the resolver sees that password is not a declared SCIM attribute on this resource and returns 400 invalidFilter — even if the resource has a password Ash attribute. The filter cannot reach the data layer.

This means filters can only target what the DSL exposes. If you have attribute :department, :string on the resource but don't write map :department, attribute: :department, IdP filters can't reference it. For SCIM that's correct — you don't expose what you didn't declare.

Sorting

SCIM ?sortBy= parameters go through the same resolution pipeline. A sort by an unmapped attribute fails with 400 invalidValue.

GET /Users?sortBy=userName&sortOrder=descending

sortOrder accepts ascending (default) or descending.