From 3485b07b2548b7ea8fbd2b6b31b91e2d36d072ef Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 13 Dec 2022 19:40:14 +0100 Subject: [PATCH] feat: add operator mode --- .../20221213112420_add_operator.down.sql | 3 + .../20221213112420_add_operator.up.sql | 4 + backend/sqlx-data.json | 456 +++++++++++------- backend/windmill-api/banned_domains.txt | 7 +- backend/windmill-api/openapi.yaml | 30 +- backend/windmill-api/src/jobs.rs | 8 +- backend/windmill-api/src/users.rs | 101 ++-- backend/windmill-api/src/workspaces.rs | 47 +- backend/windmill-worker/src/jobs.rs | 8 +- .../src/lib/components/FlowBuilder.svelte | 144 +++--- .../lib/components/InviteGlobalUser.svelte | 4 +- frontend/src/lib/components/InviteUser.svelte | 27 +- .../src/lib/components/ResourceEditor.svelte | 2 +- .../src/lib/components/ScriptBuilder.svelte | 420 ++++++++-------- .../lib/components/SuperadminSettings.svelte | 48 +- .../src/lib/components/VariableEditor.svelte | 4 +- .../components/apps/editor/AppEditor.svelte | 119 ++--- .../apps/editor/AppEditorHeader.svelte | 4 +- .../lib/components/common/table/AppRow.svelte | 28 +- .../components/common/table/FlowRow.svelte | 54 ++- .../components/common/table/ScriptRow.svelte | 55 ++- .../src/lib/components/home/ItemsList.svelte | 12 +- .../components/sidebar/SidebarContent.svelte | 12 +- .../lib/components/sidebar/UserMenu.svelte | 12 +- frontend/src/lib/stores.ts | 1 + .../src/routes/flows/get/[...path].svelte | 40 +- frontend/src/routes/index.svelte | 52 +- frontend/src/routes/run/[...run].svelte | 18 +- .../src/routes/scripts/get/[...hash].svelte | 30 +- .../src/routes/scripts/run/[...hash].svelte | 3 +- .../routes/user/create_workspace@user.svelte | 16 +- .../src/routes/user/workspaces@user.svelte | 2 + frontend/src/routes/workspace_settings.svelte | 214 +++++--- 33 files changed, 1144 insertions(+), 841 deletions(-) create mode 100644 backend/migrations/20221213112420_add_operator.down.sql create mode 100644 backend/migrations/20221213112420_add_operator.up.sql diff --git a/backend/migrations/20221213112420_add_operator.down.sql b/backend/migrations/20221213112420_add_operator.down.sql new file mode 100644 index 0000000000..5423277f84 --- /dev/null +++ b/backend/migrations/20221213112420_add_operator.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +ALTER TABLE workspace_invite DROP COLUMN operator; +ALTER TABLE workspace_settings DROP COLUMN auto_invite_operator; diff --git a/backend/migrations/20221213112420_add_operator.up.sql b/backend/migrations/20221213112420_add_operator.up.sql new file mode 100644 index 0000000000..dbc95ca8b0 --- /dev/null +++ b/backend/migrations/20221213112420_add_operator.up.sql @@ -0,0 +1,4 @@ +-- Add up migration script here +ALTER TABLE workspace_invite ADD COLUMN operator BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE workspace_settings ADD COLUMN auto_invite_operator BOOLEAN DEFAULT false; +ALTER TABLE completed_job ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz'; diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 9e6923e338..0ebb8f3c0d 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -1,5 +1,19 @@ { "db": "PostgreSQL", + "006f03e979abdf8055b1c598bc9806337216a6abf74db4eb64b0acb918a0de08": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Bool", + "Text", + "Text" + ] + } + }, + "query": "UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3" + }, "0355b53b1d45955ca56b2829372ce9c656d7f0ad7b8d0709161047f0d8cdc4f4": { "describe": { "columns": [ @@ -176,20 +190,6 @@ }, "query": "SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2" }, - "0a76ed47629cac693ba7f169a1229b62bd900bc007a63fbae3fa7374ba66df65": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Bool" - ] - } - }, - "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, $3)" - }, "0a9a191273c735c41d56ea46a39ffca075a0550eada87df7162c5037164ad6bf": { "describe": { "columns": [], @@ -204,20 +204,20 @@ }, "query": "\n INSERT INTO capture\n (workspace_id, path, created_by)\n VALUES ($1, $2, $3)\n ON CONFLICT (workspace_id, path)\n DO UPDATE SET created_at = now()\n " }, - "0ae9160591ae00117d20a616cfe07e38f0c32953c7e881e916c389255190b72d": { + "0c5faa1e3233ca15240f80c9ba805f839d9f1f7450d1ea1f2b8e9faf8533892d": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Varchar", - "Varchar", - "Varchar", + "Text", + "Text", + "Bool", "Bool" ] } }, - "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, $4)" + "query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3 AND operator = $4" }, "0d6412bc3ebb1d58bdd9cbcef774dacf9016fa402af5c1b4e339b9a3d7163d5e": { "describe": { @@ -404,6 +404,11 @@ "name": "auto_invite_domain", "ordinal": 5, "type_info": "Varchar" + }, + { + "name": "auto_invite_operator", + "ordinal": 6, + "type_info": "Bool" } ], "nullable": [ @@ -412,6 +417,7 @@ true, true, false, + true, true ], "parameters": { @@ -460,9 +466,15 @@ "name": "is_admin", "ordinal": 2, "type_info": "Bool" + }, + { + "name": "operator", + "ordinal": 3, + "type_info": "Bool" } ], "nullable": [ + false, false, false, false @@ -841,33 +853,6 @@ }, "query": "SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 " }, - "2bf44d998d7acd17ec6d98f81395f8bdac49f58880fbbb9350bf0142cd2efdc7": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Bool" - ] - } - }, - "query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3" - }, - "2c6c2358fc24d92db179306a0f18a83d71bf15d8dc76502c451634dff35715a6": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar" - ] - } - }, - "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, false)\n ON CONFLICT DO NOTHING" - }, "2e11e3ef361c41e6e055dd4805cb9d5e45eaa486e35fc5f01dae311189d6800f": { "describe": { "columns": [ @@ -933,6 +918,22 @@ }, "query": "UPDATE schedule SET enabled = false, error = $1 WHERE workspace_id = $2 AND path = $3" }, + "31a6e8a73fa568e28b8ca6e35a38b663e0b07d15a212b81883ad9c327d99a7c4": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Bool" + ] + } + }, + "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin, operator)\n VALUES ($1, $2, $3, $4, $5)" + }, "355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": { "describe": { "columns": [ @@ -1174,27 +1175,6 @@ }, "query": "UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3" }, - "4ad5fa2f08236507aad911a95697e84fc0c3a274ba0e928da28c4d146cf8f1a8": { - "describe": { - "columns": [ - { - "name": "is_admin", - "ordinal": 0, - "type_info": "Bool" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - } - }, - "query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2" - }, "4de3ba6930b91c639f9f3939dab7b3ab35aa792484e4ccb8084c336a2ec60f45": { "describe": { "columns": [], @@ -1467,6 +1447,11 @@ "name": "auto_invite_domain", "ordinal": 5, "type_info": "Varchar" + }, + { + "name": "auto_invite_operator", + "ordinal": 6, + "type_info": "Bool" } ], "nullable": [ @@ -1475,6 +1460,7 @@ true, true, false, + true, true ], "parameters": { @@ -1656,6 +1642,18 @@ }, "query": "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2" }, + "5c377fffc224a06f693c125f4c13b0a9ccfc217190ba6cf78246294bbc6c93bc": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL WHERE workspace_id = $1" + }, "5fee34ce48f01bb55707227548ea4d1e77ba75abfae412a902b1d8d6a1963725": { "describe": { "columns": [], @@ -2079,6 +2077,72 @@ }, "query": "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2" }, + "77c3915246e37ae158d0c600e2402b93ce4fb1179d034daf231e00797e214207": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Timestamptz", + "Timestamptz", + "Bool", + "Int8", + "Varchar", + "Jsonb", + "Jsonb", + "Text", + "Text", + "Text", + "Bool", + "Varchar", + "Text", + { + "Custom": { + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies" + ] + }, + "name": "job_kind" + } + }, + "Varchar", + "Varchar", + "Jsonb", + "Jsonb", + "Bool", + "Bool", + { + "Custom": { + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash" + ] + }, + "name": "script_lang" + } + }, + "Numeric", + "Varchar" + ] + } + }, + "query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)" + }, "7a511ce8dbbf761423b527672ab02156aba5594623dc269992b34398673ca387": { "describe": { "columns": [ @@ -2111,19 +2175,6 @@ }, "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ('demo', $1, false)\n ON CONFLICT DO NOTHING" }, - "7b0a6bf71751668eea56923f70e198c36386a82b8a8abbf4f55caeb0618478fe": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Text" - ] - } - }, - "query": "UPDATE workspace_settings SET auto_invite_domain = $1 WHERE workspace_id = $2" - }, "7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": { "describe": { "columns": [ @@ -2182,6 +2233,20 @@ }, "query": "SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc" }, + "7fcffd77d0957cff4dff7aa822f7bcab9ec10563738e47ab02657baa9b29179d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool" + ] + } + }, + "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, false, $3)\n ON CONFLICT DO NOTHING" + }, "808ff76fdb74482d5d3201417c8a2470e2867cb08b9a46d244653eb366d8ee5e": { "describe": { "columns": [], @@ -2705,70 +2770,26 @@ }, "query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1" }, - "90d477b39e9d2f439d378c21041af8bc7720cc21ae150a62ecaa6235142aea05": { + "924e1d5cbb682db0d6121dd911f38cf8948d0e59ca47428daa4f43fce686ba80": { "describe": { - "columns": [], - "nullable": [], + "columns": [ + { + "name": "is_admin", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + false + ], "parameters": { "Left": [ - "Varchar", - "Uuid", - "Uuid", - "Varchar", - "Timestamptz", - "Timestamptz", - "Bool", - "Int8", - "Varchar", - "Jsonb", - "Jsonb", "Text", - "Text", - "Text", - "Bool", - "Varchar", - "Text", - { - "Custom": { - "kind": { - "Enum": [ - "script", - "preview", - "flow", - "dependencies", - "flowpreview", - "script_hub", - "identity", - "flowdependencies" - ] - }, - "name": "job_kind" - } - }, - "Varchar", - "Varchar", - "Jsonb", - "Jsonb", - "Bool", - "Bool", - { - "Custom": { - "kind": { - "Enum": [ - "python3", - "deno", - "go", - "bash" - ] - }, - "name": "script_lang" - } - }, - "Numeric" + "Text" ] } }, - "query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)" + "query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2 AND disabled = false" }, "9251404312374679a4be2f9def508fcadce446405e91d95100c3397ec26e5882": { "describe": { @@ -2792,6 +2813,20 @@ }, "query": "INSERT INTO app_version\n (flow_id, value, created_by)\n VALUES ($1, $2, $3) RETURNING id" }, + "930ad84a4db26fa6d2c8447d447099e944a0ea7b1266b4d02cee620fe3d761a5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Bool", + "Text" + ] + } + }, + "query": "UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2 WHERE workspace_id = $3" + }, "9490a4388f43e45e32911f1129e623f2d73ce2da7a948fe134bea1c87cdbefd1": { "describe": { "columns": [ @@ -2949,6 +2984,32 @@ }, "query": "DELETE FROM token WHERE token = $1 RETURNING email" }, + "9db64c9ff790d8c833c1e831a87803c32103ce9de68cc9c08d6f56cc988d7e37": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "auto_invite_operator", + "ordinal": 1, + "type_info": "Bool" + } + ], + "nullable": [ + false, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT workspace_id, auto_invite_operator FROM workspace_settings WHERE auto_invite_domain = $1" + }, "9def921a0b697e4cbcb6ad88196ff6f66358f3b819aaf4e279596ccfc433fa92": { "describe": { "columns": [ @@ -3375,9 +3436,15 @@ "name": "is_admin", "ordinal": 2, "type_info": "Bool" + }, + { + "name": "operator", + "ordinal": 3, + "type_info": "Bool" } ], "nullable": [ + false, false, false, false @@ -3597,6 +3664,21 @@ }, "query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE" }, + "be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool", + "Bool" + ] + } + }, + "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, $3, $4)" + }, "bf1d8e043338867e1da1ed236ff6c85a566d5fd58d4b0d5c3a10454513811ba3": { "describe": { "columns": [], @@ -3641,6 +3723,20 @@ }, "query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6, dependency_job = NULL WHERE path = $7 AND workspace_id = $8" }, + "c1149f74ef0cca4a48be80f523d339d78e8e20e2bfacac9ad1b6b75c5ae370fb": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Bool", + "Text", + "Text" + ] + } + }, + "query": "UPDATE usr SET operator = $1 WHERE username = $2 AND workspace_id = $3" + }, "c2849e67b9fea0dc46e6d7000f5a0c9dab89ae80a183d9255f8fdb356b4bc61c": { "describe": { "columns": [ @@ -3734,26 +3830,6 @@ }, "query": "\n INSERT INTO resume_job\n (id, resume_id, job, flow, value, approver)\n VALUES ($1, $2, $3, $4, $5, $6)\n " }, - "c5429cd91368fcdc4da65ef6d84b32b2726f5ea938bc53bf6ed63465bd0eb0f2": { - "describe": { - "columns": [ - { - "name": "workspace_id", - "ordinal": 0, - "type_info": "Varchar" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "SELECT workspace_id FROM workspace_settings WHERE auto_invite_domain = $1" - }, "c59dd666b9a316c027e8c319b80ccbab3a220d93b64357981bd4a03324dad1d0": { "describe": { "columns": [ @@ -3883,18 +3959,6 @@ }, "query": "UPDATE queue SET running = false WHERE last_ping < now() - ($1 || ' seconds')::interval AND running = true AND job_kind != $2 AND same_worker = false RETURNING id, workspace_id, last_ping" }, - "ccb05bf4c0a0f297f75498a684673ae7e4f6ed47f7a8f5d7154e0e0350865d41": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "UPDATE workspace_settings SET auto_invite_domain = NULL WHERE workspace_id = $1" - }, "d0308abac80575038203b60bb66d3b39b586939da0421a595e47c7a759616431": { "describe": { "columns": [], @@ -3978,6 +4042,20 @@ }, "query": "SELECT app.id, app.path, app.summary, app.versions, app.policy,\n app.extra_perms, app_version.value, \n app_version.created_at, app_version.created_by from app, app_version \n WHERE app.path = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]" }, + "d4bf68b4c2bfd11db84787e41e92c13612fda7352e841f3cdef45ea762588f04": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Bool" + ] + } + }, + "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING" + }, "d4eb7aea60894b65498144b9bf522beba612f36368d62fe4e94b5b9e26349d32": { "describe": { "columns": [ @@ -4425,6 +4503,33 @@ }, "query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name, slack_email)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3, slack_email = $4" }, + "ec85a425f88044c6ed4f8fcea223c28eb9fb8c16c89a52d4c4552bd149badafa": { + "describe": { + "columns": [ + { + "name": "is_admin", + "ordinal": 0, + "type_info": "Bool" + }, + { + "name": "operator", + "ordinal": 1, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator" + }, "ed400dbf4a1fa597bc984240cfe9069369a8a265b127116187d78c0ba93ae076": { "describe": { "columns": [ @@ -4823,18 +4928,5 @@ } }, "query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3" - }, - "fceeeaa8d3c97273c8e77b3badd9e7cf82deead59c0d0619e0c303ea665f7d72": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - } - }, - "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n SELECT $1::text, email, false FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING" } } \ No newline at end of file diff --git a/backend/windmill-api/banned_domains.txt b/backend/windmill-api/banned_domains.txt index 24fee41d86..0a8bdda67e 100644 --- a/backend/windmill-api/banned_domains.txt +++ b/backend/windmill-api/banned_domains.txt @@ -1 +1,6 @@ -gmail.com \ No newline at end of file +gmail.com +hotmail.com +live.com +yahoo.com +protonmail.com +yandex.com \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 3a62112183..a16d703b71 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -638,9 +638,12 @@ paths: type: string is_admin: type: boolean + operator: + type: boolean required: - email - is_admin + - operator responses: "200": description: status @@ -669,9 +672,12 @@ paths: type: string is_admin: type: boolean + operator: + type: boolean required: - email - is_admin + - operator responses: "200": description: status @@ -781,6 +787,8 @@ paths: type: string auto_invite_domain: type: string + auto_invite_operator: + type: boolean /w/{workspace}/workspaces/edit_slack_command: post: @@ -825,10 +833,8 @@ paths: schema: type: object properties: - set: + operator: type: boolean - required: - - set responses: "200": @@ -4183,6 +4189,8 @@ components: enum: [python3, deno, go, bash] is_skipped: type: boolean + email: + type: string required: - id - created_by @@ -4195,6 +4203,7 @@ components: - permissioned_as - is_flow_step - is_skipped + - email Job: allOf: @@ -4245,12 +4254,8 @@ components: Usage: type: object properties: - duration_ms: - type: integer - jobs: - type: integer - flows: - type: integer + executions: + type: number Login: type: object @@ -4282,6 +4287,10 @@ components: properties: is_admin: type: boolean + operator: + type: boolean + disabled: + type: boolean TruncatedToken: type: object @@ -4858,10 +4867,13 @@ components: type: string is_admin: type: boolean + operator: + type: boolean required: - workspace_id - email - is_admin + - operator GlobalUserInfo: type: object diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index c07d6febc4..3827bc9e34 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -238,6 +238,7 @@ pub struct CompletedJob { pub is_flow_step: bool, pub language: Option, pub is_skipped: bool, + pub email: String, } #[derive(Deserialize, Clone)] @@ -400,6 +401,7 @@ async fn list_jobs( "is_flow_step", "language", "false as is_skipped", + "email", ], ); let sqlc = list_completed_jobs_query( @@ -432,6 +434,7 @@ async fn list_jobs( "is_flow_step", "language", "is_skipped", + "email", ], ); let sql = format!( @@ -807,6 +810,7 @@ struct UnifiedJob { is_flow_step: bool, language: Option, is_skipped: bool, + email: String, } impl From for Job { @@ -839,6 +843,7 @@ impl From for Job { is_flow_step: uj.is_flow_step, language: uj.language, is_skipped: uj.is_skipped, + email: uj.email, }), "QueuedJob" => Job::QueuedJob(QueuedJob { workspace_id: uj.workspace_id, @@ -868,7 +873,7 @@ impl From for Job { language: uj.language, same_worker: false, pre_run_error: None, - email: "".to_string(), + email: uj.email, }), t => panic!("job type {} not valid", t), } @@ -1392,6 +1397,7 @@ async fn list_completed_jobs( "is_flow_step", "language", "is_skipped", + "email", ], ) .sql()?; diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index de966a8376..1f591d602b 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -123,7 +123,7 @@ impl AuthCache { let is_admin = super_admin || sqlx::query_scalar!( "SELECT is_admin FROM usr where username = $1 AND \ - workspace_id = $2", + workspace_id = $2 AND disabled = false", name, &w_id.as_ref().unwrap() ) @@ -162,7 +162,7 @@ impl AuthCache { if w_id.is_some() { let row_o = sqlx::query_as::<_, (String, bool)>( "SELECT username, is_admin FROM usr where email = $1 AND \ - workspace_id = $2", + workspace_id = $2 AND disabled = false", ) .bind(&email) .bind(&w_id.as_ref().unwrap()) @@ -368,9 +368,7 @@ pub struct User { #[derive(FromRow, Serialize)] pub struct Usage { - pub duration_ms: i64, - pub jobs: i64, - pub flows: i64, + pub executions: i64, } #[derive(Serialize)] @@ -409,6 +407,7 @@ pub struct WorkspaceInvite { pub workspace_id: String, pub email: String, pub is_admin: bool, + pub operator: bool, } #[derive(FromRow, Serialize)] @@ -465,7 +464,8 @@ pub struct EditUser { #[derive(Deserialize)] pub struct EditWorkspaceUser { pub is_admin: Option, - pub enabled: Option, + pub operator: Option, + pub disabled: Option, } #[derive(Deserialize)] @@ -542,14 +542,12 @@ async fn list_users( SELECT usr.*, usage.* FROM usr , LATERAL ( - SELECT COALESCE(SUM(duration_ms), 0) duration_ms - , COALESCE(SUM(job_kind IN ('flow', 'flowpreview') ::int), 0) flows - , COALESCE(SUM(job_kind NOT IN ('flow', 'flowpreview') ::int), 0) jobs + SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0) executions FROM completed_job WHERE workspace_id = usr.workspace_id - AND created_by = usr.username - AND parent_job IS NULL - AND now() - '2 week'::interval < created_at + AND job_kind NOT IN ('flow', 'flowpreview') + AND email = usr.email + AND now() - '5 week'::interval < created_at ) usage WHERE workspace_id = $1 ", @@ -863,31 +861,40 @@ async fn accept_invite( } let mut tx = db.begin().await?; - let is_admin = sqlx::query_scalar!( - "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin", + let r = sqlx::query!( + "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator", nu.workspace_id, email, ) .fetch_optional(&mut tx) .await?; - if let Some(is_admin) = is_admin { - tx = add_user_to_workspace(&nu.workspace_id, &email, &nu.username, is_admin, tx).await?; + let is_some = r.is_some(); + if let Some(r) = r { + tx = add_user_to_workspace( + &nu.workspace_id, + &email, + &nu.username, + r.is_admin, + r.operator, + tx, + ) + .await?; + + audit_log( + &mut tx, + &nu.username, + "users.accept_invite", + ActionKind::Create, + &nu.workspace_id, + Some(&email), + None, + ) + .await?; + tx.commit().await?; } - audit_log( - &mut tx, - &nu.username, - "users.accept_invite", - ActionKind::Create, - &nu.workspace_id, - Some(&email), - None, - ) - .await?; - tx.commit().await?; - - if is_admin.is_some() { + if is_some { Ok(( StatusCode::CREATED, format!( @@ -905,6 +912,7 @@ async fn add_user_to_workspace<'c>( email: &str, username: &str, is_admin: bool, + operator: bool, mut tx: sqlx::Transaction<'c, sqlx::Postgres>, ) -> error::Result> { let already_exists_username = sqlx::query_scalar!( @@ -941,12 +949,13 @@ async fn add_user_to_workspace<'c>( sqlx::query!( "INSERT INTO usr - (workspace_id, email, username, is_admin) - VALUES ($1, $2, $3, $4)", + (workspace_id, email, username, is_admin, operator) + VALUES ($1, $2, $3, $4, $5)", &w_id, email, username, - is_admin + is_admin, + operator ) .execute(&mut tx) .await?; @@ -993,6 +1002,28 @@ async fn update_workspace_user( .await?; } + if let Some(a) = eu.operator { + sqlx::query_scalar!( + "UPDATE usr SET operator = $1 WHERE username = $2 AND workspace_id = $3", + a, + &username_to_update, + &w_id + ) + .execute(&mut tx) + .await?; + } + + if let Some(a) = eu.disabled { + sqlx::query_scalar!( + "UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3", + a, + &username_to_update, + &w_id + ) + .execute(&mut tx) + .await?; + } + audit_log( &mut tx, &username, @@ -1118,15 +1149,15 @@ async fn create_user( audit_log( &mut tx, &email, - "users.update", - ActionKind::Update, + "users.add_global", + ActionKind::Create, "global", Some(&nu.email), None, ) .await?; tx.commit().await?; - invite_user_to_all_auto_invite_worspaces(&db, &email).await?; + invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?; Ok((StatusCode::CREATED, format!("email {} created", nu.email))) } diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 297b2d8aa1..da8ebdf3fe 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -77,6 +77,7 @@ pub struct WorkspaceSettings { pub slack_command_script: Option, pub slack_email: String, pub auto_invite_domain: Option, + pub auto_invite_operator: Option, } #[derive(FromRow, Serialize, Debug)] @@ -101,7 +102,7 @@ struct EditCommandScript { #[derive(Deserialize)] struct EditAutoInvite { - set: bool, + operator: Option, } #[derive(Deserialize)] @@ -145,6 +146,7 @@ struct ValidateUsername { pub struct NewWorkspaceInvite { pub email: String, pub is_admin: bool, + pub operator: bool, } async fn list_pending_invites( @@ -277,7 +279,7 @@ async fn edit_auto_invite( let mut tx = db.begin().await?; - if ea.set { + if let Some(operator) = ea.operator { if BANNED_DOMAINS.contains(domain) { return Err(Error::BadRequest(format!( "Domain {} is not allowed", @@ -286,8 +288,9 @@ async fn edit_auto_invite( } sqlx::query!( - "UPDATE workspace_settings SET auto_invite_domain = $1 WHERE workspace_id = $2", + "UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2 WHERE workspace_id = $3", domain, + operator, &w_id ) .execute(&mut tx) @@ -295,20 +298,21 @@ async fn edit_auto_invite( sqlx::query!( "INSERT INTO workspace_invite - (workspace_id, email, is_admin) - SELECT $1::text, email, false FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS ( + (workspace_id, email, is_admin, operator) + SELECT $1::text, email, false, $3 FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS ( SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email ) ON CONFLICT DO NOTHING", &w_id, - &domain + &domain, + operator ) .execute(&mut tx) .await?; } else { sqlx::query!( - "UPDATE workspace_settings SET auto_invite_domain = NULL WHERE workspace_id = $1", - &w_id + "UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL WHERE workspace_id = $1", + &w_id, ) .execute(&mut tx) .await?; @@ -320,7 +324,7 @@ async fn edit_auto_invite( ActionKind::Update, &w_id, Some(&authed.email), - Some([("set", &ea.set.to_string()[..])].into()), + Some([("operator", &format!("{:?}", ea.operator)[..])].into()), ) .await?; tx.commit().await?; @@ -533,20 +537,21 @@ async fn delete_workspace( pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> Result<()> { let mut tx = db.begin().await?; let domain = email.split('@').last().unwrap(); - let workspaces = sqlx::query_scalar!( - "SELECT workspace_id FROM workspace_settings WHERE auto_invite_domain = $1", + let workspaces = sqlx::query!( + "SELECT workspace_id, auto_invite_operator FROM workspace_settings WHERE auto_invite_domain = $1", domain ) .fetch_all(&mut tx) .await?; - for w in workspaces { + for r in workspaces { sqlx::query!( "INSERT INTO workspace_invite - (workspace_id, email, is_admin) - VALUES ($1, $2, false) + (workspace_id, email, is_admin, operator) + VALUES ($1, $2, false, $3) ON CONFLICT DO NOTHING", - w, + r.workspace_id, email, + r.auto_invite_operator ) .execute(&mut tx) .await?; @@ -567,11 +572,12 @@ async fn invite_user( sqlx::query!( "INSERT INTO workspace_invite - (workspace_id, email, is_admin) - VALUES ($1, $2, $3)", + (workspace_id, email, is_admin, operator) + VALUES ($1, $2, $3, $4)", &w_id, nu.email, - nu.is_admin + nu.is_admin, + nu.operator ) .execute(&mut tx) .await?; @@ -596,10 +602,11 @@ async fn delete_invite( sqlx::query!( "DELETE FROM workspace_invite WHERE - workspace_id = $1 AND email = $2 AND is_admin = $3", + workspace_id = $1 AND email = $2 AND is_admin = $3 AND operator = $4", &w_id, nu.email, - nu.is_admin + nu.is_admin, + nu.operator ) .execute(&mut tx) .await?; diff --git a/backend/windmill-worker/src/jobs.rs b/backend/windmill-worker/src/jobs.rs index a8feecbef8..00f8afafe8 100644 --- a/backend/windmill-worker/src/jobs.rs +++ b/backend/windmill-worker/src/jobs.rs @@ -126,9 +126,10 @@ pub async fn add_completed_job( , raw_flow , is_flow_step , is_skipped - , language ) + , language + , email ) VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,\ - $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25) + $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27) ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)", queued_job.workspace_id, queued_job.id, @@ -155,7 +156,8 @@ pub async fn add_completed_job( queued_job.is_flow_step, skipped, queued_job.language: ScriptLang, - duration: Option + duration: Option, + queued_job.email ) .execute(&mut tx) .await diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index d87a9d09be..77ac50190f 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -2,7 +2,7 @@ import { goto } from '$app/navigation' import { page } from '$app/stores' import { FlowService, ScheduleService, type Flow } from '$lib/gen' - import { workspaceStore } from '$lib/stores' + import { userStore, workspaceStore } from '$lib/stores' import { encodeState, formatCron, @@ -186,80 +186,84 @@ let flowViewer: Drawer - +{#if !$userStore?.operator} + - - -
- -
-
-
+ + +
+ +
+
+
-
- -
-
- - -
- -
- -
+
+ +
+
+ + View Graph + +
+ +
+ +
+ +
-
- - {#if $flowStateStore} - - {:else} - Loading... - {/if} -
+ + {#if $flowStateStore} + + {:else} + Loading... + {/if} +
+{:else} + Flow Builder not available to operators +{/if} diff --git a/frontend/src/lib/components/InviteGlobalUser.svelte b/frontend/src/lib/components/InviteGlobalUser.svelte index 914cd3a934..c433a6de0a 100644 --- a/frontend/src/lib/components/InviteGlobalUser.svelte +++ b/frontend/src/lib/components/InviteGlobalUser.svelte @@ -31,7 +31,7 @@ company } }) - sendUserToast(`Successfully added ${email}. Welcome to them!`) + sendUserToast(`Added ${email}`) dispatch('new') } @@ -39,7 +39,7 @@
- + diff --git a/frontend/src/lib/components/InviteUser.svelte b/frontend/src/lib/components/InviteUser.svelte index e000c8d34b..64aefcae52 100644 --- a/frontend/src/lib/components/InviteUser.svelte +++ b/frontend/src/lib/components/InviteUser.svelte @@ -3,8 +3,8 @@ import { createEventDispatcher } from 'svelte' import { workspaceStore } from '$lib/stores' import { WorkspaceService } from '$lib/gen' - import { Button } from './common' - import Toggle from '$lib/components/Toggle.svelte' + import { Button, ToggleButton, ToggleButtonGroup } from './common' + import Tooltip from './Tooltip.svelte' const dispatch = createEventDispatcher() @@ -24,18 +24,33 @@ workspace: $workspaceStore!, requestBody: { email, - is_admin + is_admin: selected == 'admin', + operator: selected == 'operator' } }) - sendUserToast(`Successfully invited ${email}. Welcome to them!`) + sendUserToast(`Invited ${email}`) dispatch('new') } + + let selected: 'operator' | 'author' | 'admin' = 'author'
- - + + Operator An operator can only execute and view scripts/flows/apps from your workspace, and only + those that he has visibility on + Author An Author can execute and view scripts/flows/apps, but he can also create new ones + Admin + + -
- - - + +
+
+ + + +
-
- - {#if step === 1} - -
-

Path & Summary

- changeStep(2)} - namePlaceholder="my_script" - kind="script" - /> -
+{:else} + Script Builder not available to operators +{/if} diff --git a/frontend/src/lib/components/SuperadminSettings.svelte b/frontend/src/lib/components/SuperadminSettings.svelte index 898bc691c6..734d585a79 100644 --- a/frontend/src/lib/components/SuperadminSettings.svelte +++ b/frontend/src/lib/components/SuperadminSettings.svelte @@ -26,9 +26,6 @@ let users: GlobalUserInfo[] = [] let filteredUsers: GlobalUserInfo[] = [] - let deleteConfirmedCallback: (() => void) | undefined = undefined - $: openConfirmation = Boolean(deleteConfirmedCallback) - async function loadVersion(): Promise { version = await SettingsService.backendVersion() } @@ -47,39 +44,13 @@ - { - deleteConfirmedCallback = undefined - }} - on:confirmed={() => { - if (deleteConfirmedCallback) { - deleteConfirmedCallback() - } - deleteConfirmedCallback = undefined - }} - > -
- Are you sure you want to remove this user? - -
- You can press - SHIFT - while removing a variable to bypass confirmation. -
-
-
-
-
Windmill {version}
- +
@@ -91,21 +62,23 @@ email - superadmin auth name company + {#if filteredUsers && users} {#each filteredUsers as { email, super_admin, login_type, name, company } (email)} {email} - {super_admin ? 'yes' : ''} {login_type} {name ?? ''} {company ?? ''} + {#if super_admin}Superadmin{/if}
{super_admin ? 'non-superadmin' : 'superadmin'} |
diff --git a/frontend/src/lib/components/VariableEditor.svelte b/frontend/src/lib/components/VariableEditor.svelte index 03b5d09128..1dab82b6ce 100644 --- a/frontend/src/lib/components/VariableEditor.svelte +++ b/frontend/src/lib/components/VariableEditor.svelte @@ -92,7 +92,7 @@ description: variable.description } }) - sendUserToast(`Successfully created variable ${path}`) + sendUserToast(`Created variable ${path}`) dispatch('create') drawer.closeDrawer() } @@ -115,7 +115,7 @@ description: getV.description != variable.description ? variable.description : undefined } }) - sendUserToast(`Successfully updated variable at ${initialPath}`) + sendUserToast(`Updated variable ${initialPath}`) dispatch('create') drawer.closeDrawer() } catch (err) { diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index cddd714b8e..31a18e215e 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -25,6 +25,7 @@ import ContextPanel from './contextPanel/ContextPanel.svelte' import { classNames } from '$lib/utils' import AppPreview from './AppPreview.svelte' + import { userStore } from '$lib/stores' export let app: App export let path: string @@ -67,63 +68,67 @@ $: width = $breakpoint === 'sm' ? 'w-[640px]' : 'w-full ' -{#if initialMode !== 'preview'} - -{/if} +{#if !$userStore?.operator} + {#if initialMode !== 'preview'} + + {/if} -{#if previewing} - + {#if previewing} + + {:else} + + + + + +
+ {#if $appStore.grid} +
+ +
+ {/if} + {#if $connectingInput.opened} +
+ {/if} +
+ + + + +
+ + Insert +
+
+ +
+ + Settings +
+
+ + + {#if $selectedComponent !== undefined} + {#each $appStore.grid as gridItem (gridItem.id)} + {#if gridItem.data.id === $selectedComponent} + + {/if} + {/each} + {/if} + {#if $selectedComponent === undefined} +
No component selected.
+ {/if} +
+ + + +
+
+
+ + {/if} {:else} - - - - - -
- {#if $appStore.grid} -
- -
- {/if} - {#if $connectingInput.opened} -
- {/if} -
- - - - -
- - Insert -
-
- -
- - Settings -
-
- - - {#if $selectedComponent !== undefined} - {#each $appStore.grid as gridItem (gridItem.id)} - {#if gridItem.data.id === $selectedComponent} - - {/if} - {/each} - {/if} - {#if $selectedComponent === undefined} -
No component selected.
- {/if} -
- - - -
-
-
- + App editor not available to operators {/if} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index d6a022237d..c2ad4bb3e8 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -41,10 +41,10 @@ } }) .then(() => { - sendUserToast('Saved successfully.') + sendUserToast('Saved') }) .catch(() => { - sendUserToast('Error during saving. Please try again later.', true) + sendUserToast('Error during saving. Please try again later', true) }) .finally(() => { loading.save = false diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index 6b78318f78..ce51e41896 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -2,7 +2,7 @@ import Dropdown from '$lib/components/Dropdown.svelte' import SharedBadge from '$lib/components/SharedBadge.svelte' import { AppService, type ListableApp } from '$lib/gen' - import { workspaceStore } from '$lib/stores' + import { userStore, workspaceStore } from '$lib/stores' import { faEdit, faEye, faTrashAlt } from '@fortawesome/free-solid-svg-icons' import { createEventDispatcher } from 'svelte' import Button from '../button/Button.svelte' @@ -32,18 +32,20 @@
- {:else} -
- -
+ {#if !$userStore?.operator} + {#if canWrite} +
+ +
+ {:else} +
+ +
+ {/if} {/if} -
- {:else} -
- -
+ {#if !$userStore?.operator} + {#if canWrite} +
+ +
+ {:else} +
+ +
+ {/if} {/if}
+
dispatch('user-settings')} diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index 4ce3988f75..1b309692e1 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -9,6 +9,7 @@ export interface UserExt { username: string; is_admin: boolean; is_super_admin: boolean; + operator: boolean; created_at: string; groups: string[]; pgroups: string[]; diff --git a/frontend/src/routes/flows/get/[...path].svelte b/frontend/src/routes/flows/get/[...path].svelte index 72349832eb..0fb2fd66ae 100644 --- a/frontend/src/routes/flows/get/[...path].svelte +++ b/frontend/src/routes/flows/get/[...path].svelte @@ -157,25 +157,27 @@ > Run - - + {#if !$userStore?.operator} + + + {/if} diff --git a/frontend/src/routes/index.svelte b/frontend/src/routes/index.svelte index 17d3fbaada..a4d6d8bcd1 100644 --- a/frontend/src/routes/index.svelte +++ b/frontend/src/routes/index.svelte @@ -1,6 +1,6 @@ @@ -120,6 +122,16 @@ bind:checked={auto_invite} options={{ right: `Auto invite users with the same domain (${domain})` }} /> + {#if auto_invite} + + An operator can only execute and view scripts/flows/apps from your workspace, and only those + that he has visibility on + {/if} {#if !isDomainAllowed}
{domain} domain not allowed for auto-invite
{/if} diff --git a/frontend/src/routes/user/workspaces@user.svelte b/frontend/src/routes/user/workspaces@user.svelte index 2c10924ea9..dd3cbbccf0 100644 --- a/frontend/src/routes/user/workspaces@user.svelte +++ b/frontend/src/routes/user/workspaces@user.svelte @@ -131,6 +131,8 @@ {invite.workspace_id} {#if invite.is_admin} as an admin + {:else if invite.operator} + as an operator {/if}
diff --git a/frontend/src/routes/workspace_settings.svelte b/frontend/src/routes/workspace_settings.svelte index fe473c73ce..d13b1d6b49 100644 --- a/frontend/src/routes/workspace_settings.svelte +++ b/frontend/src/routes/workspace_settings.svelte @@ -28,6 +28,9 @@ import Tooltip from '$lib/components/Tooltip.svelte' import { faScroll, faBarsStaggered } from '@fortawesome/free-solid-svg-icons' import SearchItems from '$lib/components/SearchItems.svelte' + import Toggle from '$lib/components/Toggle.svelte' + import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte' + import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte' let users: User[] | undefined = undefined let invites: WorkspaceInvite[] = [] @@ -38,6 +41,7 @@ let team_name: string | undefined let auto_invite_domain: string | undefined let itemKind: 'flow' | 'script' = 'flow' + let operatorOnly: boolean | undefined = undefined // function getDropDownItems(username: string): DropdownItem[] { // return [ @@ -76,6 +80,7 @@ const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! }) team_name = settings.slack_name auto_invite_domain = settings.auto_invite_domain + operatorOnly = settings.auto_invite_operator scriptPath = (settings.slack_command_script ?? '').split('/').slice(1).join('/') initialPath = scriptPath } @@ -104,6 +109,34 @@ loadSettings() } } + + $: operatorOnly != undefined && auto_invite_domain && setOperatorOnly() + + async function removeAllInvitesFromDomain() { + await Promise.all( + invites + .filter((x) => x.email.endsWith('@' + auto_invite_domain ?? '')) + .map(({ email, is_admin, operator }) => + WorkspaceService.deleteInvite({ + workspace: $workspaceStore ?? '', + requestBody: { + email, + is_admin, + operator + } + }) + ) + ) + } + async function setOperatorOnly() { + await removeAllInvitesFromDomain() + await WorkspaceService.editAutoInvite({ + workspace: $workspaceStore ?? '', + requestBody: { operator: operatorOnly } + }) + loadSettings() + listInvites() + } email username - role - jobs & flows (2w) + executions (5w) An execution is calculated as 1 for any runs of scripts + 1 for each seconds above + the first one + + + + {#if filteredUsers} - {#each filteredUsers as { email, username, is_admin, usage } (email)} + {#each filteredUsers as { email, username, is_admin, operator, usage, disabled } (email)} {email} {username} - {is_admin ? 'admin' : 'user'} - {usage?.jobs} - {usage?.flows} - {msToSec(usage?.duration_ms)}s - - - - {usage?.executions} +
{#if disabled} + disabled + {/if}
+ +
+ { + const body = + e.detail == 'admin' + ? { is_admin: true, operator: false } + : e.detail == 'operator' + ? { is_admin: false, operator: true } + : { is_admin: false, operator: false } + await UserService.updateUser({ + workspace: $workspaceStore ?? '', + username, + requestBody: body + }) + listUsers() + }} + > + Operator An operator can only execute and view scripts/flows/apps from your + workspace, and only those that he has visibility on + Author An Author can execute and view scripts/flows/apps, but he can also create + new ones + Admin + +
+ + +
+ + | + +
+ {/each} {:else} @@ -196,10 +276,12 @@ - {#each invites as { email, is_admin }} + {#each invites as { email, is_admin, operator }} {email} - {is_admin ? 'admin' : 'user'} + {#if operator}operator{:else if is_admin}admin{/if} + cancel @@ -224,29 +307,42 @@
{#if auto_invite_domain != domain} - +
+ +
{/if} {#if auto_invite_domain} - +
+ +
+ +
+
{/if}
{#if !allowedAutoDomain} @@ -334,7 +430,7 @@ btnClasses="mt-2" on:click={async () => { await WorkspaceService.deleteWorkspace({ workspace: $workspaceStore ?? '' }) - sendUserToast(`Successfully deleted workspace ${$workspaceStore}`) + sendUserToast(`Deleted workspace ${$workspaceStore}`) workspaceStore.set(undefined) usersWorkspaceStore.set(undefined) goto('/user/workspaces')