mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
f2be625348
* 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>
239 lines
12 KiB
SQL
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;
|