Files
windmill/docs/datatable-viewer-roles.md
T

3.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
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) is refused rather than resolved as its owner, which in a publisher-mode app would be the publisher.
  • Only a tenant list naming the viewer counts. A workspace admin may use every role, admin included, and admin is 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 never admin.
  • One role is that role's login. Several are a login per distinct set. wm_set_<hash> is a member of exactly those roles, with INHERIT; 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, so SET ROLE only narrows and RESET ROLE comes back to it. The prefix is reserved in validate_role_name.
  • A datatable_role_set row 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 made NOLOGIN. A login without a row is taken back with a new password on next use.
  • Disabling or dropping a role retires every set login holding it. NOLOGIN on 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 is admin unless changed. For the same reason the request reaches an agent worker's server in the role parameter, 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.