Skip to content
Talk to our solutions team

Access

Every call to rules.svc carries its caller’s principal: the user, bot, service or agent its token names, with the privileges iam holds for them. What the call may do is decided by access rules, written and evaluated exactly like a data entity’s access rules: an expr expression over the user.* vocabulary and can(path, action), which asks the product’s privilege catalogue.

rules.svc has no role names of its own. Its defaults ask can("rules.scopes", …), so they work with whatever roles a product defines.

ActionRoutesDefault rule
readGET /rulesets, and each folder or scope it lists; reading and cancelling your own sweepscan("rules.scopes", "read")
executePOST /rules/execute, POST /rulecan("rules.scopes", "read")
runPOST /rules/run of what the tenant servescan("rules.scopes", "read")
draftPOST /rules/run with files: rule text the caller sendscan("rules.scopes", "owner")
evaluatePOST /rules/evaluate; starting a dry sweepcan("rules.scopes", "read")
applyPOST /rules/evaluate with apply: true; starting a sweep with apply: truecan("rules.scopes", "read-write")
managereading and cancelling anyone’s sweepscan("rules.scopes", "owner")

Privileges rank none < read < read-write < owner, so a caller with owner on rules.scopes passes every default.

A refusal answers 403 rules-access-denied, naming the action and the scope:

{"error":{"code":"rules-access-denied","context":null,"message":"access denied: apply invoice-routing","meta":null}}

A rule that cannot be evaluated, because iam cannot be reached for one, refuses too. It never passes.

The product’s privilege catalogue needs a rules.scopes leaf, granted to the roles that use rules: read to run and evaluate them, read-write to apply what they propose, owner to author drafts and manage everyone’s sweeps. A product with no such grant refuses every call by default.

The system user, which sweeps and data-triggered evaluations run as (Configuration), needs the same grants for the work it does: evaluate for a dry sweep, apply for a sweep or an event that writes.

A product refines any of the actions in rules/extend/access.yaml; the others keep their defaults. Each rule sees row.name, the scope or folder the call names, so a rule can single one out:

rules/extend/access.yaml
access:
execute: { language: expr, expression: 'can("rules.scopes", "read") || (user.ptyp == "bot" && row.name == "triage")' }
read: { expression: 'can("rules.scopes", "read") && row.name != "pricing"' }
apply: { expression: 'can("rules.scopes", "read-write") && row.name != "credit-limits"' }
  • language is expr, the default. Any other, an unknown action or key, or an empty expression fails the load of the product’s rules, naming the file.
  • A product that wants anyone in the tenant to execute its rules says so: execute: { expression: 'true' }. Nothing is open unless a product writes it.
  • rules/extend/ holds rules.svc’s settings and no rules; a .grl there fails the load.

A rule here is the product’s own and changes with the folder: an edit to access.yaml takes effect when the tenant’s rules load again.

Passing rules.svc’s access rule lets a call run a scope. Whatever the scope reads or writes is checked again where the data lives, for the same principal:

SourceChecked by
datadata.svc, for the caller’s own token: its access rules, row-level security, and its record of who wrote
seshatthe product’s access rules and row-level security for the entity, run in-process for the caller

So a write needs three things, each held by a different party:

  1. the right to apply the scope (apply, the product’s access rules here);
  2. the right to write that record where it lives (the entity’s access rules in data.svc);
  3. the rule author’s declaration of the write in the scope’s writes (Scopes).

A source that refuses the caller answers 403 rules-source-denied.