Files
windmill/backend/tests/fixtures/permissions_test.sql
T
Ruben Fiszel f2be625348 feat: store hashed tokens instead of plaintext (#8217)
* feat: store hashed tokens in the token table instead of plaintext

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address review issues in token hash migration

- Update all base.sql fixtures to include token_hash/token_prefix columns
- Keep plaintext token for webhook tokens (needed for URL reconstruction)
- Restore get_token_by_prefix to query DB for webhook tokens
- Fix down migration to delete NULL-token rows before restoring NOT NULL
- Update parser fixture standalone schema
- Update EE dedicated_worker_ee.rs to use token_hash/token_prefix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore sqlx offline cache (only add new query files)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: keep writing plaintext token column for backward compat

Write to token column alongside token_hash until MIN_VERSION_SUPPORTS_TOKEN_HASH
(1.649.0) is reached. This ensures older workers can still authenticate
during rolling upgrades. Remove the separate UPDATE in new_webhook_token
since create_token_internal now writes plaintext directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: branch on MIN_VERSION to write plaintext token or null

Check MIN_VERSION_SUPPORTS_TOKEN_HASH at runtime: write plaintext to
token column while old workers exist, switch to NULL once all workers
are >= 1.649.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: set MIN_VERSION_SUPPORTS_TOKEN_HASH to 1.650.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use token_hash for email lookup and expiry notifications

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: rotate webhook tokens instead of recovering plaintext from DB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: use token_hash for native trigger token lookups and deletes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* sqlx

* refactor: drop webhook_token_prefix from native_trigger table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: backward compat for token rotation and make webhook_token_hash NOT NULL

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: prevent panic on short superadmin secret token prefix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: prevent panic on short superadmin secret token prefix

Replace all `token[0..TOKEN_PREFIX_LEN]` slicing with
`token.get(..TOKEN_PREFIX_LEN).unwrap_or(token)` to prevent
panics when a token shorter than 10 chars is provided (e.g.
malformed Authorization header, short superadmin secret).

Co-authored-by: hugocasa <hugocasa@users.noreply.github.com>

* fix: prevent panic on short token prefix slicing

Replace all `token[0..TOKEN_PREFIX_LEN]` with safe
`token.get(..TOKEN_PREFIX_LEN).unwrap_or(token)` to prevent panics
on malformed tokens shorter than 10 characters.

Co-authored-by: hugocasa <hugocasa@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert "fix: prevent panic on short superadmin secret token prefix"

This reverts commit 37ec2e5ad5.

* revert: remove unnecessary defensive token prefix slicing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add token_hash to end_user_email test fixture

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add integration tests for token hash migration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: correct token_hash test assertions for cache and version

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: add plaintext column removal reminder to test fixtures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: log count of orphaned triggers deleted during migration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: preserve orphaned triggers with error instead of deleting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: rename token_expiry_notification.token to token_hash and copy owner/expiration in rotate

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: hash existing plaintext values before renaming token_expiry_notification column

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove unnecessary length check in token_expiry_notification migration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* update dates and version

* updat ee ref + sqlx

* improve mcp migration

* fix: atomic token rotation with rollback on trigger update failure

rotate_webhook_token now atomically inserts the new token and deletes
the old one in a single transaction, preventing token leaks.

Returns new_token_hash so callers can clean up the new token if their
subsequent trigger update fails (which involves external HTTP calls
and cannot be in the same DB transaction).

- Handler: wraps post-rotation work; deletes new token on failure
- Google renewal: deletes new token if service_config update fails
- Tests updated to match new atomic semantics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* higher min version

* fix: defer old token deletion to avoid breaking triggers on update failure

rotate_webhook_token now keeps the old token alive and returns
old_token_hash. Callers delete it only after the trigger row has been
successfully updated. If the external service call or DB update fails,
the trigger keeps working with the old token.

Worst case: if the best-effort delete fails, the old token leaks as an
extra DB row — harmless compared to breaking the trigger.

Also update summarized_schema.txt for renamed columns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 2d0823a471014e2bc2d898c63518323946b7474f

This commit updates the EE repository reference after PR #437 was merged in windmill-ee-private.

Previous ee-repo-ref: 7aef8b06cb6f54c2bc89dd57b70947deed72553c

New ee-repo-ref: 2d0823a471014e2bc2d898c63518323946b7474f

Automated by sync-ee-ref workflow.

* fix: prevent panic on short tokens by using safe prefix extraction

Add safe_token_prefix() helper that uses .get(..TOKEN_PREFIX_LEN).unwrap_or(token)
instead of direct slice indexing, preventing panics when tokens are shorter than
10 characters (e.g., short superadmin secrets or malformed Bearer tokens).

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: HugoCasa <hugo@casademont.ch>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: hugocasa <hugocasa@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
2026-03-17 01:15:38 +00:00

239 lines
12 KiB
SQL

-- Permissions test fixture
-- This sets up a comprehensive permission testing scenario
-- ============================================
-- USERS
-- ============================================
-- Admin user (may already exist from base.sql, use ON CONFLICT)
INSERT INTO usr (workspace_id, email, username, is_admin, role)
VALUES ('test-workspace', 'admin@windmill.dev', 'admin', true, 'Admin')
ON CONFLICT (workspace_id, username) DO NOTHING;
-- Regular users (non-admin)
INSERT INTO usr (workspace_id, email, username, is_admin, role)
VALUES
('test-workspace', 'alice@windmill.dev', 'alice', false, 'Developer'),
('test-workspace', 'bob@windmill.dev', 'bob', false, 'Developer'),
('test-workspace', 'charlie@windmill.dev', 'charlie', false, 'Developer');
-- Operator user (can execute but cannot create/update scripts, flows, apps)
INSERT INTO usr (workspace_id, email, username, is_admin, operator, role)
VALUES
('test-workspace', 'operator@windmill.dev', 'operator', false, true, 'Operator');
-- Add users to password table (use ON CONFLICT since admin may exist from base.sql)
INSERT INTO password (email, password_hash, login_type, super_admin, verified, name)
VALUES
('admin@windmill.dev', 'dummy_hash', 'password', false, true, 'Admin User'),
('alice@windmill.dev', 'dummy_hash', 'password', false, true, 'Alice'),
('bob@windmill.dev', 'dummy_hash', 'password', false, true, 'Bob'),
('charlie@windmill.dev', 'dummy_hash', 'password', false, true, 'Charlie'),
('operator@windmill.dev', 'dummy_hash', 'password', false, true, 'Operator')
ON CONFLICT (email) DO NOTHING;
-- ============================================
-- TOKENS for authentication
-- ============================================
-- Tokens associated with emails (workspace-scoped)
-- The auth system will look up the user by email in the usr table
-- Note: tokens must be at least 10 characters (TOKEN_PREFIX_LEN)
INSERT INTO token (token_hash, token_prefix, token, email, label, super_admin, owner, workspace_id)
VALUES
(encode(sha256('ADMIN_TOKEN_TEST'::bytea), 'hex'), 'ADMIN_TOKE', 'ADMIN_TOKEN_TEST', 'admin@windmill.dev', 'Admin token', false, 'u/admin', 'test-workspace'),
(encode(sha256('ALICE_TOKEN_TEST'::bytea), 'hex'), 'ALICE_TOKE', 'ALICE_TOKEN_TEST', 'alice@windmill.dev', 'Alice token', false, 'u/alice', 'test-workspace'),
(encode(sha256('BOB_TOKEN_TEST12'::bytea), 'hex'), 'BOB_TOKEN_', 'BOB_TOKEN_TEST12', 'bob@windmill.dev', 'Bob token', false, 'u/bob', 'test-workspace'),
(encode(sha256('CHARLIE_TOKEN_01'::bytea), 'hex'), 'CHARLIE_TO', 'CHARLIE_TOKEN_01', 'charlie@windmill.dev', 'Charlie token', false, 'u/charlie', 'test-workspace'),
(encode(sha256('OPERATOR_TOKEN_1'::bytea), 'hex'), 'OPERATOR_T', 'OPERATOR_TOKEN_1', 'operator@windmill.dev', 'Operator token', false, 'u/operator', 'test-workspace');
-- ============================================
-- GROUPS
-- ============================================
-- 'developers' group - Charlie is a member
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'developers', 'Developer group', '{}');
-- 'editors' group - Bob is a member (has write access to some folders)
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'editors', 'Editor group', '{}');
-- Group memberships
INSERT INTO usr_to_group (workspace_id, group_, usr)
VALUES
('test-workspace', 'developers', 'charlie'),
('test-workspace', 'editors', 'bob');
-- ============================================
-- FOLDERS
-- ============================================
-- 'shared' folder:
-- - Alice has read-only access (extra_perms: u/alice -> false)
-- - Bob has write access (extra_perms: u/bob -> true)
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'shared', 'Shared Folder', '{"u/admin"}',
'{"u/alice": false, "u/bob": true}', 'admin');
-- 'team' folder:
-- - 'developers' group has read-only access
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'team', 'Team Folder', '{"u/admin"}',
'{"g/developers": false}', 'admin');
-- 'editable' folder:
-- - 'editors' group has write access
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'editable', 'Editable Folder', '{"u/admin"}',
'{"g/editors": true}', 'admin');
-- 'admin_only' folder:
-- - Only admin has access (but we'll add item-level perms for specific items)
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'admin_only', 'Admin Only Folder', '{"u/admin"}',
'{}', 'admin');
-- 'alice_owned' folder:
-- - Alice is an owner (owners must also be in extra_perms for get_folders_for_user to find them)
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by)
VALUES ('test-workspace', 'alice_owned', 'Alice Owned Folder', '{"u/alice"}',
'{"u/alice": true}', 'alice');
-- ============================================
-- SCRIPTS
-- ============================================
-- Alice's personal script
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1001, 'u/alice/my_script',
'export function main() { return "alice script"; }',
'deno', 'script', 'alice', '{}', 'Alice script', '', '', '{}');
-- Bob's personal script
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1002, 'u/bob/my_script',
'export function main() { return "bob script"; }',
'deno', 'script', 'bob', '{}', 'Bob script', '', '', '{}');
-- Script in shared folder (accessible by Alice read-only, Bob write)
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1003, 'f/shared/public_script',
'export function main() { return "public"; }',
'deno', 'script', 'admin', '{}', 'Public script', '', '', '{}');
-- Script in team folder (accessible by developers group)
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1004, 'f/team/team_script',
'export function main() { return "team"; }',
'deno', 'script', 'admin', '{}', 'Team script', '', '', '{}');
-- Script in admin_only folder but with explicit share to Alice
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1005, 'f/admin_only/shared_with_alice',
'export function main() { return "shared"; }',
'deno', 'script', 'admin', '{}', 'Shared with Alice', '', '',
'{"u/alice": false}');
-- Script in alice_owned folder
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1006, 'f/alice_owned/owner_script',
'export function main() { return "owner"; }',
'deno', 'script', 'alice', '{}', 'Owner script', '', '', '{}');
-- Alice's script with extra_perms sharing to Bob (read-only)
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
VALUES ('test-workspace', 1007, 'u/alice/extra_shared_script',
'export function main() { return "extra shared"; }',
'deno', 'script', 'alice', '{}', 'Extra shared script', '', '',
'{"u/bob": false}');
-- ============================================
-- FLOWS (with flow_versions)
-- ============================================
-- Create flows first (without versions)
INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, extra_perms)
VALUES
('test-workspace', 'u/alice/my_flow', 'Alice flow', '', '{"modules": []}', 'alice', NOW(), '{}', '{}'),
('test-workspace', 'u/bob/my_flow', 'Bob flow', '', '{"modules": []}', 'bob', NOW(), '{}', '{}'),
('test-workspace', 'f/shared/shared_flow', 'Shared flow', '', '{"modules": []}', 'admin', NOW(), '{}', '{}');
-- Create flow versions
INSERT INTO flow_version (id, workspace_id, path, value, schema, created_by, created_at)
VALUES
(1001, 'test-workspace', 'u/alice/my_flow', '{"modules": []}', '{}', 'alice', NOW()),
(1002, 'test-workspace', 'u/bob/my_flow', '{"modules": []}', '{}', 'bob', NOW()),
(1003, 'test-workspace', 'f/shared/shared_flow', '{"modules": []}', '{}', 'admin', NOW());
-- Update flows with version references
UPDATE flow SET versions = ARRAY[1001::bigint] WHERE path = 'u/alice/my_flow' AND workspace_id = 'test-workspace';
UPDATE flow SET versions = ARRAY[1002::bigint] WHERE path = 'u/bob/my_flow' AND workspace_id = 'test-workspace';
UPDATE flow SET versions = ARRAY[1003::bigint] WHERE path = 'f/shared/shared_flow' AND workspace_id = 'test-workspace';
-- ============================================
-- RESOURCES
-- ============================================
-- Alice's personal resource
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/alice/my_resource', '{"key": "alice_value"}',
'Alice resource', 'object', '{}', 'alice');
-- Bob's personal resource
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/bob/my_resource', '{"key": "bob_value"}',
'Bob resource', 'object', '{}', 'bob');
-- ============================================
-- VARIABLES
-- ============================================
-- Alice's personal variable (non-secret for testing permissions, not encryption)
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/alice/my_variable', 'alice_value', false,
'Alice variable', '{}');
-- Bob's personal variable (non-secret for testing permissions, not encryption)
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false,
'Bob variable', '{}');
-- ============================================
-- SCHEDULES
-- ============================================
-- Alice's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false,
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}');
-- Bob's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false,
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}');
-- ============================================
-- APPS (with app_versions)
-- ============================================
-- Alice's personal app
INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)
VALUES (2001, 'test-workspace', 'u/alice/my_app', 'Alice app', '{}',
'{"on_behalf_of": "u/alice", "on_behalf_of_email": "alice@windmill.dev", "execution_mode": "viewer"}', '{}');
-- Shared folder app
INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)
VALUES (2002, 'test-workspace', 'f/shared/shared_app', 'Shared app', '{}',
'{"on_behalf_of": "u/admin", "on_behalf_of_email": "admin@windmill.dev", "execution_mode": "viewer"}', '{}');
-- Create app versions
INSERT INTO app_version (id, app_id, value, created_by, created_at)
VALUES
(2001, 2001, '{"grid": []}', 'alice', NOW()),
(2002, 2002, '{"grid": []}', 'admin', NOW());
-- Update apps with version references
UPDATE app SET versions = ARRAY[2001::bigint] WHERE id = 2001;
UPDATE app SET versions = ARRAY[2002::bigint] WHERE id = 2002;