EddytorDocs & API
07 · Tables A→ZL · Row & column security

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

Group policies by 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.

On this page