Tenant isolation, with keys of two columns
Every ticket is in one organisation, and only that organisation's members see it. Tickets are numbered per organisation, so their key has two columns.
authz
type org = app.orgs
member : user = app.org_members(org_id -> user_id)
admin : user = app.org_members(org_id -> user_id) where {role = 'admin'}
type ticket = app.tickets (org_id, id)
org : org = org_id
assignee : user = assignee_id
can edit = assignee or org.admin
can view = edit or org.member
rules app.tickets
select : view
insert : org.member
update : edit
update assignee_id : org.admin
update org_id, id : nobodyorg.memberfollows the ticket'sorgto that organisation's members: the tenant check, written once, where the type is. No query has to rememberWHERE org_id = ....where {role = 'admin'}makes a second relation from the same table.(org_id, id)after the table says the key has two columns. Inauthz.canand the other functions, such a row's id is its text,'(7,42)'.update org_id, id : nobodykeeps a ticket in its organisation;update assignee_id : org.adminnarrows one change to admins.
Tested
Cy is an admin of another organisation:
authz
test "a ticket is seen in its organisation, and nowhere else"
user $cy cannot view ticket $t
as user $cy sees 0 {SELECT FROM app.tickets WHERE org_id = $acme}
as user $cy refused {INSERT INTO app.tickets VALUES ($acme, 2, $cy, 'Not mine to file')}
as user $bo refused {UPDATE app.tickets SET assignee_id = $ann WHERE (org_id, id) = ($acme, 1)}
as user $ann refused {UPDATE app.tickets SET org_id = $globex WHERE (org_id, id) = ($acme, 1)}Try it
Open it in the playground: the tables, the policy and the test, in your browser, with nothing to install. Or take the files of docs/cookbook/tenants/ (schema.sql, policy.authz, tests.authz) and run rowstile dev. CI applies and tests them on every change (core/tests/cookbook.sh), and every line shown here is in those files.
What it costs
A key of two columns costs more to check than a single one: the id is built as text for each row (Speed and limits). If tickets had an id of their own, the same policy without (org_id, id) would be cheaper.
More: keys, the other recipes.