Create policies
Builder mode for common patterns, advanced Cedar for everything else, and member attributes for ABAC.
Policies are managed at
/v1/organisations/{org_id}/policies (see the
Policies API reference) with
the rls_policies:write scope - Builder or Admin. Each policy is written in one of
two modes.
Builder mode: structured definitions
Builder mode takes a small structured definition and compiles it to Cedar
server-side - no Cedar knowledge needed, and the UI can round-trip it. A definition
is effect + subject + (for RLS) condition:
curl -X POST "$BASE/api/v1/organisations/$ORG_ID/policies" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"kind": "rls",
"tableFqn": "prod.sales.orders",
"columnName": "region",
"definition": {
"effect": "forbid",
"subject": { "attribute": "group", "op": "equals", "value": "contractor" },
"condition": { "op": "in", "values": ["EU"] }
}
}'That definition generates:
forbid(principal, action == Eddytor::Action::"read", resource)
when {
principal.hasTag("group") && principal.getTag("group") == "contractor"
&& context has rowValue && ["EU"].contains(context.rowValue)
};Subjects (exactly one): {"everyone": true} · {"role": "viewer"} ·
{"attribute": "...", "op": "...", "value": "..."} with ops equals, notEquals,
startsWith, endsWith, contains.
RLS conditions (exactly one): {"op": "in"|"notIn", "values": [...]} (up to 50
values) · {"op": "startsWith"|"endsWith"|"contains", "value": "..."} ·
{"matchesAttribute": "<key>"} - the row value must equal the caller's attribute,
the building block of per-tenant filtering.
Advanced mode: raw Cedar
Anything the builder grammar doesn't cover - multiple conditions, email targeting,
combinations - is written as raw Cedar in policyText instead of definition.
Policies are type-checked against the schema at write time; a policy that doesn't
compile is rejected.
{
"kind": "cls",
"tableFqn": "prod.hr.employees",
"policyText": "permit(principal, action == Eddytor::Action::\"read\", resource);\nforbid(principal, action == Eddytor::Action::\"read\", resource == Eddytor::Column::\"prod.hr.employees::salary\") unless { principal.role == \"admin\" };"
}A policy is either builder-managed or advanced. Converting builder → advanced
requires an explicit detachDefinition: true and is one-way; going back means
recreating the policy.
Member attributes (ABAC)
Attributes are the per-member values policies reference via
principal.getTag(...) - the policy holds the logic, attributes hold the data:
# Replace the whole bag
curl -X PUT "$BASE/api/v1/organisations/$ORG_ID/users/$USER_ID/attributes" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"tenant": "ACME", "group": "contractor"}'
# Or merge a partial patch: null deletes a key, omitted keys are untouched
curl -X PATCH ... -d '{"group": null}'Values are strings (up to 64 attributes per member; keys ≤128 bytes, values ≤512
bytes). Attributes are org-scoped and readable/writable with the same
rls_policies:* scopes.
Updating & versioning
Every policy carries a version. Updates (PUT for full replace, PATCH for
partial) require the expectedVersion you last read; a concurrent change returns
409 - re-fetch and re-apply. enabled: false switches a policy off without
deleting it.
Good to know
tableId, not tableFqn - one physical
table can appear under several FQNs when reached through different storage
configurations. GET /v1/tables/resolve/{table_id} turns the id back into a
name.From MCP
AI clients get read-and-authoring help, not write access: list_policies,
validate_policy, and generate_policy (which returns both the Cedar text and the
exact REST body to apply it). Applying policies is deliberately REST/CLI-only.