From 0bd756839c0261f255111d62088bdaaecb838085 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 25 Mar 2026 18:10:20 +0100 Subject: [PATCH] feat: SCIM user deprovisioning (active:false) + instance-level user disable (#8484) * [ee] feat: handle active:false in SCIM user PATCH/PUT for deprovisioning Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref for SCIM active:false deprovision fix Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref Co-Authored-By: Claude Opus 4.6 (1M context) * nit sqlx * [ee] feat: add password.disabled column for SCIM user deactivation Co-Authored-By: Claude Opus 4.6 (1M context) * [ee] feat: enforce password.disabled in auth checks Co-Authored-By: Claude Opus 4.6 (1M context) * [ee] refactor: use scim_deactivated_user table instead of password.disabled Co-Authored-By: Claude Opus 4.6 (1M context) * [ee] fix: apply SCIM filters to deactivated users, add name column Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref Co-Authored-By: Claude Opus 4.6 (1M context) * chore: add down migration for scim_deactivated_user Co-Authored-By: Claude Opus 4.6 (1M context) * fix: rename migration to avoid timestamp conflict, update sqlx cache Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref Co-Authored-By: Claude Opus 4.6 (1M context) * [ee] refactor: use password.disabled for SCIM deactivation, block login for disabled users Co-Authored-By: Claude Opus 4.6 (1M context) * [ee] feat: show disabled toggle in superadmin user list, add disabled field to API Co-Authored-By: Claude Opus 4.6 (1M context) * feat: add confirmation modal when disabling instance user Co-Authored-By: Claude Opus 4.6 (1M context) * fix: improve disable user confirmation text Co-Authored-By: Claude Opus 4.6 (1M context) * fix: revert toggle state when disable confirmation is cancelled Co-Authored-By: Claude Opus 4.6 (1M context) * fix: properly revert toggle on disable cancel using reset key Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: move disable/enable to dropdown menu, add disabled badge on email Co-Authored-By: Claude Opus 4.6 (1M context) * fix: rename 'Show active users only' to 'Recently active only' to avoid confusion with disabled state Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref Co-Authored-By: Claude Opus 4.6 (1M context) * chore: remove accidentally committed gen files Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use .catch() for enable user error handling in dropdown action Co-Authored-By: Claude Opus 4.6 (1M context) * fix: delete tokens on user removal, improve confirmation modal texts Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update sqlx cache for non-enterprise code paths Co-Authored-By: Claude Opus 4.6 (1M context) * fix: restore sqlx cache files deleted by incorrect prepare run Co-Authored-By: Claude Opus 4.6 (1M context) * fix: add missing sqlx cache for non-enterprise git sync query Co-Authored-By: Claude Opus 4.6 (1M context) * chore: update ee-repo-ref to a1274aa11a83f608eacc32c0d449ca3527d98c15 This commit updates the EE repository reference after PR #473 was merged in windmill-ee-private. Previous ee-repo-ref: 30f8c53b101b9e25107e793cdc038b0e07061739 New ee-repo-ref: a1274aa11a83f608eacc32c0d449ca3527d98c15 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: windmill-internal-app[bot] --- ...1410d32a8d672cfa4929e9e3763c51daa1bc.json} | 10 +- ...024d9826a328bf0416c22daf06fff5ced08f6.json | 14 +++ ...ef901cd3c417b9f3af03f35009213143bd443.json | 28 ++++++ ...153c43903f929ae5d62fbba12610f89c36d55.json | 2 +- ...7d18161eb6f5ec1ba9f1b55feffe8b6518c67.json | 15 +++ ...2d14755474cba82b3b388a47585a8bb325b1a.json | 17 ---- ...5b31f0efc6d8ef73f691009c73f833dcee10.json} | 10 +- ...bdc0e1934d67d3f2b14047d434b77d370af21.json | 22 +++++ ...ac9019767074158e0c027988e5b0d51a3656.json} | 4 +- ...504b7b5cb7a39538ab9abeb44f781c711493.json} | 10 +- ...78447d0aa3d143e94e49924ff7ac8b7abf924.json | 22 +++++ backend/ee-repo-ref.txt | 2 +- ...60324000000_scim_deactivated_user.down.sql | 1 + ...0260324000000_scim_deactivated_user.up.sql | 1 + backend/windmill-api-users/src/users.rs | 32 ++++++- backend/windmill-api/openapi.yaml | 5 + .../components/SuperadminSettingsInner.svelte | 96 ++++++++++++++++--- 17 files changed, 248 insertions(+), 43 deletions(-) rename backend/.sqlx/{query-05027983ffdb11824190543754d0be922e1463d2046753cf80377369a90013ab.json => query-115a9cb44d0a41952c08dc36e0331410d32a8d672cfa4929e9e3763c51daa1bc.json} (83%) create mode 100644 backend/.sqlx/query-192ddae8c3c82a8f099a4944483024d9826a328bf0416c22daf06fff5ced08f6.json create mode 100644 backend/.sqlx/query-23b9c862d050b00aaa332527b62ef901cd3c417b9f3af03f35009213143bd443.json create mode 100644 backend/.sqlx/query-8bd266705fc8272f3d8941922ad7d18161eb6f5ec1ba9f1b55feffe8b6518c67.json delete mode 100644 backend/.sqlx/query-9f07510019ebe6f0c5fa17bf31c2d14755474cba82b3b388a47585a8bb325b1a.json rename backend/.sqlx/{query-60118de85463098220b1c74f667b6fedb0f3f0040844c3774145e8f1f4c023ce.json => query-a5fd115e7be5129d623543bbfa7b5b31f0efc6d8ef73f691009c73f833dcee10.json} (82%) create mode 100644 backend/.sqlx/query-ccc49a2a6e11f874825365de758bdc0e1934d67d3f2b14047d434b77d370af21.json rename backend/.sqlx/{query-638d3c2ba1198dce5b5b0e47df59a92ff8011e19fbefcc3960d6f0fe167e55b6.json => query-daa1a6bf3d4a1001da88301932a7ac9019767074158e0c027988e5b0d51a3656.json} (59%) rename backend/.sqlx/{query-65c59e224e460351c2f88261f8b1b1e7ce2bb160270b59c0f359b7952453b2b9.json => query-f0c9c54740cc1c0c2a6fa4e79d4d504b7b5cb7a39538ab9abeb44f781c711493.json} (85%) create mode 100644 backend/.sqlx/query-fc6c6310ae8ac5eb351d7e2af1678447d0aa3d143e94e49924ff7ac8b7abf924.json create mode 100644 backend/migrations/20260324000000_scim_deactivated_user.down.sql create mode 100644 backend/migrations/20260324000000_scim_deactivated_user.up.sql diff --git a/backend/.sqlx/query-05027983ffdb11824190543754d0be922e1463d2046753cf80377369a90013ab.json b/backend/.sqlx/query-115a9cb44d0a41952c08dc36e0331410d32a8d672cfa4929e9e3763c51daa1bc.json similarity index 83% rename from backend/.sqlx/query-05027983ffdb11824190543754d0be922e1463d2046753cf80377369a90013ab.json rename to backend/.sqlx/query-115a9cb44d0a41952c08dc36e0331410d32a8d672cfa4929e9e3763c51daa1bc.json index 53a3863587..d2c85b0e53 100644 --- a/backend/.sqlx/query-05027983ffdb11824190543754d0be922e1463d2046753cf80377369a90013ab.json +++ b/backend/.sqlx/query-115a9cb44d0a41952c08dc36e0331410d32a8d672cfa4929e9e3763c51daa1bc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT $1 OFFSET $2", + "query": "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT $1 OFFSET $2", "describe": { "columns": [ { @@ -57,6 +57,11 @@ "ordinal": 10, "name": "role_source", "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "disabled", + "type_info": "Bool" } ], "parameters": { @@ -76,8 +81,9 @@ true, null, false, + false, false ] }, - "hash": "05027983ffdb11824190543754d0be922e1463d2046753cf80377369a90013ab" + "hash": "115a9cb44d0a41952c08dc36e0331410d32a8d672cfa4929e9e3763c51daa1bc" } diff --git a/backend/.sqlx/query-192ddae8c3c82a8f099a4944483024d9826a328bf0416c22daf06fff5ced08f6.json b/backend/.sqlx/query-192ddae8c3c82a8f099a4944483024d9826a328bf0416c22daf06fff5ced08f6.json new file mode 100644 index 0000000000..dc7c41cfd3 --- /dev/null +++ b/backend/.sqlx/query-192ddae8c3c82a8f099a4944483024d9826a328bf0416c22daf06fff5ced08f6.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM token WHERE email = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "192ddae8c3c82a8f099a4944483024d9826a328bf0416c22daf06fff5ced08f6" +} diff --git a/backend/.sqlx/query-23b9c862d050b00aaa332527b62ef901cd3c417b9f3af03f35009213143bd443.json b/backend/.sqlx/query-23b9c862d050b00aaa332527b62ef901cd3c417b9f3af03f35009213143bd443.json new file mode 100644 index 0000000000..d6946e80d0 --- /dev/null +++ b/backend/.sqlx/query-23b9c862d050b00aaa332527b62ef901cd3c417b9f3af03f35009213143bd443.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT email, disabled FROM password WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "disabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "23b9c862d050b00aaa332527b62ef901cd3c417b9f3af03f35009213143bd443" +} diff --git a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json index 713ccb9dd3..36ddb8ab9f 100644 --- a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json +++ b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - null + true ] }, "hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55" diff --git a/backend/.sqlx/query-8bd266705fc8272f3d8941922ad7d18161eb6f5ec1ba9f1b55feffe8b6518c67.json b/backend/.sqlx/query-8bd266705fc8272f3d8941922ad7d18161eb6f5ec1ba9f1b55feffe8b6518c67.json new file mode 100644 index 0000000000..fc86915946 --- /dev/null +++ b/backend/.sqlx/query-8bd266705fc8272f3d8941922ad7d18161eb6f5ec1ba9f1b55feffe8b6518c67.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE password SET disabled = $1 WHERE email = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "8bd266705fc8272f3d8941922ad7d18161eb6f5ec1ba9f1b55feffe8b6518c67" +} diff --git a/backend/.sqlx/query-9f07510019ebe6f0c5fa17bf31c2d14755474cba82b3b388a47585a8bb325b1a.json b/backend/.sqlx/query-9f07510019ebe6f0c5fa17bf31c2d14755474cba82b3b388a47585a8bb325b1a.json deleted file mode 100644 index 25a32e5338..0000000000 --- a/backend/.sqlx/query-9f07510019ebe6f0c5fa17bf31c2d14755474cba82b3b388a47585a8bb325b1a.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET deployment_msg = EXCLUDED.deployment_msg", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Int8", - "Text" - ] - }, - "nullable": [] - }, - "hash": "9f07510019ebe6f0c5fa17bf31c2d14755474cba82b3b388a47585a8bb325b1a" -} diff --git a/backend/.sqlx/query-60118de85463098220b1c74f667b6fedb0f3f0040844c3774145e8f1f4c023ce.json b/backend/.sqlx/query-a5fd115e7be5129d623543bbfa7b5b31f0efc6d8ef73f691009c73f833dcee10.json similarity index 82% rename from backend/.sqlx/query-60118de85463098220b1c74f667b6fedb0f3f0040844c3774145e8f1f4c023ce.json rename to backend/.sqlx/query-a5fd115e7be5129d623543bbfa7b5b31f0efc6d8ef73f691009c73f833dcee10.json index 6d2382f494..dab14d9f1d 100644 --- a/backend/.sqlx/query-60118de85463098220b1c74f667b6fedb0f3f0040844c3774145e8f1f4c023ce.json +++ b/backend/.sqlx/query-a5fd115e7be5129d623543bbfa7b5b31f0efc6d8ef73f691009c73f833dcee10.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2", + "query": "WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source, disabled\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2", "describe": { "columns": [ { @@ -57,6 +57,11 @@ "ordinal": 10, "name": "role_source", "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "disabled", + "type_info": "Bool" } ], "parameters": { @@ -76,8 +81,9 @@ true, true, false, + false, false ] }, - "hash": "60118de85463098220b1c74f667b6fedb0f3f0040844c3774145e8f1f4c023ce" + "hash": "a5fd115e7be5129d623543bbfa7b5b31f0efc6d8ef73f691009c73f833dcee10" } diff --git a/backend/.sqlx/query-ccc49a2a6e11f874825365de758bdc0e1934d67d3f2b14047d434b77d370af21.json b/backend/.sqlx/query-ccc49a2a6e11f874825365de758bdc0e1934d67d3f2b14047d434b77d370af21.json new file mode 100644 index 0000000000..a9348dfa8b --- /dev/null +++ b/backend/.sqlx/query-ccc49a2a6e11f874825365de758bdc0e1934d67d3f2b14047d434b77d370af21.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT super_admin FROM password WHERE email = $1 AND disabled = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "super_admin", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ccc49a2a6e11f874825365de758bdc0e1934d67d3f2b14047d434b77d370af21" +} diff --git a/backend/.sqlx/query-638d3c2ba1198dce5b5b0e47df59a92ff8011e19fbefcc3960d6f0fe167e55b6.json b/backend/.sqlx/query-daa1a6bf3d4a1001da88301932a7ac9019767074158e0c027988e5b0d51a3656.json similarity index 59% rename from backend/.sqlx/query-638d3c2ba1198dce5b5b0e47df59a92ff8011e19fbefcc3960d6f0fe167e55b6.json rename to backend/.sqlx/query-daa1a6bf3d4a1001da88301932a7ac9019767074158e0c027988e5b0d51a3656.json index d37bc73dad..5b18e66ed4 100644 --- a/backend/.sqlx/query-638d3c2ba1198dce5b5b0e47df59a92ff8011e19fbefcc3960d6f0fe167e55b6.json +++ b/backend/.sqlx/query-daa1a6bf3d4a1001da88301932a7ac9019767074158e0c027988e5b0d51a3656.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO password (email, login_type, verified, username, name) VALUES ($1, 'saml', true, $2, $3) ON CONFLICT DO NOTHING", + "query": "INSERT INTO password (email, login_type, verified, username, name) VALUES ($1, 'saml', true, $2, $3) ON CONFLICT (email) DO UPDATE SET disabled = false", "describe": { "columns": [], "parameters": { @@ -12,5 +12,5 @@ }, "nullable": [] }, - "hash": "638d3c2ba1198dce5b5b0e47df59a92ff8011e19fbefcc3960d6f0fe167e55b6" + "hash": "daa1a6bf3d4a1001da88301932a7ac9019767074158e0c027988e5b0d51a3656" } diff --git a/backend/.sqlx/query-65c59e224e460351c2f88261f8b1b1e7ce2bb160270b59c0f359b7952453b2b9.json b/backend/.sqlx/query-f0c9c54740cc1c0c2a6fa4e79d4d504b7b5cb7a39538ab9abeb44f781c711493.json similarity index 85% rename from backend/.sqlx/query-65c59e224e460351c2f88261f8b1b1e7ce2bb160270b59c0f359b7952453b2b9.json rename to backend/.sqlx/query-f0c9c54740cc1c0c2a6fa4e79d4d504b7b5cb7a39538ab9abeb44f781c711493.json index 2ccf7bfdc8..c1d113ee27 100644 --- a/backend/.sqlx/query-65c59e224e460351c2f88261f8b1b1e7ce2bb160270b59c0f359b7952453b2b9.json +++ b/backend/.sqlx/query-f0c9c54740cc1c0c2a6fa4e79d4d504b7b5cb7a39538ab9abeb44f781c711493.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password WHERE email = $1", + "query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password WHERE email = $1", "describe": { "columns": [ { @@ -57,6 +57,11 @@ "ordinal": 10, "name": "role_source", "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "disabled", + "type_info": "Bool" } ], "parameters": { @@ -75,8 +80,9 @@ true, null, false, + false, false ] }, - "hash": "65c59e224e460351c2f88261f8b1b1e7ce2bb160270b59c0f359b7952453b2b9" + "hash": "f0c9c54740cc1c0c2a6fa4e79d4d504b7b5cb7a39538ab9abeb44f781c711493" } diff --git a/backend/.sqlx/query-fc6c6310ae8ac5eb351d7e2af1678447d0aa3d143e94e49924ff7ac8b7abf924.json b/backend/.sqlx/query-fc6c6310ae8ac5eb351d7e2af1678447d0aa3d143e94e49924ff7ac8b7abf924.json new file mode 100644 index 0000000000..c85c557a90 --- /dev/null +++ b/backend/.sqlx/query-fc6c6310ae8ac5eb351d7e2af1678447d0aa3d143e94e49924ff7ac8b7abf924.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT disabled FROM password WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "disabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fc6c6310ae8ac5eb351d7e2af1678447d0aa3d143e94e49924ff7ac8b7abf924" +} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 263ec1de9a..7a74b353a3 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -182943e5ad9bf2a905ccdf07d4e346437fb329a9 +a1274aa11a83f608eacc32c0d449ca3527d98c15 diff --git a/backend/migrations/20260324000000_scim_deactivated_user.down.sql b/backend/migrations/20260324000000_scim_deactivated_user.down.sql new file mode 100644 index 0000000000..1af6e77c62 --- /dev/null +++ b/backend/migrations/20260324000000_scim_deactivated_user.down.sql @@ -0,0 +1 @@ +ALTER TABLE password DROP COLUMN IF EXISTS disabled; diff --git a/backend/migrations/20260324000000_scim_deactivated_user.up.sql b/backend/migrations/20260324000000_scim_deactivated_user.up.sql new file mode 100644 index 0000000000..7410481bdd --- /dev/null +++ b/backend/migrations/20260324000000_scim_deactivated_user.up.sql @@ -0,0 +1 @@ +ALTER TABLE password ADD COLUMN disabled BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/windmill-api-users/src/users.rs b/backend/windmill-api-users/src/users.rs index 75db0e2d24..8790a9c541 100644 --- a/backend/windmill-api-users/src/users.rs +++ b/backend/windmill-api-users/src/users.rs @@ -157,6 +157,7 @@ pub struct GlobalUserInfo { operator_only: Option, first_time_user: bool, role_source: String, + disabled: bool, } #[derive(Serialize, Debug)] @@ -213,6 +214,7 @@ pub struct EditUser { pub is_super_admin: Option, pub is_devops: Option, pub name: Option, + pub disabled: Option, } #[derive(Deserialize)] @@ -396,7 +398,7 @@ async fn list_users_as_super_admin( GlobalUserInfo, "WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')), authors as (SELECT distinct email FROM usr WHERE usr.operator IS false) - SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source + SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source, disabled FROM password WHERE email IN (SELECT email FROM active_users) ORDER BY super_admin DESC, devops DESC @@ -409,7 +411,7 @@ async fn list_users_as_super_admin( } else { sqlx::query_as!( GlobalUserInfo, - "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT \ + "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT \ $1 OFFSET $2", per_page as i32, offset as i32 @@ -657,7 +659,7 @@ async fn global_whoami( ) -> JsonResult { let user = sqlx::query_as!( GlobalUserInfo, - "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password WHERE \ + "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password WHERE \ email = $1", email ) @@ -680,6 +682,7 @@ async fn global_whoami( operator_only: None, first_time_user: false, role_source: "manual".to_string(), + disabled: false, })) } else { Err(user.unwrap_err()) @@ -1439,6 +1442,22 @@ async fn update_user( .await?; } + if let Some(d) = eu.disabled { + sqlx::query_scalar!( + "UPDATE password SET disabled = $1 WHERE email = $2", + d, + &email_to_update + ) + .execute(&mut *tx) + .await?; + if d { + // Delete all tokens for immediate session revocation + sqlx::query!("DELETE FROM token WHERE email = $1", &email_to_update) + .execute(&mut *tx) + .await?; + } + } + audit_log( &mut *tx, &authed, @@ -1461,6 +1480,9 @@ async fn delete_user( require_super_admin(&db, &authed.email).await?; let mut tx = db.begin().await?; + sqlx::query!("DELETE FROM token WHERE email = $1", &email_to_delete) + .execute(&mut *tx) + .await?; sqlx::query!("DELETE FROM password WHERE email = $1", &email_to_delete) .execute(&mut *tx) .await?; @@ -1719,7 +1741,7 @@ async fn login( }; let email_w_h: Option<(String, String, bool)> = sqlx::query_as( "SELECT email, password_hash, super_admin FROM password WHERE email = $1 AND login_type = \ - 'password'", + 'password' AND disabled = false", ) .bind(&email) .fetch_optional(&mut *tx) @@ -1808,7 +1830,7 @@ async fn refresh_token( } let super_admin = sqlx::query_scalar!( - "SELECT super_admin FROM password WHERE email = $1", + "SELECT super_admin FROM password WHERE email = $1 AND disabled = false", &authed.email ) .fetch_optional(&mut *tx) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index cb86c2a8b9..7b90c64d12 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -588,6 +588,8 @@ paths: type: boolean name: type: string + disabled: + type: boolean responses: "200": description: user updated @@ -23470,6 +23472,8 @@ components: role_source: type: string enum: ["manual", "instance_group"] + disabled: + type: boolean required: - email @@ -23478,6 +23482,7 @@ components: - verified - first_time_user - role_source + - disabled Flow: allOf: diff --git a/frontend/src/lib/components/SuperadminSettingsInner.svelte b/frontend/src/lib/components/SuperadminSettingsInner.svelte index 4a1d376f8c..683db57724 100644 --- a/frontend/src/lib/components/SuperadminSettingsInner.svelte +++ b/frontend/src/lib/components/SuperadminSettingsInner.svelte @@ -17,7 +17,7 @@ import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' import { userStore, workspaceStore } from '$lib/stores' - import { ExternalLink, Pencil, UserMinus, UserPlus } from 'lucide-svelte' + import { Ban, CheckCircle2, ExternalLink, Pencil, UserMinus, UserPlus } from 'lucide-svelte' import DropdownV2 from './DropdownV2.svelte' import Popover from './meltComponents/Popover.svelte' import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte' @@ -67,6 +67,8 @@ let filteredUsers: GlobalUserInfo[] = $state([]) let deleteConfirmedCallback: (() => void) | undefined = $state(undefined) let deleteUserEmail: string = $state('') + let disableConfirmedCallback: (() => void) | undefined = $state(undefined) + let disableUserEmail: string = $state('') let editWrappers: Record = $state({}) let activeOnly = $state(false) @@ -293,9 +295,9 @@ /> @@ -347,13 +349,25 @@ {#if filteredUsers && users} - {#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only, role_source }, i (email)} - - {email} + {#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only, role_source, disabled }, i (email)} + + +
+ {email} + {#if disabled} + Disabled + {/if} +
+
{#if automateUsernameCreation} {#if username} @@ -514,6 +528,39 @@ if (btn instanceof HTMLElement) btn.click() } }, + { + displayName: disabled ? 'Enable' : 'Disable', + icon: disabled ? CheckCircle2 : Ban, + action: () => { + if (!disabled) { + disableUserEmail = email + disableConfirmedCallback = async () => { + try { + await UserService.globalUserUpdate({ + email, + requestBody: { disabled: true } + }) + sendUserToast('User disabled') + listUsers(activeOnly) + } catch (e) { + sendUserToast('Failed to disable user', true) + } + } + } else { + UserService.globalUserUpdate({ + email, + requestBody: { disabled: false } + }) + .then(() => { + sendUserToast('User enabled') + listUsers(activeOnly) + }) + .catch(() => { + sendUserToast('Failed to enable user', true) + }) + } + } + }, { displayName: 'Remove', icon: UserMinus, @@ -578,6 +625,33 @@ }} >
- Are you sure you want to remove {deleteUserEmail}? + Are you sure you want to remove {deleteUserEmail}? They will be removed from all + workspaces and instance groups, and all their sessions and tokens will be revoked. This action + is irreversible. Their workspace content (scripts, flows, apps) will not be deleted. +
+ + { + disableConfirmedCallback = undefined + listUsers(activeOnly) + }} + on:confirmed={() => { + if (disableConfirmedCallback) { + disableConfirmedCallback() + } + disableConfirmedCallback = undefined + }} +> +
+ Are you sure you want to disable {disableUserEmail}? All their active sessions and + tokens will be revoked immediately. They will be unable to log in until re-enabled. Their + workspace memberships and content will be preserved.