4.4 KiB
Data table connections as the viewer
A data table under roles (Enterprise Edition) normally resolves to one role: the one a caller
names, or the data table's default. ?as=viewer resolves to every role the job's viewer is a
tenant of, so Postgres row-level security can answer per viewer from a backend runnable that
runs as the app's publisher.
wmill.datatable('main', { as: 'viewer' }) // TypeScript
wmill.datatable('main', as_viewer=True) # Python
ATTACH 'datatable://main?as=viewer' AS dt; -- DuckDB
-- PostgreSQL script: `datatable://main?as=viewer` as its `database` argument
Policies test membership, which is true for a role's own login too, so one policy serves both kinds of connection:
CREATE POLICY emea ON orders FOR SELECT
USING (region = 'emea' AND pg_has_role(current_user, 'sales_emea', 'member'));
A policy names its role as text, so renaming the role breaks it.
Code
| Where | What |
|---|---|
windmill-common/src/workspaces.rs |
DATATABLE_VIEWER_ROLE, parse_datatable_ref, DatatableAccess |
windmill-common/src/datatable_roles_ee.rs |
resolve_viewer_login, ensure_role_set_login, retire_role_set_logins_of, sweep_unused_role_set_logins |
windmill-api-jobs/src/execution.rs |
run_end_user_email, where a run's viewer is recorded |
What holds it together
- The viewer is
job_perms.end_user_email, and nothing else. The server records it when the run starts: the session behind an app request, the caller of a direct run or preview, and for a job started with another job's token, that job's viewer. No argument reaches it. A job without one (anonymous app viewer, schedule, trigger, a tool job started by an AI agent step) is refused rather than resolved as its owner, which in a publisher-mode app would be the publisher. - A cached result belongs to one viewer.
cached_result_pathhashes the run's recorded viewer into the key, since a cache hit returns before any connection is resolved. Runs with no viewer recorded keep sharing one entry. - Only a tenant list naming the viewer counts. A workspace admin may use every role,
adminincluded, andadminis the connection that owns the tables and bypasses row-level security. The viewer's roles are those whose tenants name them (user, group, folder,*), and neveradmin. - One role is that role's login. Several are a login per distinct set.
wm_set_<hash>is a member of exactly those roles, withINHERIT; the hash is over their catalog ids, so a rename moves nothing and a change in someone's groups resolves them to another login instead of editing one. Its privileges are its memberships, soSET ROLEonly narrows andRESET ROLEcomes back to it. The prefix is reserved invalidate_role_name. - A
datatable_role_setrow always names a login that can authenticate. The row is written after the login exists and removed, in its own commit, before the login is dropped or madeNOLOGIN. A login without a row is taken back with a new password on next use. - Set logins are bounded by use, not by the number of role combinations. One exists only for
a set some viewer actually connected with, so there are at most as many as (viewer, data table)
pairs.
last_used_atis recorded at most once a day, andsweep_unused_role_set_logins, run from the monitor loop, drops those unused for 30 days; a dropped one is made again on demand. The sweep goes by this instance's rows, never by the cluster'swm_set_*roles: a cluster can serve several Windmill instances. - Disabling or dropping a role retires every set login holding it.
NOLOGINon a role does not reach a login that is a member of it. - It travels in the reference, not in an annotation. A worker that predates
?as=refuses the reference as malformed. It would skip an unknown--annotation and connect as the default role, which isadminunless changed. For the same reason the request reaches an agent worker's server in theroleparameter, as a name no role can have.
Trust
A runnable connecting as the viewer runs its author's code with the viewer's reach into the data
table, the same trust as an app in viewer execution mode or a script someone else wrote and you
run. Whoever can deploy a publisher-mode app, or a script that others run, can therefore read
through the non-admin roles of the people who use it; nothing on the data table opts in to
that.