mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-15 16:03:02 +00:00
* feat: connect to a data table with every role of the job's viewer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: key a cached result by the run's recorded viewer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: pin the per-viewer cache key and fix a stale doc sentence Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: length-prefix the viewer in the result cache key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: drop data table role set logins unused for 30 days Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: pin the ee ref with the set login sweep fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: pin the ee ref with the sweep retry fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: give the cache key test its own migrated database Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: add the viewer's roles to a data table connection that names none The default role of a data table under roles is now a floor: a connection naming no role also holds the roles its viewer is a tenant of, unless the default is admin. This replaces the explicit as=viewer option, which is removed from the reference parser, the executors and both clients. The roles drawer warns while the default is admin and confirms before removing the default role. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: let a data table have no default role A connection naming no role then holds its viewer's roles only and is refused without any. The roles drawer offers the choice, states what an unnamed connection gets for the current settings, says who gets admin while it is the default, and notes who owns a table created over several roles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: start a data table's roles with admin open to everyone Turning roles on from the drawer seeded admin with no tenant, which refused every non-admin member's job as soon as it was saved. It now starts open to everyone, so the toggle changes nothing until the roles are edited, and the drawer warns for as long as everyone can connect as admin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update ee-repo-ref to 40ab35596e6fcb82a4dfc56e186befb807f8e2af This commit updates the EE repository reference after PR #873 was merged in windmill-ee-private. Previous ee-repo-ref: a5def184a29fffab8ed42c70c5a7445b2b3cbe98 New ee-repo-ref: 40ab35596e6fcb82a4dfc56e186befb807f8e2af Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
94 lines
6.1 KiB
Markdown
94 lines
6.1 KiB
Markdown
# Data table connections and the viewer's roles
|
|
|
|
A data table under roles (Enterprise Edition) resolves a connection that names a role to that
|
|
role alone. A connection that names none is resolved from the data table's default role:
|
|
|
|
| Default role | A connection naming no role holds |
|
|
|---|---|
|
|
| `admin` (until changed) | `admin`, as before viewer roles existed |
|
|
| another role | that role when the job's own identity may use it, plus **every role the job's viewer is a tenant of** |
|
|
| none (`NO_DEFAULT_DATATABLE_ROLE`) | the viewer's roles only |
|
|
|
|
A connection left with no role at all is refused. Postgres row-level security can then answer
|
|
per viewer from a backend runnable that runs as the app's publisher, with nothing written in the
|
|
runnable.
|
|
|
|
Policies test membership, which is true for a role's own login too, so one policy serves a named
|
|
connection and an unnamed one:
|
|
|
|
```sql
|
|
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` | `DatatableAccess`, `get_datatable_resource_from_db` |
|
|
| `windmill-common/src/datatable_roles_ee.rs` | `resolve_datatable_role_connection`, `viewer_role_ids`, `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 |
|
|
| `frontend/.../workspaceSettings/DataTablePermissionsButton.svelte` | the roles drawer, which warns while the default is `admin` |
|
|
|
|
## What holds it together
|
|
|
|
- **A default of `admin` is not a floor.** `admin` owns the tables and bypasses row-level
|
|
security, so nothing could be added to it, and it is the default until someone changes it.
|
|
Such a data table resolves exactly as it did before viewer roles existed. Nothing ever sets a
|
|
default back to `admin`: removing or deleting the default role leaves none.
|
|
- **The default role is tenant-checked against the job's own identity**, the publisher for a
|
|
publisher-mode app, and held only when that passes. The viewer's roles are added either way.
|
|
- **No role at all is a refusal, never an empty answer.** There is no login that would read
|
|
nothing: what a role sees is up to each table's grants and policies. A job without a recorded
|
|
viewer (anonymous app viewer, schedule, trigger, a tool job started by an AI agent step), or
|
|
whose viewer is not a member of the governing workspace, therefore needs a default it may use
|
|
or a named role.
|
|
- **A set login owns what it creates.** Postgres gives a new object to `current_user`, and
|
|
default privileges go by the creating role, so a table created over a connection that holds
|
|
several roles belongs to that set's login: no other role or viewer reaches it, and it passes to
|
|
`custom_instance_user` when the login is swept. Unnamed connections are for rows. To create an
|
|
object, name a role or run `SET ROLE <role>` first, which a set login may do for any role it
|
|
holds. Migrations are not affected: one that declares no role runs as `admin`.
|
|
- **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 cached result belongs to one viewer.** `cached_result_path` hashes 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,
|
|
`admin` included. 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`.
|
|
- **Resolving stays cheap where no viewer could add anything.** The viewer is only looked up when
|
|
some role other than the default and `admin` has tenants on that data table.
|
|
- **A `datatable_role_set` row with a password names a login that can authenticate with it.** 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,
|
|
and so is one whose row has an empty password: that is what a sweep leaves when it could
|
|
neither drop a login nor let it back in, so the next sweep still finds it.
|
|
- **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_at` is recorded at most once a day, and `sweep_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's `wm_set_*` roles: a cluster can
|
|
serve several Windmill instances.
|
|
- **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.
|
|
- **A worker or server that predates this connects as the default role alone**, so a mixed fleet
|
|
shows a viewer fewer rows, never more. It refuses a data table with no default role, whose
|
|
marker it reads as a role the data table lacks.
|
|
|
|
## Trust
|
|
|
|
A job that names no role 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. Naming a role in the code is how an author pins the
|
|
connection to one role instead.
|