Skip to main content

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​

SituationWhat 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 nothingFail: throw, or let the lookup's error propagate
Anything elseLet 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: return alreadyAbsent().
  • 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.

AddRemove
Required qualifiers presentcheckedchecked
No undeclared qualifierscheckednot checked
Each value of its declared typecheckedchecked
Enum values among the declared valuescheckednot checked
Strings matching the declared pattern (values over 1,024 characters refused)checkednot 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:

KindLooks up firstAlready presentAlready absent
LDAPThe member and the group; a miss fails the writeThe directory answers "attribute value exists" (result 20), or Active Directory's 68The directory answers "no such attribute value" (result 16), or Active Directory's 53 after reading the group back
SCIMThe group, and the user unless members are addressed by idThe definition's errors.conflictStatus on the add, when errors.idempotency treats it as successThe definition's errors.notFoundStatus on the remove, after confirming the group exists, when errors.idempotency treats it as success
SQLNothingThe add statement changed no rowThe 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:

  1. Grant: the add succeeds.
  2. 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".
  3. Duplicate grant: gives already_present: true.
  4. Absent revoke: gives already_absent: true.
  5. Forbidden revoke: a 403 fails with AUTH_ERROR. Expect one retry before the failure, after a token refresh when the authentication option can refresh.
  6. Lookup miss: a remove whose user or target lookup finds nothing fails.
  7. Read-first shapes: an absent Grant found by an exact read or a complete list gives already_absent: true.
  8. Qualifier refusals: one per declared pattern, enum and entity_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.