feat: read data as the job's identity, viewer is opt-in per runnable (#11744)

* 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>
This commit is contained in:
Ruben Fiszel
2026-10-14 00:05:52 +02:00
committed by GitHub
co-authored by Claude Opus 5.5 windmill-internal-app[bot]
parent 633135bb91
commit b7b3306195
43 changed files with 1424 additions and 137 deletions
+21
View File
@@ -569,6 +569,26 @@ export async function pushRawApp(
);
}
/**
* `@windmill-labs/shared-utils` up to 1.0.13 derives the policy without `viewer_data_access`,
* which would silently deploy an inline runnable that asks to connect as its viewer as one
* connecting as the app. The server honors the flag on inline runnables only.
*/
export function carryViewerDataAccess(
runnables: Record<string, any> | undefined,
policy: Policy | undefined,
) {
const triggerables = policy?.triggerables_v2 as Record<string, any> | undefined;
if (!runnables || !triggerables) return;
for (const [id, runnable] of Object.entries(runnables)) {
if (runnable?.viewer_data_access !== true) continue;
if (runnable.type !== "inline" && runnable.type !== "runnableByName") continue;
for (const [key, triggerable] of Object.entries(triggerables)) {
if (key.startsWith(`${id}:rawscript/`)) triggerable.viewer_data_access = true;
}
}
}
export async function generatingPolicy(
app: any,
path: string,
@@ -579,6 +599,7 @@ export async function generatingPolicy(
log.info(colors.gray(`Generating fresh policy for app ${path}...`));
try {
app.policy = await windmillUtils.updateRawAppPolicy(app.runnables, base);
carryViewerDataAccess(app.runnables, app.policy);
finalizeDerivedPolicy(app.policy, executionMode, isolation, true);
} catch (e) {
log.error(colors.red(`Error generating policy for app ${path}: ${e}`));
+9 -2
View File
@@ -6522,6 +6522,8 @@ fields:
value: "default_user"
\`\`\`
\`viewer_data_access: true\` makes an inline runnable read data tables and object storage as the person using the app instead of as the app's identity (see "Reading data as the viewer").
### Referencing existing scripts
To use an existing Windmill script instead of inline code:
@@ -6558,7 +6560,7 @@ data:
- \`<datatable>/<table>\` — Specific table in public schema
- \`<datatable>/<schema>:<table>\` — Table in specific schema
**Roles:** when a datatable is under roles, its queries run as a role, which only reaches what it was granted. \`roles\` records the role the app uses each datatable through; the app's code must pass the same role: \`wmill.datatable('main', { role: 'analyst' })\` in TypeScript, \`wmill.datatable('main', role='analyst')\` in Python. A datatable without an entry is used as its default role, if it has one, plus the roles of whoever is using the app unless that default is \`admin\`.
**Roles:** when a datatable is under roles, its queries run as a role, which only reaches what it was granted. \`roles\` records the role the app uses each datatable through; the app's code must pass the same role: \`wmill.datatable('main', { role: 'analyst' })\` in TypeScript, \`wmill.datatable('main', role='analyst')\` in Python. A datatable without an entry is used as its default role, if it has one, plus the roles of the app's identity, or of whoever is using the app for a runnable with \`viewer_data_access: true\`, unless that default is \`admin\`.
## SQL Migrations (sql_to_apply/)
@@ -6853,6 +6855,11 @@ frontend_api: [jobs:run, users:read]
A file with a \`sandbox\` key states both settings: \`sandbox: false\` turns isolation off, and a missing \`frontend_api\` means no scopes. A file without the key leaves the deployed app's isolation and scopes as they are, so removing \`sandbox: true\` does not unsandbox it. \`frontend_api\` needs \`sandbox: true\`; the push fails otherwise.
## Reading data as the viewer
A backend runnable reads data as the app's identity (the publisher in publisher mode), the same for every viewer. An inline runnable can instead read data as the person using the app: its data table connections that name no role hold that viewer's roles, and object storage permission rules (which paths may be listed, read, written) are checked against the viewer. Resources, variables and everything else stay the app's, and an anonymous viewer is refused. Turn it on only when what the runnable reads should differ per viewer: row-level security on a data table, or per-user paths in object storage. Both are Enterprise Edition features.
Set it with \`viewer_data_access: true\` in the runnable's \`backend/<id>.yaml\`.
## Data Tables
Data tables are PostgreSQL databases managed by Windmill. Backend runnables query them via the \`wmill\` client; the frontend never queries them directly. **When the app needs to store or persist data** (user data, settings, application state, records, logs), use a data table.
@@ -6864,7 +6871,7 @@ Data tables are PostgreSQL databases managed by Windmill. Backend runnables quer
3. **No DDL inside runnables**: runnables only read and write rows (SELECT, INSERT, UPDATE, DELETE) on existing tables. Never CREATE, ALTER or DROP a table from a runnable.
4. **Qualify table names**: an unqualified name means the \`public\` schema, so write every other table as \`schema.table\`, in table creation and queries alike. The app's \`data\` config sets the default datatable and schema its tables go in; use them consistently across runnables.
5. **Pass the role**: when the app's \`data.roles\` gives a data table a role, every \`wmill.datatable\` call on it passes that role (\`wmill.datatable('main', { role: 'analyst' })\` in TypeScript, \`wmill.datatable('main', role='analyst')\` in Python). The role only reaches what it was granted, so a query outside it fails with \`permission denied\`.
6. **Per-viewer rows**: a data table with no entry in \`data.roles\` is called with no role. Unless its default role is \`admin\`, the connection then holds the default role, if it has one, plus every role the person using the app is a tenant of, and is refused when that is none. Row-level security policies written as \`pg_has_role(current_user, '<role>', 'member')\` return that viewer's rows with nothing added to the runnable. A role pins the connection to that one role for every viewer, so where rows should differ per viewer give the data table no \`data.roles\` entry.
6. **Per-viewer rows**: a data table with no entry in \`data.roles\` is called with no role. Unless its default role is \`admin\`, the connection then holds the default role, if it may use it, plus every role it is a tenant of, and is refused when that is none. Whose roles those are follows the runnable's identity (see "Reading data as the viewer"). Row-level security policies written as \`pg_has_role(current_user, '<role>', 'member')\` then return the right rows with nothing added to the code. A role pins the connection to that one role either way, so a data table whose rows should differ per viewer gets no \`data.roles\` entry. Data table roles are an Enterprise Edition feature, self-hosted only.
\`wmill datatable list\` lists the workspace's data tables. Create or change tables with a migration in \`sql_to_apply/\` (see "SQL Migrations" above), then add them to \`data.tables\` in \`raw_app.yaml\`.