mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
add comprehensive permissions tests and restrict operator script archiving
- Add permissions test suite covering scripts, flows, apps, resources, variables, schedules, and folder-based permissions - Add test for operator restrictions (cannot create/update/archive) - Restrict operators from archiving scripts (archive_script_by_path, archive_script_by_hash) for security consistency with flows/apps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/otel_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/otel_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/secret_backend/vault_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/secret_backend/vault_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/oidc_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/oidc_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/db_iam_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/db_iam_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/job_s3_helpers_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/job_s3_helpers_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/email_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/email_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/teams_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/teams_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/stats_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/stats_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-common/src/git_sync_ee.rs|/home/rfiszel/windmill-ee-private/windmill-common/src/git_sync_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-indexer/src/indexer_ee.rs|/home/rfiszel/windmill-ee-private/windmill-indexer/src/indexer_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-indexer/src/service_logs_ee.rs|/home/rfiszel/windmill-ee-private/windmill-indexer/src/service_logs_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-indexer/src/completed_runs_ee.rs|/home/rfiszel/windmill-ee-private/windmill-indexer/src/completed_runs_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-git-sync/src/git_sync_ee.rs|/home/rfiszel/windmill-ee-private/windmill-git-sync/src/git_sync_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-autoscaling/src/kubernetes_integration_ee.rs|/home/rfiszel/windmill-ee-private/windmill-autoscaling/src/kubernetes_integration_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-autoscaling/src/autoscaling_ee.rs|/home/rfiszel/windmill-ee-private/windmill-autoscaling/src/autoscaling_ee.rs
|
||||
/home/rfiszel/windmill/backend/src/windows_service_ee.rs|/home/rfiszel/windmill-ee-private/src/windows_service_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/s3_proxy_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/s3_proxy_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/teams_approvals_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/teams_approvals_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/email/handler_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/email/handler_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/email/mod_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/email/mod_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/nats/handler_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/nats/handler_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/nats/mod_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/nats/mod_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/nats/listener_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/nats/listener_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/sqs/handler_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/sqs/handler_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/sqs/mod_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/sqs/mod_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/sqs/listener_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/sqs/listener_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/gcp/handler_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/gcp/handler_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/gcp/mod_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/gcp/mod_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/gcp/listener_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/gcp/listener_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/kafka/handler_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/kafka/handler_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/kafka/mod_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/kafka/mod_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/triggers/kafka/listener_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/triggers/kafka/listener_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/oidc_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/oidc_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/teams_cache_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/teams_cache_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/mcp_oauth_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/mcp_oauth_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/workspaces_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/workspaces_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/stripe_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/stripe_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/apps_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/apps_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/scim_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/scim_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/job_helpers_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/job_helpers_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/teams_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/teams_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/inkeep_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/inkeep_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/oauth2_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/oauth2_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/users_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/users_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/azure_proxy_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/azure_proxy_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/indexer_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/indexer_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/smtp_server_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/smtp_server_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/agent_workers_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/agent_workers_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/git_sync_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/git_sync_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-api/src/saml_ee.rs|/home/rfiszel/windmill-ee-private/windmill-api/src/saml_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-queue/src/jobs_ee.rs|/home/rfiszel/windmill-ee-private/windmill-queue/src/jobs_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-worker/src/otel_ee.rs|/home/rfiszel/windmill-ee-private/windmill-worker/src/otel_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-worker/src/otel_tracing_proxy_ee.rs|/home/rfiszel/windmill-ee-private/windmill-worker/src/otel_tracing_proxy_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-worker/src/dedicated_worker_ee.rs|/home/rfiszel/windmill-ee-private/windmill-worker/src/dedicated_worker_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-worker/src/job_logger_ee.rs|/home/rfiszel/windmill-ee-private/windmill-worker/src/job_logger_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-worker/src/memory_ee.rs|/home/rfiszel/windmill-ee-private/windmill-worker/src/memory_ee.rs
|
||||
/home/rfiszel/windmill/backend/windmill-audit/src/audit_ee.rs|/home/rfiszel/windmill-ee-private/windmill-audit/src/audit_ee.rs
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
-- 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, email, label, super_admin, owner, workspace_id)
|
||||
VALUES
|
||||
('ADMIN_TOKEN_TEST', 'admin@windmill.dev', 'Admin token', false, 'u/admin', 'test-workspace'),
|
||||
('ALICE_TOKEN_TEST', 'alice@windmill.dev', 'Alice token', false, 'u/alice', 'test-workspace'),
|
||||
('BOB_TOKEN_TEST12', 'bob@windmill.dev', 'Bob token', false, 'u/bob', 'test-workspace'),
|
||||
('CHARLIE_TOKEN_01', 'charlie@windmill.dev', 'Charlie token', false, 'u/charlie', 'test-workspace'),
|
||||
('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;
|
||||
@@ -0,0 +1,776 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
/// Helper to create a client authenticated as a specific user
|
||||
async fn create_client_for_user(_port: u16, token: &str) -> reqwest::Client {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
reqwest::header::HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
|
||||
);
|
||||
reqwest::ClientBuilder::new()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Test helper to check if a GET request succeeds
|
||||
async fn can_read(client: &reqwest::Client, url: &str) -> bool {
|
||||
let resp = client.get(url).send().await.unwrap();
|
||||
resp.status().is_success()
|
||||
}
|
||||
|
||||
/// Test helper to check if a POST request succeeds (for write operations)
|
||||
async fn can_write(client: &reqwest::Client, url: &str, body: serde_json::Value) -> bool {
|
||||
let resp = client.post(url).json(&body).send().await.unwrap();
|
||||
let status = resp.status();
|
||||
// 200, 201, 204 are success, 403 is forbidden, 401 is unauthorized
|
||||
status.is_success()
|
||||
}
|
||||
|
||||
/// Comprehensive permissions test that verifies:
|
||||
/// - Non-admin users can only access their user-space (u/username)
|
||||
/// - Non-admin users can only access folders they have been given access to
|
||||
/// - Read access when given read permission
|
||||
/// - Write access when given write permission
|
||||
/// - Access through direct permissions (u/username)
|
||||
/// - Access through group memberships (g/groupname)
|
||||
///
|
||||
/// This test is ignored in CI because it requires a full Windmill setup
|
||||
/// and takes longer to run. Run it locally with:
|
||||
/// `cargo test --features deno_core permissions -- --ignored`
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
async fn test_permissions_exhaustive(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let base_url = format!("http://localhost:{}/api", port);
|
||||
|
||||
// Create clients for each user (tokens must be at least 10 chars)
|
||||
let admin_client = create_client_for_user(port, "ADMIN_TOKEN_TEST").await;
|
||||
let alice_client = create_client_for_user(port, "ALICE_TOKEN_TEST").await;
|
||||
let bob_client = create_client_for_user(port, "BOB_TOKEN_TEST12").await;
|
||||
let charlie_client = create_client_for_user(port, "CHARLIE_TOKEN_01").await;
|
||||
|
||||
// ============================================
|
||||
// TEST 1: User namespace access (u/username)
|
||||
// ============================================
|
||||
|
||||
// Alice should be able to read her own scripts
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/u/alice/my_script")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read her own script"
|
||||
);
|
||||
|
||||
// Alice should NOT be able to read Bob's scripts
|
||||
assert!(
|
||||
!can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/u/bob/my_script")
|
||||
)
|
||||
.await,
|
||||
"Alice should NOT be able to read Bob's script"
|
||||
);
|
||||
|
||||
// Bob should be able to read his own scripts
|
||||
assert!(
|
||||
can_read(
|
||||
&bob_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/u/bob/my_script")
|
||||
)
|
||||
.await,
|
||||
"Bob should be able to read his own script"
|
||||
);
|
||||
|
||||
// Bob should NOT be able to read Alice's scripts
|
||||
assert!(
|
||||
!can_read(
|
||||
&bob_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/u/alice/my_script")
|
||||
)
|
||||
.await,
|
||||
"Bob should NOT be able to read Alice's script"
|
||||
);
|
||||
|
||||
// Admin should be able to read any script
|
||||
assert!(
|
||||
can_read(
|
||||
&admin_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/u/alice/my_script")
|
||||
)
|
||||
.await,
|
||||
"Admin should be able to read Alice's script"
|
||||
);
|
||||
assert!(
|
||||
can_read(
|
||||
&admin_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/u/bob/my_script")
|
||||
)
|
||||
.await,
|
||||
"Admin should be able to read Bob's script"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 2: Folder access without permissions
|
||||
// ============================================
|
||||
|
||||
// Charlie has no folder permissions, should not be able to access folder items
|
||||
assert!(
|
||||
!can_read(
|
||||
&charlie_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/shared/public_script")
|
||||
)
|
||||
.await,
|
||||
"Charlie should NOT be able to read shared folder script without permissions"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 3: Folder read-only access
|
||||
// ============================================
|
||||
|
||||
// Alice has read-only access to 'shared' folder
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/shared/public_script")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read shared folder script (read permission)"
|
||||
);
|
||||
|
||||
// Alice should NOT be able to create/update scripts in shared folder (read-only)
|
||||
let create_script_body = json!({
|
||||
"path": "f/shared/alice_new_script",
|
||||
"content": "export function main() { return 'test'; }",
|
||||
"language": "deno",
|
||||
"summary": "Test script",
|
||||
"description": ""
|
||||
});
|
||||
assert!(
|
||||
!can_write(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/create"),
|
||||
create_script_body.clone()
|
||||
)
|
||||
.await,
|
||||
"Alice should NOT be able to create scripts in shared folder (read-only)"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 4: Folder write access
|
||||
// ============================================
|
||||
|
||||
// Bob has write access to 'shared' folder
|
||||
assert!(
|
||||
can_read(
|
||||
&bob_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/shared/public_script")
|
||||
)
|
||||
.await,
|
||||
"Bob should be able to read shared folder script (write permission includes read)"
|
||||
);
|
||||
|
||||
// Bob should be able to create scripts in shared folder (write access)
|
||||
let create_script_body = json!({
|
||||
"path": "f/shared/bob_new_script",
|
||||
"content": "export function main() { return 'test'; }",
|
||||
"language": "deno",
|
||||
"summary": "Test script",
|
||||
"description": "",
|
||||
"schema": {}
|
||||
});
|
||||
assert!(
|
||||
can_write(
|
||||
&bob_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/create"),
|
||||
create_script_body
|
||||
)
|
||||
.await,
|
||||
"Bob should be able to create scripts in shared folder (write permission)"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 5: Group-based folder access
|
||||
// ============================================
|
||||
|
||||
// Charlie is in the 'developers' group which has read access to 'team' folder
|
||||
// First, let's verify Charlie has no direct access
|
||||
// Then verify he gets access through group membership
|
||||
|
||||
assert!(
|
||||
can_read(
|
||||
&charlie_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/team/team_script")
|
||||
)
|
||||
.await,
|
||||
"Charlie should be able to read team folder script via group membership"
|
||||
);
|
||||
|
||||
// Charlie's group only has read access, so he shouldn't be able to write
|
||||
let create_team_script = json!({
|
||||
"path": "f/team/charlie_script",
|
||||
"content": "export function main() { return 'test'; }",
|
||||
"language": "deno",
|
||||
"summary": "Test script",
|
||||
"description": "",
|
||||
"schema": {}
|
||||
});
|
||||
assert!(
|
||||
!can_write(
|
||||
&charlie_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/create"),
|
||||
create_team_script
|
||||
)
|
||||
.await,
|
||||
"Charlie should NOT be able to create scripts in team folder (group has read-only)"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 6: Resource permissions
|
||||
// ============================================
|
||||
|
||||
// Alice should be able to read her own resources
|
||||
// Resources use /resources/get/{path} (no /p/ prefix)
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/resources/get/u/alice/my_resource")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read her own resource"
|
||||
);
|
||||
|
||||
// Alice should NOT be able to read Bob's resources
|
||||
assert!(
|
||||
!can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/resources/get/u/bob/my_resource")
|
||||
)
|
||||
.await,
|
||||
"Alice should NOT be able to read Bob's resource"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 7: Variable permissions
|
||||
// ============================================
|
||||
|
||||
// Alice should be able to read her own variables
|
||||
// Variables use /variables/get/{path} (no /p/ prefix)
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/variables/get/u/alice/my_variable")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read her own variable"
|
||||
);
|
||||
|
||||
// Alice should NOT be able to read Bob's variables
|
||||
assert!(
|
||||
!can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/variables/get/u/bob/my_variable")
|
||||
)
|
||||
.await,
|
||||
"Alice should NOT be able to read Bob's variable"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 8: Flow permissions
|
||||
// ============================================
|
||||
|
||||
// Alice should be able to read flows in folders she has access to
|
||||
// Flows use /flows/get/{path} (no /p/ prefix)
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/flows/get/f/shared/shared_flow")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read shared folder flow"
|
||||
);
|
||||
|
||||
// Charlie should NOT be able to read flows in shared folder (no access)
|
||||
assert!(
|
||||
!can_read(
|
||||
&charlie_client,
|
||||
&format!("{base_url}/w/test-workspace/flows/get/f/shared/shared_flow")
|
||||
)
|
||||
.await,
|
||||
"Charlie should NOT be able to read shared folder flow (no permission)"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 9: Direct extra_perms grants
|
||||
// ============================================
|
||||
|
||||
// Alice has direct read permission on a specific script owned by admin
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/admin_only/shared_with_alice")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read script explicitly shared with her"
|
||||
);
|
||||
|
||||
// Bob should NOT be able to read that script (not shared with him)
|
||||
assert!(
|
||||
!can_read(
|
||||
&bob_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/admin_only/shared_with_alice")
|
||||
)
|
||||
.await,
|
||||
"Bob should NOT be able to read script only shared with Alice"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 10: Write via group membership
|
||||
// ============================================
|
||||
|
||||
// Bob is in the 'editors' group which has write access to 'editable' folder
|
||||
let create_editable_script = json!({
|
||||
"path": "f/editable/bob_editor_script",
|
||||
"content": "export function main() { return 'test'; }",
|
||||
"language": "deno",
|
||||
"summary": "Test script",
|
||||
"description": "",
|
||||
"schema": {}
|
||||
});
|
||||
assert!(
|
||||
can_write(
|
||||
&bob_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/create"),
|
||||
create_editable_script
|
||||
)
|
||||
.await,
|
||||
"Bob should be able to create scripts in editable folder via group write access"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 11: Schedule permissions
|
||||
// ============================================
|
||||
|
||||
// Schedules follow the same permission model
|
||||
// Schedules use /schedules/get/{path} (no /p/ prefix)
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/schedules/get/u/alice/my_schedule")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read her own schedule"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/schedules/get/u/bob/my_schedule")
|
||||
)
|
||||
.await,
|
||||
"Alice should NOT be able to read Bob's schedule"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 12: App permissions
|
||||
// ============================================
|
||||
|
||||
// Alice should be able to read apps in shared folder
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/apps/get/p/f/shared/shared_app")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read shared folder app"
|
||||
);
|
||||
|
||||
// Charlie should NOT be able to read apps in shared folder
|
||||
assert!(
|
||||
!can_read(
|
||||
&charlie_client,
|
||||
&format!("{base_url}/w/test-workspace/apps/get/p/f/shared/shared_app")
|
||||
)
|
||||
.await,
|
||||
"Charlie should NOT be able to read shared folder app"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 13: Operator permissions
|
||||
// ============================================
|
||||
// Note: Operators can execute but have limited management permissions
|
||||
// This is tested separately if needed
|
||||
|
||||
// ============================================
|
||||
// TEST 14: Folder owner permissions
|
||||
// ============================================
|
||||
|
||||
// Alice is an owner of the 'alice_owned' folder
|
||||
assert!(
|
||||
can_read(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/alice_owned/owner_script")
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to read scripts in folder she owns"
|
||||
);
|
||||
|
||||
// Alice should be able to write to her owned folder
|
||||
let create_owned_script = json!({
|
||||
"path": "f/alice_owned/new_owner_script",
|
||||
"content": "export function main() { return 'test'; }",
|
||||
"language": "deno",
|
||||
"summary": "Test script",
|
||||
"description": "",
|
||||
"schema": {}
|
||||
});
|
||||
assert!(
|
||||
can_write(
|
||||
&alice_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/create"),
|
||||
create_owned_script
|
||||
)
|
||||
.await,
|
||||
"Alice should be able to create scripts in folder she owns"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST 15: Cross-user script with extra_perms
|
||||
// ============================================
|
||||
|
||||
// Verify that giving someone read-only permission doesn't give write
|
||||
// Bob has read-only permission on alice's extra_shared_script
|
||||
assert!(
|
||||
can_read(
|
||||
&bob_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/u/alice/extra_shared_script")
|
||||
)
|
||||
.await,
|
||||
"Bob should be able to read Alice's script shared with him"
|
||||
);
|
||||
|
||||
// But Bob should not be able to archive/delete it
|
||||
let archive_resp = bob_client
|
||||
.post(&format!(
|
||||
"{base_url}/w/test-workspace/scripts/archive/p/u/alice/extra_shared_script"
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!archive_resp.status().is_success(),
|
||||
"Bob should NOT be able to archive Alice's script (read-only permission)"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Additional test for verifying group permission inheritance
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
async fn test_group_permission_inheritance(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let base_url = format!("http://localhost:{}/api", port);
|
||||
|
||||
let charlie_client = create_client_for_user(port, "CHARLIE_TOKEN_01").await;
|
||||
|
||||
// Charlie is in 'developers' group
|
||||
// 'developers' group has read access to 'team' folder
|
||||
|
||||
// Test 1: Charlie can read from team folder via group
|
||||
// Scripts use /scripts/get/p/{path}
|
||||
assert!(
|
||||
can_read(
|
||||
&charlie_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/get/p/f/team/team_script")
|
||||
)
|
||||
.await,
|
||||
"Charlie should read team folder via group"
|
||||
);
|
||||
|
||||
// Test 2: Add Charlie to 'editors' group (which has write to 'editable')
|
||||
// This should give him write access
|
||||
sqlx::query!(
|
||||
"INSERT INTO usr_to_group (workspace_id, group_, usr) VALUES ('test-workspace', 'editors', 'charlie')"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// Note: The AUTH_CACHE in windmill_api::auth caches authentication results
|
||||
// for (workspace_id, token) tuples. Since we can't easily clear it from tests,
|
||||
// we use a different token or wait for cache expiry. For this test, we create
|
||||
// a new token for Charlie.
|
||||
sqlx::query!(
|
||||
"INSERT INTO token (token, email, label, super_admin, owner, workspace_id)
|
||||
VALUES ('CHARLIE_TOKEN_NEW', 'charlie@windmill.dev', 'Charlie new token', false, 'u/charlie', 'test-workspace')"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// Use the new token to get fresh permissions
|
||||
let charlie_client_fresh = create_client_for_user(port, "CHARLIE_TOKEN_NEW").await;
|
||||
|
||||
let create_script = json!({
|
||||
"path": "f/editable/charlie_editor_script",
|
||||
"content": "export function main() { return 'from charlie'; }",
|
||||
"language": "deno",
|
||||
"summary": "Charlie's editor script",
|
||||
"description": "",
|
||||
"schema": {}
|
||||
});
|
||||
|
||||
assert!(
|
||||
can_write(
|
||||
&charlie_client_fresh,
|
||||
&format!("{base_url}/w/test-workspace/scripts/create"),
|
||||
create_script
|
||||
)
|
||||
.await,
|
||||
"Charlie should be able to write to editable folder after joining editors group"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that permissions work correctly for all item types
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
async fn test_all_item_types_permissions(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let base_url = format!("http://localhost:{}/api", port);
|
||||
|
||||
let alice_client = create_client_for_user(port, "ALICE_TOKEN_TEST").await;
|
||||
let bob_client = create_client_for_user(port, "BOB_TOKEN_TEST12").await;
|
||||
|
||||
// Test Scripts - uses /scripts/get/p/{path}
|
||||
assert!(can_read(&alice_client, &format!("{base_url}/w/test-workspace/scripts/get/p/u/alice/my_script")).await);
|
||||
assert!(!can_read(&bob_client, &format!("{base_url}/w/test-workspace/scripts/get/p/u/alice/my_script")).await);
|
||||
|
||||
// Test Flows - uses /flows/get/{path} (no /p/)
|
||||
assert!(can_read(&alice_client, &format!("{base_url}/w/test-workspace/flows/get/u/alice/my_flow")).await);
|
||||
assert!(!can_read(&bob_client, &format!("{base_url}/w/test-workspace/flows/get/u/alice/my_flow")).await);
|
||||
|
||||
// Test Resources - uses /resources/get/{path} (no /p/)
|
||||
assert!(can_read(&alice_client, &format!("{base_url}/w/test-workspace/resources/get/u/alice/my_resource")).await);
|
||||
assert!(!can_read(&bob_client, &format!("{base_url}/w/test-workspace/resources/get/u/alice/my_resource")).await);
|
||||
|
||||
// Test Variables - uses /variables/get/{path} (no /p/)
|
||||
assert!(can_read(&alice_client, &format!("{base_url}/w/test-workspace/variables/get/u/alice/my_variable")).await);
|
||||
assert!(!can_read(&bob_client, &format!("{base_url}/w/test-workspace/variables/get/u/alice/my_variable")).await);
|
||||
|
||||
// Test Schedules - uses /schedules/get/{path} (no /p/)
|
||||
assert!(can_read(&alice_client, &format!("{base_url}/w/test-workspace/schedules/get/u/alice/my_schedule")).await);
|
||||
assert!(!can_read(&bob_client, &format!("{base_url}/w/test-workspace/schedules/get/u/alice/my_schedule")).await);
|
||||
|
||||
// Test Apps - uses /apps/get/p/{path}
|
||||
assert!(can_read(&alice_client, &format!("{base_url}/w/test-workspace/apps/get/p/u/alice/my_app")).await);
|
||||
assert!(!can_read(&bob_client, &format!("{base_url}/w/test-workspace/apps/get/p/u/alice/my_app")).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that operators cannot create or update scripts, flows, and apps
|
||||
/// Operators have limited permissions - they can execute but cannot manage resources
|
||||
#[ignore]
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base", "permissions_test"))]
|
||||
async fn test_operator_cannot_create_update(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let base_url = format!("http://localhost:{}/api", port);
|
||||
|
||||
// Create operator client
|
||||
let operator_client = create_client_for_user(port, "OPERATOR_TOKEN_1").await;
|
||||
|
||||
// ============================================
|
||||
// TEST: Operator cannot create scripts
|
||||
// ============================================
|
||||
let create_script = json!({
|
||||
"path": "u/operator/test_script",
|
||||
"content": "export function main() { return 'test'; }",
|
||||
"language": "deno",
|
||||
"summary": "Test script",
|
||||
"description": "",
|
||||
"schema": {}
|
||||
});
|
||||
assert!(
|
||||
!can_write(
|
||||
&operator_client,
|
||||
&format!("{base_url}/w/test-workspace/scripts/create"),
|
||||
create_script
|
||||
)
|
||||
.await,
|
||||
"Operator should NOT be able to create scripts"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST: Operator cannot create flows
|
||||
// ============================================
|
||||
let create_flow = json!({
|
||||
"path": "u/operator/test_flow",
|
||||
"summary": "Test flow",
|
||||
"description": "",
|
||||
"value": {"modules": []},
|
||||
"schema": {}
|
||||
});
|
||||
assert!(
|
||||
!can_write(
|
||||
&operator_client,
|
||||
&format!("{base_url}/w/test-workspace/flows/create"),
|
||||
create_flow
|
||||
)
|
||||
.await,
|
||||
"Operator should NOT be able to create flows"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST: Operator cannot create apps
|
||||
// ============================================
|
||||
let create_app = json!({
|
||||
"path": "u/operator/test_app",
|
||||
"summary": "Test app",
|
||||
"value": {"grid": []},
|
||||
"policy": {
|
||||
"on_behalf_of": "u/operator",
|
||||
"on_behalf_of_email": "operator@windmill.dev",
|
||||
"execution_mode": "viewer"
|
||||
}
|
||||
});
|
||||
assert!(
|
||||
!can_write(
|
||||
&operator_client,
|
||||
&format!("{base_url}/w/test-workspace/apps/create"),
|
||||
create_app
|
||||
)
|
||||
.await,
|
||||
"Operator should NOT be able to create apps"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST: Operator cannot archive scripts
|
||||
// ============================================
|
||||
|
||||
// Insert a script in operator's namespace using direct DB access
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms)
|
||||
VALUES ('test-workspace', 3001, 'u/operator/existing_script', 'export function main() { return "original"; }', 'deno', 'script', 'admin', '{}', 'Existing script', '', '', '{}')"#
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// Operator tries to archive this script
|
||||
let archive_resp = operator_client
|
||||
.post(&format!(
|
||||
"{base_url}/w/test-workspace/scripts/archive/p/u/operator/existing_script"
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!archive_resp.status().is_success(),
|
||||
"Operator should NOT be able to archive scripts"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST: Operator cannot update flows
|
||||
// ============================================
|
||||
|
||||
// Insert a flow in operator's namespace
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, extra_perms)
|
||||
VALUES ('test-workspace', 'u/operator/existing_flow', 'Existing flow', '', '{"modules": []}', 'admin', NOW(), '{}', '{}')"#
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let update_flow = json!({
|
||||
"path": "u/operator/existing_flow",
|
||||
"summary": "Updated flow",
|
||||
"description": "Updated",
|
||||
"value": {"modules": []},
|
||||
"schema": {}
|
||||
});
|
||||
|
||||
let update_flow_resp = operator_client
|
||||
.post(&format!(
|
||||
"{base_url}/w/test-workspace/flows/update/u/operator/existing_flow"
|
||||
))
|
||||
.json(&update_flow)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!update_flow_resp.status().is_success(),
|
||||
"Operator should NOT be able to update flows"
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// TEST: Operator cannot update apps
|
||||
// ============================================
|
||||
|
||||
// Insert an app in operator's namespace
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms)
|
||||
VALUES (3001, 'test-workspace', 'u/operator/existing_app', 'Existing app', '{}',
|
||||
'{"on_behalf_of": "u/admin", "on_behalf_of_email": "admin@windmill.dev", "execution_mode": "viewer"}', '{}')"#
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// Create an app version
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO app_version (id, app_id, value, created_by, created_at)
|
||||
VALUES (3001, 3001, '{"grid": []}', 'admin', NOW())"#
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// Update app versions
|
||||
sqlx::query!(
|
||||
"UPDATE app SET versions = ARRAY[3001::bigint] WHERE id = 3001"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let update_app = json!({
|
||||
"path": "u/operator/existing_app",
|
||||
"summary": "Updated app",
|
||||
"value": {"grid": []},
|
||||
"policy": {
|
||||
"on_behalf_of": "u/operator",
|
||||
"on_behalf_of_email": "operator@windmill.dev",
|
||||
"execution_mode": "viewer"
|
||||
}
|
||||
});
|
||||
|
||||
let update_app_resp = operator_client
|
||||
.post(&format!(
|
||||
"{base_url}/w/test-workspace/apps/update/u/operator/existing_app"
|
||||
))
|
||||
.json(&update_app)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!update_app_resp.status().is_success(),
|
||||
"Operator should NOT be able to update apps"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1905,6 +1905,11 @@ async fn archive_script_by_path(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<()> {
|
||||
if authed.is_operator {
|
||||
return Err(Error::NotAuthorized(
|
||||
"Operators cannot archive scripts for security reasons".to_string(),
|
||||
));
|
||||
}
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("scripts:write:{}", path))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
@@ -1974,6 +1979,11 @@ async fn archive_script_by_hash(
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<Script<ScriptRunnableSettingsInline>> {
|
||||
if authed.is_operator {
|
||||
return Err(Error::NotAuthorized(
|
||||
"Operators cannot archive scripts for security reasons".to_string(),
|
||||
));
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let script = sqlx::query_as::<_, Script<ScriptRunnableSettingsHandle>>(
|
||||
|
||||
Reference in New Issue
Block a user