Data read across tenants
Table of Contents
1. Summary
Most data belongs to one tenant, and a tenant reads only its own. Some rows belong to the system tenant and every tenant must read them. Reference data is one case. The service accounts are another. This page states the six building blocks that make a table readable across tenants. Every table that is read across tenants uses all six, and no table uses a second mechanism. A new shared table follows the checklist at the end.
2. Detail
2.1. When to share
Share a row only when every tenant has a reason to read it and the row holds no private data. A history names the service that wrote an entry. The service account holds a name, a picture and a type, so every tenant may read it. A person of a tenant is not shared. The party rule keeps applying to people.
Do not share a row to avoid a token exchange. A service that needs the data of one tenant acts inside that tenant instead. See Tenant ownership and the information flow.
2.2. The six building blocks
- Owner. A shared row has
tenant_id = system. A table that holds both tenant rows and shared rows adds a boolean columnshared, false by default. Only the seed sets it. A row is admitted by its owner and by the flag, so a later row of the system tenant stays hidden until a person sets the flag. Policy. One policy per command. The select policy admits the rows of the current tenant and the shared rows of the system tenant. The insert, update and delete policies admit only the current tenant. One
for allpolicy is not enough, because it uses oneusingclause for select, update and delete, and a tenant could then change or remove a shared row. A restrictive policy, such as the party rule, must also pass the shared rows. A restrictive policy only narrows.The generated delete rule needs the same care. A delete is a rule that closes the row, and the rule runs with the owner's rights, which row-level security does not bind. A tenant that reads a shared row could otherwise close it. The template adds
and tenant_id = ores_iam_current_tenant_id_fn()to the delete rule of every entity that states:system_tenant_visible: true. The pgTAP test of the table proves it.- Read. The model states
:system_tenant_visible: trueon its repository. The generated read then asks for the rows of the current tenant or of the system tenant. The policy decides which system rows return. A table whose rows are all shared states:tenant_read_scope: sharedand the read has no tenant predicate. Writes stay in the own tenant. - Guard. The read has a
:readcode, or an allow-list row with a reason, as the authorised reads rules require. - Existence check. A record that names a shared row, or a row the party rule hides, checks that it exists through a security definer function. The function returns yes or no and names no column. It takes the id and nothing else, and it takes the tenant from the session, so a caller cannot probe another tenant. It sets a fixed
search_path. The grant is revoked from public and given to the service roles by name. The codegen mapEXISTENCE_FUNCTIONSinorg_loader.pynames the function for each table, and the generated soft foreign key trigger calls it. - Page. This page. The model of the table links to it.
2.3. The accounts table
The accounts table follows the blocks. It is the open table: username, type, status, full name, picture, email, job title, reports-to and default party. The confidential data of an account is in other tables with their own read codes: the credential, the contact information, the sign-ins and the sessions. No column hides.
| Block | In the accounts table |
|---|---|
| Owner | The service accounts and the super admin belong to the system tenant. The column shared is true for them. |
| Policy | accounts_select_policy admits the current tenant and the shared system rows. accounts_insert_policy, accounts_update_policy and accounts_delete_policy admit the current tenant only. The party rule passes a shared row. |
| Read | :system_tenant_visible: true on the account repository. |
| Guard | iam::accounts:read. The account holds no private data that a shared row adds. |
| Existence | ores_iam_account_exists_fn(id). Every soft foreign key to the accounts table calls it. |
The super admin is a user account, because humans sign in with it. It is shared by its flag and not by its type.
2.4. Who sees which account
A person sees the accounts of the parties they have access to, the accounts of the system party of the tenant, and the shared accounts. A tenant administrator sees the tenant, because the administrator is linked to every party, the system party included. A service acting in a tenant works in no party, so the party rule passes it as a service actor and it reads the accounts of that tenant. The function ores_iam_actor_is_service_fn() decides this, and ores_iam_system_party_id_fn() names the system party. Both are security definer, because reading the actor's own account must not apply the policy to itself.
The existence function stays. A record that names a person asks it for yes or no, so the service does not need to read the account to write the record.
2.5. Who sees which party's compute work
The batches, workunits, results and grid samples of the compute grid carry the party that submitted the work. A session that acts in a party sees the work of that party and of the parties below it, and the work that has no party. A session that acts in the system party of its tenant administers the tenant, so it sees the work of every party of that tenant. The function ores_compute_session_is_system_party_fn() decides this, and it compares the session's party with ores_iam_system_party_id_fn(). The rule is inside one tenant. It reads no other tenant's work, and an operator still reads a tenant by entering it.
This is an exception to the party rule. Its reason, its limits and the state of its review and sign-off are in the RLS and permission exceptions register, entry E1. Every exception and installation-owned table of the compute grid is listed there, so that a security review can read them together.
2.6. An operator reads a tenant's work inside the tenant
The six blocks share a row of the system tenant with every tenant. The opposite direction, a system-tenant session that reads every tenant's rows, is not a sixth block. It is the widening that check_rls_system_tenant_widenings.py exists to stop, and the audit task removes the widenings that remain.
The compute grid shows the rule at work. The fleet (hosts, nodes, node samples) belongs to the installation, and the policy that exposes it is the shared-reference shape. The work (batches, workunits and results) belongs to the tenant that owns it. The operator still needs to see which tenant uses what share of the grid, and the answer is to read each tenant from inside it.
- The usage read counts the session's own tenant and groups it by party. It names no tenant, and a session of the system tenant reads the system tenant's work and no other tenant's.
- The BFF builds the operator's breakdown. It lists the tenants, enters each one with
readInsideTenant, reads the usage there, and totals it. The server records each entry and each exit. - A tenant that cannot be entered or read is listed as unavailable. It is never shown as empty.
- The operator's usage is read when the tab opens and when the person asks. It is not polled, because every read enters every tenant.
- The system tenant's load series takes the job counters from the work totals, a table of the installation that only the system tenant reads. The poller writes it by entering each tenant and summing the counters there. The table names no tenant, so a tenant session that reads it gets nothing.
- The compute policies carry no system-tenant read, and the guard's exception list names none of them.
An exception to the rule needs a stated reason, a review and a sign-off before it goes on the guard's list. The first change to the grid listed three read policies on the grid's behalf with a reason that had not been reviewed. It was withdrawn, because the documented way, entering each tenant, serves the need without a policy.
2.7. Checklist for a new shared table
- The rows belong to the system tenant, and no row holds private data.
- The table has the column
sharedif it also holds tenant rows. - The policy is one per command, every restrictive policy passes shared rows, and the delete rule keeps to the own tenant.
- The model states
:system_tenant_visible: true, or:tenant_read_scope: sharedif every row is shared. - The read has a read code or an allow-list row with a reason.
- A record that names a row of the table checks it through an existence function, and the map
EXISTENCE_FUNCTIONSnames the function. - A pgTAP test proves five facts: a tenant reads the shared rows, a tenant reads nothing else of the system tenant, a tenant reads no other tenant, a tenant changes or removes no shared row, and a tenant writes none for the system tenant.
- This page and the model link to each other.
3. See also
- Tenant ownership and the information flow: the four mechanisms and the reads that cross tenants.
- Authorised reads: the read codes and the allow-list.
- Multi-Tenancy Architecture: the accounts exception and the system tenant.
- Story: Split accounts into open and confidential data: why the standard exists.