Tenant ownership and the information flow

Table of Contents

1. Summary

Every row belongs to one of three owners, and the schema names only two. A tenant owns its own business data; the system tenant owns the installation's registers and the shared datasets every tenant reads; the installation owns the facts about the deployment itself — which services run, which nodes report, what the bus is doing. Because the third owner has no name, its rows are stored under the system tenant and read through policies written for tenant data, so every read of them is a workaround. This page names the three owners, states the mechanism each deserves, and lists where the missing third class already bites.

2. Detail

2.1. The three owners

Owner What it owns Examples
A tenant Its own business data parties, portfolios, books, trades, workunits, batches, results, a party's quote series, routes and subscriptions, synthetic configuration
The system tenant The installation's registers, and the datasets every tenant reads the tenants registry, service accounts, permissions, roles, seed profiles; countries, currencies, coding schemes, asset classes, avatars, calendars
The installation Facts about the deployment itself, owned by nobody in particular the services roster and its instances, hosts and wrapper nodes, grid and node samples, telemetry logs, message-bus samples, the database and build row, the expected-services registry, the tick streams and their points

The third row is the one the schema does not name. Its rows have no tenant in any meaningful sense: a node sample describes a machine, and the machine runs whatever work the installation gives it.

A tick point is the same kind of row: an observation of a source, not a tenant's data. It carries no tenant and no row-level security, only the marketdata service's roles reach it, and a party reads it through its own series and entitlement. See A source tick is stored once and entitled when it is read.

2.2. What the schema does today

Measured across projects/ores.sql/create on 2026-10-07, by a throwaway script that matched every create policy statement and classified its body by the tenant functions it names: a widening compares the session to the system tenant, a system-owned policy compares tenant_id to the system tenant, an own-tenant policy names the current tenant and no system function, and a policy that names neither is scoped by party, sandbox or role. The same run reproduced the guard's count of check_rls_system_tenant_widenings.py:

Policy shape Count Meaning
Own tenant only 353 tenant_id = ores_iam_current_tenant_id_fn()
Party or sandbox scoping 152 keyed on party_id or a sandbox, with no tenant predicate
A system-tenant session sees every tenant 10 current = system; every one is listed as under audit by the guard
System-owned rows readable by every tenant 14 tenant_id = system; the shared-reference idiom
A role-scoped cross-tenant read 3 using (true) granted to the market-data service role, keyed on neither tenant nor party
Total 532 every policy in projects/ores.sql/create

The count does not include the policy on accounts that limits a read to the parties the caller's account works in. That policy is a special case and belongs to accounts alone. It is hand-written, it names no system tenant function, and it only narrows a read the own-tenant policy already bounds. See Multi-Tenancy Architecture, "The accounts exception".

The guarded shape is the third. check_rls_system_tenant_widenings.py fails any new one, and its message is the rule: "A service that needs a tenant's data acts inside that tenant, with a token for it (Token Exchange), instead."

2.3. The four mechanisms, and what each is for

Mechanism What it does Correct use
Own-tenant policy Scopes every read to the session's tenant. A tenant's data.
System-owned policy (tenant_id = system) Every session sees the system's rows. Shared reference data.
:tenant_read_scope: shared The read states no tenant predicate; the policy decides. A read whose rows the policy already admits.
tenant_context::with_tenant, or internal impersonation Acts inside a tenant. Reaching a tenant's data from outside it.

Data read across tenants states the building blocks of the second and third rows together, and the checklist for a new shared table.

The last row has two forms because it serves two layers: with_tenant sets app.current_tenant_id for SQL, and Internal Actor Impersonation mints a JWT so a service can drive another tenant's NATS handlers. Both are the Token Exchange pattern.

2.4. The hole

Installation-scoped data is stored in tenant-scoped tables. Every read of it must therefore answer "which tenant?", and there is no right answer — only four workarounds: write it under the system tenant and hope the policy admits it; widen the policy, which the guard exists to stop; state no predicate and let a policy written for tenant data decide; or loop over tenants and impersonate each.

2.5. Where it bites today

  1. The grid summary counts the wrong thing. ores_compute_grid_stats_fn takes a tenant, and the poller that calls it holds the system tenant, so the stored summary is that one tenant's slice. Measured on the running fleet: the stored sample reported total_workunits = 0 while the installation held one workunit and two batches under another tenant.
  2. The node and grid samples carry a constant, not attribution. Every sample is written under the system tenant, because the launcher bakes the system tenant into every wrapper's --tenant-id. The tenant_id on those rows says nothing about the node or the work; it holds the same value in every row.
  3. The roster works by accident. The services roster reads every expected instance and every instance's last report. It works only because every process that reports is a service account, and service accounts live in the system tenant — so no policy had to be widened. The same read over a tenant-owned subject would not work.
  4. The compute service cannot enumerate tenants. The tenants registry is owned by the system tenant, so a system-tenant session may read it — but the only C++ that reads that table is ores.iam.core, and ores.compute.service has no dependency on it. The one cross-tenant read the design blesses is unreachable from a component that needs it.

2.6. The design

Name the third owner, and give it the mechanism it has earned.

  • Class I — the installation. Rows that describe the deployment. Services write them, and they carry the system tenant as a constant rather than as an owner. Their policy is the shared-reference shape — tenant_id = current or tenant_id = system — which the guard does not flag, because it admits no tenant's data to anyone: the rows it exposes are the installation's own. The permission decides who may call the read, exactly as compute::grid_samples:read does today.
  • Class R — shared reference. Already right: system-owned rows every tenant reads, with writes tenant-scoped.
  • Class T — a tenant. Already right: an own-tenant policy, with cross-tenant access only by acting inside the tenant.
  • A cross-tenant aggregate is Class T, not Class I. A count of every tenant's workunits is not a fact about the installation; it is a fact about several tenants at once, and no single tenant owns it. The sanctioned way to compute one is to act inside each tenant. If the Operations area needs installation-wide work totals, the aggregate needs an owner allowed to enter tenants — the mechanism Read each tenant's cache partition inside its tenant is building, and the decision belongs with Audit the policies that let the system tenant read every tenant.

    The grid's work totals are the first such aggregate. The poller enters each tenant, reads that tenant's counters there, and writes the sum to ores_compute_work_totals_tbl, whose policy admits a session of the system tenant and no other. The sum is stored in a table of its own and not in the fleet row, because every session reads the fleet row. A function that runs with its owner's rights to count across tenants is not the way: it lets any database role read any tenant's counts.

2.7. What follows for the grid

The fleet is Class I: hosts, wrappers, node samples and the services roster describe the installation, and every session the permission admits should read them whole. The work counts are Class T: workunits, batches and results belong to the tenant that owns them.

The installation-wide work totals are now written by the poller into a table that only the system tenant reads (entry E8 of the RLS and permission exceptions register).

So the two questions the screen raises — "is the grid healthy?" and "what is my tenant running?" — are the same problem seen from two sides, and both want the same answer: read the fleet as the installation, and enter a tenant for its work. That makes the Operations grid a read-only screen over the fleet, and moves installation-wide work totals to the audit, where the cross-tenant mechanism is being decided.

The usage tab applies this. The compute service counts one tenant and groups it by party. The BFF enters each tenant in turn and reads that count there, so the operator's breakdown by tenant needs no policy that reaches across tenants. The page Data read across tenants states the rule and what an exception needs.

3. See also

Emacs 29.3 (Org mode 9.6.15)