mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-15 08:02:53 +00:00
* feat: connect a data table as the job's own identity unless the runnable opts into its viewer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: key cached results by the data table mode and keep the flag across ai edits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: hash the cache key's mode and viewer as fixed fields so no argument name can forge them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update ee-repo-ref to 2f74548ad1ec50c8e1faf17e42d5648160e833f1 This commit updates the EE repository reference after PR #878 was merged in windmill-ee-private. Previous ee-repo-ref: 4ad768a777fff9f1ecc86a2803bc27d80f8242d6 New ee-repo-ref: 2f74548ad1ec50c8e1faf17e42d5648160e833f1 Automated by sync-ee-ref workflow. * feat: rename the flag to viewer_data_access and apply it to object storage permission rules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: describe viewer_data_access as covering object storage rules too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update ee-repo-ref to 0ffb633473c5465513576afc6f54d18f93c7f1e1 This commit updates the EE repository reference after PR #879 was merged in windmill-ee-private. Previous ee-repo-ref: e9e06a9eb94168f46b378276b01fa0117dc7d8ed New ee-repo-ref: 0ffb633473c5465513576afc6f54d18f93c7f1e1 Automated by sync-ee-ref workflow. * feat: show the viewer data access control as an icon button Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- 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>
105 lines
7.5 KiB
Markdown
105 lines
7.5 KiB
Markdown
# Data table connections and whose roles they hold
|
|
|
|
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 and the
|
|
roles of one **identity**:
|
|
|
|
| Default role | A connection naming no role holds |
|
|
|---|---|
|
|
| `admin` (until changed) | `admin`, as before identity roles existed |
|
|
| another role | that role when the identity may use it, plus **every role the identity is a tenant of** |
|
|
| none (`NO_DEFAULT_DATATABLE_ROLE`) | the identity's roles only |
|
|
|
|
The identity is the job's own, `permissioned_as`, like everything else a job reaches: the
|
|
publisher for a publisher-mode app, the owner of a schedule or trigger, whoever ran a script. It
|
|
is the job's **viewer** only for an inline runnable of a full-code app whose policy entry sets
|
|
`viewer_data_access` (`DatatableConnectAs::Viewer`). Postgres row-level security then answers per
|
|
viewer from a runnable that runs as the publisher, with nothing written in the code.
|
|
|
|
A connection left with no role at all is refused. 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`, `DatatableConnectAs`, `get_datatable_resource_from_db` |
|
|
| `windmill-common/src/datatable_roles_ee.rs` | `resolve_datatable_role_connection`, `resolve_unnamed_login`, `governing_identity`, `ensure_role_set_login`, `retire_role_set_logins_of`, `sweep_unused_role_set_logins` |
|
|
| `windmill-api/src/apps.rs` | `execute_component`, which sets `job_perms.viewer_data_access` for a flagged inline runnable |
|
|
| `windmill-api-jobs/src/execution.rs` | `run_end_user_email`, where a run's viewer is recorded, and `run_inline_preview_end_user` |
|
|
| `frontend/.../raw_apps/ViewerDataAccessPopup.svelte` | the runnable setting, shown when the code reads a data table or object storage |
|
|
| `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 identity roles existed. Nothing ever sets a
|
|
default back to `admin`: removing or deleting the default role leaves none.
|
|
- **One identity decides both the default and the added roles.** The default is held when that
|
|
identity may use it, and the roles added are those whose tenants name that same identity. A
|
|
job never mixes its own identity's default with its viewer's roles.
|
|
- **Connecting as the viewer is the publisher's choice, made per runnable and stored in the
|
|
policy.** It is honored on inline runnables only, whose code is deployed with the app: a path
|
|
runnable is someone else's script, which would read with the viewer's roles without its author
|
|
choosing to. The server stamps `job_perms.viewer_data_access` in the transaction that pushes
|
|
the job. Nested jobs do not inherit it; an inline preview run with the job's token
|
|
(`run_inline/preview`, what `wmill.datatable()` uses) does, since that code is the job's own.
|
|
- **A job that connects as its viewer and has none is refused**, never resolved as the job: that
|
|
would undo what the runnable asked for. An anonymous app viewer is the usual case.
|
|
- **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 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 identity 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 identity, one viewer and one mode.** `cached_result_path`
|
|
hashes the job's `permissioned_as`, its recorded viewer and `viewer_data_access` into the key,
|
|
since a cache hit returns before any connection is resolved. Turning the setting on changes no
|
|
code, so without the mode a result read with the app's roles would be served to the viewer.
|
|
- **Only a tenant list naming the identity counts.** A workspace admin may *use* every role,
|
|
`admin` included. The roles added are those whose tenants name the identity (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 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 identity actually connected with, so there are at most as many as (identity, 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.
|
|
- **Mixed fleets.** A worker or server older than v1.831 connects as the default role alone, and
|
|
refuses a data table with no default role, whose marker it reads as a role the data table
|
|
lacks. One from v1.831 or v1.832 adds the recorded viewer's roles to every job that has one,
|
|
flagged or not, and the job's own roles to none.
|
|
|
|
## Trust
|
|
|
|
A job that names no role reads with its own identity's roles, the same trust as the resources and
|
|
variables it reaches: every viewer of a publisher-mode app sees what the publisher's roles see.
|
|
An inline runnable that connects as its viewer runs its publisher's code with the viewer's reach
|
|
into the data table, the same trust as an app in viewer execution mode. Naming a role in the code
|
|
pins the connection to that one role either way.
|