Execution delegation
A trusted service, such as a portal or an internal app with its own users, can query curral on behalf of those users. The policy, row filters, masks, limits and audit then apply to the effective identity it names, not to the service’s API key. The contract is opt-in and works over HTTP and Flight SQL; Basic, JWT and ordinary API keys behave as before.
The delegating key
Section titled “The delegating key”Generate a key with curral gen-api-key and mark it in the
users file:
api_keys: - name: portal key_sha256: "<generated sha256>" roles: [] # never used for delegated requests delegation: true attributes: # optional strings describing the service tenant: tenant-a- A delegating key must name an effective identity on every data
request. Without one, or with one curral refuses, the request gets
403 (
execution identity refused): it never falls back to the key’s own roles. - Delegated SQL requests are read-only (
SELECT), even if a policy mistakenly allows writes. Semantic document writes require the separatedocs_write: trueopt-in introduced in v0.8.0; see below. attributesreach the policy asinput.actor.attributes. Use them to scope what each key may assert, for instance a tenant. Keys must be identifiers; values are strings of at most 256 bytes.
Checking the server: GET /v1/capabilities
Section titled “Checking the server: GET /v1/capabilities”Called with the key alone, without an identity, it grants no data access and describes the credential:
curl -H "Authorization: Bearer curral_..." https://curral.example.com/v1/capabilities{"execution_context_version": 1, "delegation_required": true, "subject": "portal", "attributes": {"tenant": "tenant-a"}, "formats": ["json","csv","ndjson","arrow"], "docs": {"enabled": true, "scope": "schema", "ranker": "bm25", "delegated_writes": false}, "version": "v0.8.0"}A client should check it before sending identities, and refuse a server
that does not advertise the execution_context_version it implements.
docs reports availability, scope, search ranker and whether the key supports
delegated document writes.
The execution context
Section titled “The execution context”Send the identity in the X-Curral-Execution-Context header (HTTP) or the
x-curral-execution-context metadata key on every Flight SQL call. The
value is unpadded base64url of one UTF-8 JSON object, at most 8 KiB and
8 levels deep:
{ "subject": "portal:tenant-a:user:42", "kind": "user", "tenant": "tenant-a", "page": {"id": "7", "slug": "sales-dashboard"}}| Field | |
|---|---|
subject |
required: a non-empty string of at most 256 bytes, without control characters or surrounding spaces. It becomes the effective user |
kind |
optional: a short string the policy can switch on |
| anything else | the service’s own, passed to the policy as given |
roles, groups, actor |
refused: a service cannot assert roles |
The effective identity gets only the roles that the identities: entries
of the users file map to its subject; the key’s roles are not inherited.
ctx=$(printf '%s' '{"subject":"portal:tenant-a:user:42","kind":"user","tenant":"tenant-a"}' \ | base64 | tr '+/' '-_' | tr -d '=\n')
curl https://curral.example.com/v1/query \ -H "Authorization: Bearer curral_..." \ -H "X-Curral-Execution-Context: $ctx" \ -d '{"sql": "SELECT count(*) FROM orders"}'Every authenticated response carries X-Curral-Effective-Subject (or the
x-curral-effective-subject Flight header metadata) with the identity the
request ran as, schema and dry-run responses included. Clients should check
it.
Delegation covers /v1/query (dry runs included), /v1/schema,
/v1/databases, document reads (GET /v1/docs, GET /v1/docs/{id},
POST /v1/docs/search), document writes with docs_write, and Flight SQL. A Flight handshake with a delegating
key returns the same key, never a session token, so send the identity with
every call.
Semantic documents
Section titled “Semantic documents”With --docs-scope schema, delegated identities read only documents within
their query scope. Document
writes (POST, PUT, PATCH, DELETE) are denied unless the key opts in:
api_keys: - name: portal key_sha256: "<generated sha256>" delegation: true docs_write: truedocs_write is valid only on a delegating key; otherwise the users file is
rejected. It enables the write path but does not grant permission: the
docs_allow decision must still allow the effective identity. It receives
input.user, input.roles, input.identity and input.actor along with
action, doc and current. The document’s author is the effective subject.
docs_allow if { input.actor.name == "portal" input.identity.kind == "user" "poet" in input.roles}The key cannot write without an effective identity, and SQL remains read-only.
Policy
Section titled “Policy”curral does not interpret subjects: the policy decides what each service may assert. It receives:
| Input | Value |
|---|---|
input.user |
the effective subject |
input.roles |
roles mapped to that subject by identities: |
input.identity |
the whole execution context |
input.actor |
{name, auth_method, attributes} of the delegating key |
Tie every rule that allows delegated requests to the actor, so one service cannot pass for another service’s users, or for your own:
package curralimport rego.v1
default allow := false
# Identities asserted by the portal key must belong to its tenant.portal_identity if { input.actor.name == "portal" input.identity.tenant == input.actor.attributes.tenant startswith(input.user, concat(":", ["portal", input.actor.attributes.tenant, ""]))}
allow if { portal_identity input.identity.kind in {"user", "page"} input.resolved input.statement_type == "SELECT" every t in input.tables { t == "sales.main.orders" }}
row_filters := {"sales.main.orders": "region = 'north'"} if { input.identity.kind == "page"}
masks := {"sales.main.orders": {"customer_email": "redact"}} if { input.identity.kind != "user"}Row filters and masks per identity
Section titled “Row filters and masks per identity”The policy can return row filters
as well as masks, and both can read the
structured identity from the curral_identity session variable (empty
when the request is not delegated):
json_extract_string(getvariable('curral_identity'), '$.page.slug') = slugCompare against the identity inside SQL like this rather than building SQL
strings from input.identity in Rego. Schema discovery, dry runs,
protections and per-user concurrency all use the effective identity.
Shared keys and audit
Section titled “Shared keys and audit”A key without delegation may send X-Curral-Caller-Context, in the
same format, to record who it is calling for. It changes nothing but the
audit trail: the request runs as the key, with its roles, filters and
quotas, and the policy sees the context as input.caller. A request with a
caller context is read-only. The two headers are mutually exclusive, and
each may appear only once.
Audit events carry the effective user, the
delegating actor, the structured identity (or caller for shared
keys), the policy hash and the request ID. A refused identity is recorded
as a delegation_denied event. Parameter values are never logged; SQL
redaction and the fail-closed audit keep working.
Trust model
Section titled “Trust model”The delegating service is a trust boundary: whoever holds its key can
assert any identity its policy rules accept. The service is responsible for
authenticating its users and deciding what they may reach before asking
curral. Use one key per service or tenant, TLS,
rotation with expires and proper secret storage. A signed end-user token
is not required by this model.