Skip to content

ReBAC (OpenFGA)

A RelationshipPolicy asks OpenFGA, a server that stores relations, if a subject has a relation to a table. Without the relation, Sluice denies the query. This is ReBAC: relationship-based access control.

Example

Goal: only a viewer of a table reads it.

The policy runs only as the rebac member of a composite engine. See Policy engines.

policies:
  directory: "./policies.d"
  engine: composite
  composite:
    members: ["yaml", "rebac"]
apiVersion: sluice.bino.bi/v1beta1
kind: RelationshipPolicy
metadata:
  name: shop-table-viewers
spec:
  match:
    any:
      - resources:
          catalogs: ["shop"]
  backend:
    endpoint: "http://localhost:8091"
    storeId: "demo-store"
  checks:
    - objectTemplate: "table:{{catalog}}.{{schema}}.{{table}}"
      relation: viewer

The example uses a stub in place of OpenFGA. The stub knows one tuple. A real model needs the type table with the relation viewer.

[
  {"user": "user:alice", "relation": "viewer", "object": "table:shop.main.orders"}
]

Alice reads the table.

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_alice.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, country, amount FROM shop.main.orders ORDER BY id"}'
{
  "query_id": "<query-id>",
  "columns": ["id", "country", "amount"],
  "rows": [
    [1, "de", 120],
    [2, "fr", 80]
  ],
  "row_count": 2,
  "truncated": false
}

Bob sends the same query.

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_bob.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, country, amount FROM shop.main.orders ORDER BY id"}'
{
  "code": "ACL_DENIED",
  "message": "relationship check failed on shop.main.orders",
  "query_id": "<query-id>",
  "policy": "shop-table-viewers"
}

For Bob, Sluice sent this body to POST <endpoint>/stores/<storeId>/check.

tail -n 1 data/checks.jsonl
{
  "tuple_key": {
    "user": "user:bob",
    "relation": "viewer",
    "object": "table:shop.main.orders"
  }
}

What happened: Sluice ran each check for each table that the policy matches. OpenFGA answered allowed: false for Bob. A passed check allows nothing: Alice also needs the SqlAccessPolicy of the yaml member.

Fields

Field Type Default Meaning
backend.endpoint string required URL of the OpenFGA server.
backend.storeId string required Store id.
backend.authorizationModelId string Model id for the check.
backend.tokenRef string Secret, sent as a Bearer token.
backend.timeout duration 3s Timeout of one check.
checks[].objectTemplate string required The object.
checks[].relation string required The relation.
checks[].subjectTemplate string user:{{subject.id}} The user.

Templates know {{catalog}}, {{schema}}, {{table}}, {{subject.id}} and {{subject.email}}. exclude takes single tables out of the check.

Sluice caches each answer for policies.rebac.cacheTtl (default 10s). A reload clears the cache.

Limits

  • A check decides on a whole table. It does not filter rows.
  • If OpenFGA fails, the query gets 503 ERR_POLICY_UNAVAILABLE. If it does not answer within backend.timeout, the query gets 504 ERR_TIMEOUT.
  • Write placeholders without spaces. Sluice sends {{ table }} and unknown placeholders as plain text.
  • With enforcementMode: Audit or DryRun, a failed check does not deny. The policy is listed under shadow, with would: deny for a failed check.
  • The selector cannot use resources.tags.

See also