Data API
Writing your product's resources and relations, searching who can reach what, reading plan features, and keeping reads consistent with your writes, all through the decision client.
On this page
Status: In development, behind two flags that default OFF
The write calls (resources.upsert, relations.write) need authz_write_api. The search calls (listAccessible, subjectsFor, actionsFor) and hasFeature need authz_search_api. Both flags default OFF. While a flag is off its endpoints answer 404 as if they were not mounted, and the matching call throws NotAvailableError. The invalidation event stream is not behind either flag.
Identity's tokens carry no roles. When your product has its own objects (documents, projects, boards), the decision API can only decide about them if identity knows they exist and who is related to them. The data API is how your product tells it, and asks the reverse questions. It is part of the decision client: the methods below are on the object createIdentity returns, and they use the same authenticated, DPoP-bound transport. Read that page first for the options, the headers credential and the caller rules.
What the caller needs#
The caller is your product's service client, as for a decision. Beyond that:
- Your product must have an active installation in the organization named in the call (the
tenantfield, an organization id), or the answer is403. See Installations and the entry rule. - Every reference you send is
<ns>:<type>:<id>andnsmust be your product's namespace. A reference in another namespace is answered like one that does not exist, so a product cannot probe another product's data. - An organization's installation clamps what a relation can grant: a tuple can only imply your product's own permissions, and every decision is cut to what the organization consented to.
Write resources and relations#
const { resources, rv } = await identity.resources.upsert({ tenant, resources: [{ type: 'document', external_id: 'q3-plan', parent: 'eidos:folder:finance' }],})await identity.relations.write({ tenant, writes: [{ resource: 'eidos:document:q3-plan', relation: 'viewer', subject: { principal_sub: sub } }], deletes: [],})resources.upsert creates or updates resources and is idempotent. Each item has type (lower case letters, digits and _, 2 to 32 characters, starting with a letter), external_id (1 to 128 characters of letters, digits and . _ ~ -), and optionally parent (a resource reference) and owner_sub. It returns the references and the organization's rv. The reference is built on the server from type and external_id, so it is always in your namespace.
relations.write writes and deletes tuples. A tuple is resource, relation (up to 64 characters) and a subject that is exactly one of { principal_sub }, { principal_id } or { resource, relation }. The last form makes the tuple a set: everyone holding that relation on another resource of yours. It returns the number written and deleted and the rv. A body with no writes and no deletes is refused locally.
Things to know:
- Unknown fields are refused by the client before any request is sent, and the server's body schema is strict too, so a typo is a clear local
IdentityRequestError, not an opaque400. - A person must exist, but not be a member. A relation subject or an
owner_subis a person identity knows. Membership is deliberately not checked, so a success cannot tell you who belongs to an organization. A tuple for someone who is not a member grants nothing. - Errors name the item and a fixed code only, such as
items[3]withunknown_relation; no request value is echoed back. - Batches. One request carries at most 500 items (the
authz.write.max_itemssetting, ceiling 500). Give the client a larger list and it sends several requests one after another. They are not atomic: if a later request fails, the earlier ones stay applied and the error message says how many. Make your writes idempotent so you can repeat the whole call. - Quotas. The number of resources and of relations per organization and product is capped by the
authz.quota.resourcesandauthz.quota.relationssettings (100,000 and 1,000,000 by default). Over quota istoo_bigonitems.
Search#
for await (const ref of identity.listAccessible({ tenant, subject: { principal_sub: sub }, action: 'eidos.document.read', type: 'document' })) { /* ref: 'eidos:document:q3-plan' */}for await (const who of identity.subjectsFor({ tenant, resource: 'eidos:document:q3-plan', action: 'eidos.document.read' })) { /* who: { principal_id, principal_sub? } */}for await (const code of identity.actionsFor({ tenant, subject: { principal_sub: sub }, resource: 'eidos:document:q3-plan' })) { /* code: 'eidos.document.read' */}listAccessible yields the references of the given type that the person can perform action on. subject is exactly one of principal_id and principal_sub. subjectsFor yields the people who can perform action on one resource. actionsFor yields the permission codes the person may use on one resource, or the tenant-level ones when resource is left out; its subject is the same one of principal_id and principal_sub. All three are async iterators that follow the server's paging token until the last page (an empty next_token), and throw if the server repeats a token. The optional limit is a page size hint, not a cap on results.
On the server, the page size is the request's limit or 50 by default, never above 1,000 (authz.search.default_limit, authz.search.max_limit), and one search examines a bounded number of rows (authz.search.scan_budget, 1,000 by default, ceiling 10,000) before it stops.
Rules the server applies:
actionmust be a concrete permission code in your namespace. A wildcard is refused by the client.subjectsForalso needs the installation to have the explain surface enabled, because listing who holds authority is what that surface exists for. Without it the call is403.- A person who does not exist and a member with no authority get the same empty result.
- Every search writes one audit row with counts, never the references.
Plan features#
hasFeature(tenant, code) answers whether the organization's plan includes a feature of your product. It reads GET /authz/v1/features, which returns the installation's plan and features. The client reuses the list for 30 seconds at most, and drops it earlier when the organization's policy version changes.
Read your writes#
Every write returns rv, the organization's resource version. It is a decimal string and only grows. The client remembers the highest rv it has seen for each organization, from a write result, from an access token that carries one, or from the event stream, and sends it as min_rv with the next check and checkBatch.
If identity has not caught up to that version, it answers INDETERMINATE with the reason indeterminate_consistency instead of a possibly stale answer. The client waits 100 ms and asks once more, then returns that INDETERMINATE. It is never an ALLOW, so treat it as a refusal and, if it matters, try again shortly. A cached answer about a resource is stored under the rv it was decided at, so a write moves it to a new key.
If you make the call yourself, send the rv you last got back as min_rv. See Authorization decision API.
Invalidation events#
With events: true, the client opens GET /authz/v1/events per organization it checks. The stream only tells the cache to forget: a frame can drop a person's cached answers or move the version vector, and it can never create an ALLOW. A frame naming another organization is dropped, and any error or end of stream is a quiet reconnect after a jittered backoff, during which the cache simply misses. A connection lasts about 25 seconds on the server, and the client reopens it. The caller must be your product with an active installation in that organization. The caching consequences are in Decision client.
Errors#
| Error | When |
|---|---|
IdentityRequestError | A body that fails the local check, a response that cannot be read, a call that got no answer, or any status other than success and 404. It carries the status when there was one. |
NotAvailableError | The endpoint answered 404: the flag for that call is off. Degrade, for example by falling back to your own list. |