Grant write contract
Every writable Grant kind has an add and a remove. This page is the contract both must meet, in every kind of connector. The short version: report "already there" or "already gone" only when the write call's own response proves it; a lookup that finds nothing fails the write; every other error propagates. Owlie absorbs no error on your behalf, because a "not found" from a user lookup, a renamed role or a mistyped URL looks exactly like an absent Grant, and a remove recorded as done while the access remains leaves someone with access that Owlie reports as removed.
The rule
| Situation | What the handler does |
|---|---|
| The add's own response says the Grant exists (a documented code or message) | Return ctx.grant.alreadyPresent() |
| The remove's own response says the Grant does not exist (a documented code or message, or a 404 from the remove call that on that endpoint can mean nothing else) | Return ctx.grant.alreadyAbsent() |
| A lookup that resolves the principal or target (by email, by name) finds nothing | Fail: throw, or let the lookup's error propagate |
| Anything else | Let the error propagate |
For a Grant kind with qualifiers, "a Grant exists on this pair" is not "this Grant exists". Return
alreadyPresent() only after confirming that the existing assignment carries the requested qualifier
values; otherwise change it. Returning the other action's marker (alreadyAbsent() from an add) is
an error. Otherwise a handler returns nothing, or a record Owlie keeps with the result.
Reading the Grant itself
Two API shapes need a read before the write. The read counts as the vendor's answer once both the principal and the target are confirmed to exist:
- An exact read of the assignment, such as
GET /groups/{group}/members/{member}, that answers 404 while the group is known to exist: returnalreadyAbsent(). - An API where the remove needs an assignment id that only a list gives. A complete list of the
principal's assignments, every page read, with none matching the target and qualifiers: return
alreadyAbsent(). A partial or failed list fails the remove.
An add can check first in the same way: if an exact read shows the Grant already exists with the
requested qualifiers, return alreadyPresent() without writing.
Qualifier checks Owlie runs first
Before your handler runs, Owlie checks the Grant's qualifiers against the Grant kind's declaration.
| Add | Remove | |
|---|---|---|
| Required qualifiers present | checked | checked |
| No undeclared qualifiers | checked | not checked |
| Each value of its declared type | checked | checked |
Enum values among the declared values | checked | not checked |
Strings matching the declared pattern (values over 1,024 characters refused) | checked | not checked |
A remove is checked less strictly because the Grant may have been observed by Sync before the pattern
or enum existed, and access that exists must stay removable. Your handler must still encode every
qualifier it puts in a URL path. A set qualifier is a list of strings; a multi_valued qualifier is
a list of its schema's values.
What engines treat as proof
LDAP, SCIM and SQL connectors do not write Grant handlers; the engine performs the write and reads the system's own answer:
| Kind | Looks up first | Already present | Already absent |
|---|---|---|---|
| LDAP | The member and the group; a miss fails the write | The directory answers "attribute value exists" (result 20), or Active Directory's 68 | The directory answers "no such attribute value" (result 16), or Active Directory's 53 after reading the group back |
| SCIM | The group, and the user unless members are addressed by id | The definition's errors.conflictStatus on the add, when errors.idempotency treats it as success | The definition's errors.notFoundStatus on the remove, after confirming the group exists, when errors.idempotency treats it as success |
| SQL | Nothing | The add statement changed no row | The remove statement changed no row |
A SQL connector's only evidence is the row count, so its remove statement must address the Grant row
itself (DELETE FROM user_roles WHERE user_id = :source AND role_code = :target). A remove for a
principal that does not exist changes no row and reports "already absent". If you replace an engine's
Grant handler with your own code, your code takes on the rule above.
Tests every writable Grant kind needs
Run each case against a test account in the vendor's system:
- Grant: the add succeeds.
- Revoke: the remove succeeds on a Grant that exists. This is the case that catches a mistyped remove URL, which would otherwise answer 404 and pass every remove as "already gone".
- Duplicate grant: gives
already_present: true. - Absent revoke: gives
already_absent: true. - Forbidden revoke: a 403 fails with
AUTH_ERROR. Expect one retry before the failure, after a token refresh when the authentication option can refresh. - Lookup miss: a remove whose user or target lookup finds nothing fails.
- Read-first shapes: an absent Grant found by an exact read or a complete list gives
already_absent: true. - Qualifier refusals: one per declared
pattern, enum andentity_ref.
Run them from the Connector Builder's Test tab, or through the API with connectors.runTest for an
HTTP connector or connectors.testArtifact for the other kinds, and save each case's input so it can be
run again.