Nested groups: teams inside teams
A relation can come from a link table, and it can be declared twice, with two sources. Here a team's members are the people in it and the members of its sub-teams, to any depth, loops included.
type team = app.teams
member : user = app.team_members(team_id -> user_id)
member : team#member = app.teams(parent_id -> id)
type document = app.documents
owner : user = owner_id
reader : team#member = app.document_teams(document_id -> team_id)
can view = owner or readerteam#memberis "the members of a team". The secondmemberline says: the members of a team whoseparent_idis this team are members of this team too.reader : team#membergives a document to a team's members, whoever they are that day. Nobody is copied into a list per document: joining Web is enough to read what Engineering reads.
Tested
Bo is in Web, Web is inside Engineering, and the document is given to Engineering:
test "what a team may read, its sub-teams' members may read"
user $bo cannot view document $d
given {INSERT INTO app.document_teams VALUES ($d, $eng)}
user $bo can view document $d
user $cy cannot view document $dTry 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/teams-inside-teams/ (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
Nested groups are worked out for each query, walking up from the asker's own teams; nothing is stored for them. Speed and limits has the measure: with 20,000 teams in chains ten deep, opening something given to the top of a chain takes 1.7 ms.
More: subjects and groups, sharing with a team, the other recipes.