mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: Add a devops role to act as a "readonly admin" (#4775)
* Prepare sqlx * Add devops role as bool * Add devops reloe and use it for certain endpoints * Critical alerts: complete devops role visiblity * Update ee repo ref * Prepare sqlx
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+1
-2
@@ -72,8 +72,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -52,8 +52,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM queue_view\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "202a4fee4e70ff74d150a187a8daec3609328b1e3fe6e4fac20fb6fe91ff1732"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT devops FROM password WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2c615128e28a3fb1ecb4b7a40f45801b58221898175c5d00dbdaaa085f769ac8"
|
||||
}
|
||||
+1
-2
@@ -58,8 +58,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\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, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC\n LIMIT $1 OFFSET $2",
|
||||
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\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\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": [
|
||||
{
|
||||
@@ -30,16 +30,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
@@ -56,10 +61,11 @@
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef"
|
||||
"hash": "3895cee539a24b4c6ea89fa7a835fc62bc93b0530efba09fc3c32a8f93eaabb1"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE password SET devops = $1 WHERE email = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "411788111afccd826ce78b266153600939c65c75be8894322b90d9da18dcb824"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow AS \"raw_flow!: Json<Box<sqlx::types::JsonRawValue>>\"\n FROM queue_view WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "raw_flow!: Json<Box<sqlx::types::JsonRawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4996b5af348bc335a5ec14496683cee096e37bdde7bb05c11ee0c4792f70dc60"
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n flow_status AS \"flow_status!: Json<Box<RawValue>>\",\n raw_flow->'modules'->(flow_status->'step')::int AS \"module: Json<Box<RawValue>>\"\n FROM queue_view WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "flow_status!: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "module: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4bbf4dbf5b18d8dfd62c0a029ec4deb95a348dc804ee23a02698adb9aa32b375"
|
||||
}
|
||||
+1
-2
@@ -57,8 +57,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM completed_job_view\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "6673aeb02e1c39616c6f38fd2bad841271bc08401573f4d1be8e8896cd77507b"
|
||||
}
|
||||
+1
-2
@@ -52,8 +52,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -30,8 +30,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -57,8 +57,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -52,8 +52,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_code, raw_lock, raw_flow AS \"raw_flow: Json<Box<JsonRawValue>>\"\n FROM queue_view WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "raw_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "raw_lock",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "raw_flow: Json<Box<JsonRawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a1a339c9e744b5661d2b4d1354e593b97b4e007fa9a499b49c3d1ebf9b645a3b"
|
||||
}
|
||||
+1
-2
@@ -38,8 +38,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_code, raw_lock, raw_flow AS \"raw_flow: Json<Box<RawValue>>\"\n FROM queue_view WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "raw_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "raw_lock",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "raw_flow: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c05f21e7027a985e5a657b689a0f5bedefcaa15b7887402c0d5d33cb7b5fb362"
|
||||
}
|
||||
+1
-2
@@ -78,8 +78,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+12
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE email = $1",
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only FROM password WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,26 +20,31 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "verified",
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -54,11 +59,12 @@
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542"
|
||||
"hash": "df5b933f81ca7e3bbb3fb522baedf749fa3bbbf2c0e43d5d4ea148b5bc990067"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow AS \"raw_flow!: Json<Box<JsonRawValue>>\"\n FROM completed_job_view WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "raw_flow!: Json<Box<JsonRawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e0e895527d6807699c918ca87e0b016751c9b5a1dab632dc4abdb1dd6aa1fcbc"
|
||||
}
|
||||
+11
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, login_type::text, verified, super_admin, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
|
||||
"query": "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,21 +25,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -55,11 +60,12 @@
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a"
|
||||
"hash": "f07a705df1a988827e099d146f5308b763293a27adf30d02df605317791d8126"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'modules'->($1)::text->'value'->>'type' = 'flow' FROM queue_view WHERE id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "f5c94d89eb8c86916d7c64ffa8ea2de1feecbfde74e9a1374b3668ec72d7bac1"
|
||||
}
|
||||
+1
-2
@@ -57,8 +57,7 @@
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp"
|
||||
"ansible"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
ce2e482a5fa4f5a8ae2300276256784da8d1cf26
|
||||
4a0e64ab860eb88ff0d21b8705e647c9b76c46ee
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE password DROP COLUMN devops;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE password ADD COLUMN devops BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -407,6 +407,8 @@ paths:
|
||||
properties:
|
||||
is_super_admin:
|
||||
type: boolean
|
||||
is_devops:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
responses:
|
||||
@@ -12580,6 +12582,8 @@ components:
|
||||
enum: ["password", "github"]
|
||||
super_admin:
|
||||
type: boolean
|
||||
devops:
|
||||
type: boolean
|
||||
verified:
|
||||
type: boolean
|
||||
name:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::utils::content_plain;
|
||||
use crate::utils::{content_plain, require_devops_role};
|
||||
use axum::{body::Body, extract::Query, response::Response, routing::get, Extension, Json, Router};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -15,10 +15,7 @@ use windmill_common::{
|
||||
utils::Pagination,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
utils::require_super_admin,
|
||||
};
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -51,7 +48,7 @@ async fn list_files(
|
||||
Query(pagination): Query<Pagination>,
|
||||
Query(lq): Query<LogFileQuery>,
|
||||
) -> JsonResult<Vec<LogFile>> {
|
||||
require_super_admin(&db, &email).await?;
|
||||
require_devops_role(&db, &email).await?;
|
||||
let (per_page, offset) = windmill_common::utils::paginate(pagination);
|
||||
|
||||
let mut sqlb = sql_builder::SqlBuilder::select_from("log_file")
|
||||
@@ -98,7 +95,7 @@ async fn get_log_file(
|
||||
) -> windmill_common::error::Result<Response> {
|
||||
use windmill_common::tracing_init::TMP_WINDMILL_LOGS_SERVICE;
|
||||
|
||||
require_super_admin(&db, &email).await?;
|
||||
require_devops_role(&db, &email).await?;
|
||||
let path = path.to_path();
|
||||
#[cfg(feature = "parquet")]
|
||||
let s3_client = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::time::Duration;
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
ee::validate_license_key,
|
||||
utils::{generate_instance_username_for_all_users, require_super_admin},
|
||||
utils::{require_devops_role, generate_instance_username_for_all_users, require_super_admin},
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
|
||||
@@ -440,7 +440,7 @@ pub async fn get_critical_alerts(
|
||||
authed: ApiAuthed,
|
||||
Query(params): Query<crate::utils::AlertQueryParams>,
|
||||
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
crate::utils::get_critical_alerts(db, params, None).await
|
||||
}
|
||||
@@ -456,8 +456,7 @@ pub async fn acknowledge_critical_alert(
|
||||
authed: ApiAuthed,
|
||||
Path(id): Path<i32>,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
crate::utils::acknowledge_critical_alert(db, None, id).await
|
||||
}
|
||||
|
||||
|
||||
@@ -796,6 +796,7 @@ pub struct GlobalUserInfo {
|
||||
email: String,
|
||||
login_type: Option<String>,
|
||||
super_admin: bool,
|
||||
devops: bool,
|
||||
verified: bool,
|
||||
name: Option<String>,
|
||||
company: Option<String>,
|
||||
@@ -853,6 +854,7 @@ pub struct DeclineInvite {
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditUser {
|
||||
pub is_super_admin: Option<bool>,
|
||||
pub is_devops: Option<bool>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1042,10 +1044,10 @@ async fn list_users_as_super_admin(
|
||||
GlobalUserInfo,
|
||||
"WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),
|
||||
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, name, company, username
|
||||
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username
|
||||
FROM password
|
||||
WHERE email IN (SELECT email FROM active_users)
|
||||
ORDER BY super_admin DESC
|
||||
ORDER BY super_admin DESC, devops DESC
|
||||
LIMIT $1 OFFSET $2",
|
||||
per_page as i32,
|
||||
offset as i32
|
||||
@@ -1055,7 +1057,7 @@ async fn list_users_as_super_admin(
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
GlobalUserInfo,
|
||||
"SELECT email, login_type::text, verified, super_admin, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, email LIMIT \
|
||||
"SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT \
|
||||
$1 OFFSET $2",
|
||||
per_page as i32,
|
||||
offset as i32
|
||||
@@ -1218,7 +1220,7 @@ async fn global_whoami(
|
||||
) -> JsonResult<GlobalUserInfo> {
|
||||
let user = sqlx::query_as!(
|
||||
GlobalUserInfo,
|
||||
"SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE \
|
||||
"SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only FROM password WHERE \
|
||||
email = $1",
|
||||
email
|
||||
)
|
||||
@@ -1233,6 +1235,7 @@ async fn global_whoami(
|
||||
email: email.clone(),
|
||||
login_type: Some("superadmin_secret".to_string()),
|
||||
super_admin: true,
|
||||
devops: false,
|
||||
verified: true,
|
||||
name: None,
|
||||
company: None,
|
||||
@@ -1876,6 +1879,16 @@ async fn update_user(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(dv) = eu.is_devops {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE password SET devops = $1 WHERE email = $2",
|
||||
dv,
|
||||
&email_to_update
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(n) = eu.name {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE password SET name = $1 WHERE email = $2",
|
||||
|
||||
@@ -11,7 +11,7 @@ use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use windmill_common::{
|
||||
auth::is_super_admin_email,
|
||||
auth::{is_devops_email, is_super_admin_email},
|
||||
error::{self, Error},
|
||||
DB,
|
||||
};
|
||||
@@ -32,13 +32,25 @@ pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
|
||||
|
||||
if !is_admin {
|
||||
Err(Error::NotAuthorized(
|
||||
"This endpoint require caller to be a super admin".to_owned(),
|
||||
"This endpoint requires the caller to be a super admin".to_owned(),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn require_devops_role(db: &DB, email: &str) -> error::Result<()> {
|
||||
let is_devops = is_devops_email(db, email).await?;
|
||||
|
||||
if is_devops {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::NotAuthorized(
|
||||
"This endpoint requires the caller to have the `devops` role".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref INVALID_USERNAME_CHARS: Regex = Regex::new(r"[^A-Za-z0-9_]").unwrap();
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ use windmill_common::{
|
||||
global_settings::AUTOMATE_USERNAME_CREATION_SETTING,
|
||||
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
|
||||
scripts::{Schema, Script, ScriptLang},
|
||||
utils::{paginate, rd_string, require_admin, Pagination},
|
||||
utils::{paginate, rd_string, require_admin, require_admin_or_devops, Pagination},
|
||||
variables::ExportableListableVariable,
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
@@ -3092,7 +3092,7 @@ pub async fn get_critical_alerts(
|
||||
authed: ApiAuthed,
|
||||
Query(params): Query<crate::utils::AlertQueryParams>,
|
||||
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
require_admin_or_devops(authed.is_admin, &authed.username, &authed.email, &db).await?;
|
||||
|
||||
crate::utils::get_critical_alerts(db, params, Some(w_id)).await
|
||||
}
|
||||
@@ -3108,8 +3108,8 @@ pub async fn acknowledge_critical_alert(
|
||||
Path((w_id, id)): Path<(String, i32)>,
|
||||
authed: ApiAuthed,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
crate::utils::acknowledge_critical_alert(db, Some(w_id), id).await
|
||||
require_admin_or_devops(authed.is_admin, &authed.username, &authed.email, &db).await?;
|
||||
crate::utils::acknowledge_critical_alert(db, Some(w_id), id).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
@@ -3173,4 +3173,4 @@ async fn mute_critical_alerts(
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn mute_critical_alerts() -> Error {
|
||||
Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,20 @@ pub async fn is_super_admin_email(db: &DB, email: &str) -> Result<bool> {
|
||||
Ok(is_admin)
|
||||
}
|
||||
|
||||
pub async fn is_devops_email(db: &DB, email: &str) -> Result<bool> {
|
||||
if is_super_admin_email(db, email).await? {
|
||||
return Ok(true)
|
||||
}
|
||||
|
||||
let is_devops = sqlx::query_scalar!("SELECT devops FROM password WHERE email = $1", email)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e:#}")))?
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(is_devops)
|
||||
}
|
||||
|
||||
pub fn permissioned_as_to_username(permissioned_as: &str) -> String {
|
||||
if let Some((prefix, name)) = permissioned_as.split_once('/') {
|
||||
if prefix == "u" {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::auth::is_devops_email;
|
||||
use crate::ee::LICENSE_KEY_ID;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::ee::{send_critical_alert, CriticalAlertKind};
|
||||
@@ -77,6 +78,16 @@ pub fn require_admin(is_admin: bool, username: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn require_admin_or_devops(is_admin: bool, username: &str, email: &str, db: &DB) -> Result<()> {
|
||||
if !is_admin {
|
||||
if !is_devops_email(db, email).await? {
|
||||
return Err(Error::RequireAdmin(username.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
pub fn hostname() -> String {
|
||||
std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
|
||||
gethostname()
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
</tr>
|
||||
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
|
||||
{#if filteredUsers && users}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username, operator_only } (email)}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{login_type}</td>
|
||||
@@ -252,25 +252,51 @@
|
||||
{/if}
|
||||
<td>
|
||||
<ToggleButtonGroup
|
||||
selected={super_admin}
|
||||
selected={super_admin ? "super_admin" : devops ? "devops" : "user"}
|
||||
on:selected={async (e) => {
|
||||
if (email == $userStore?.email) {
|
||||
sendUserToast('You cannot demote yourself', true)
|
||||
listUsers(activeOnly)
|
||||
return
|
||||
}
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: !super_admin
|
||||
}
|
||||
})
|
||||
|
||||
let role = e.detail;
|
||||
|
||||
if (role === "super_admin") {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: true,
|
||||
is_devops: false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (role === "devops") {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: false,
|
||||
is_devops: true
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
if (role === "user") {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: false,
|
||||
is_devops: false
|
||||
}
|
||||
})
|
||||
}
|
||||
sendUserToast('User updated')
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={false} size="xs" label="User" />
|
||||
<ToggleButton value={true} size="xs" label="Superadmin" />
|
||||
<ToggleButton value={"user"} size="xs" label="User" />
|
||||
<ToggleButton value={"devops"} size="xs" label="Devops" tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin" />
|
||||
<ToggleButton value={"super_admin"} size="xs" label="Superadmin" />
|
||||
</ToggleButtonGroup>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import CriticalAlertModalInner from './CriticalAlertModalInner.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { superadmin, workspaceStore, isCriticalAlertsUIOpen } from '$lib/stores'
|
||||
import { workspaceStore, isCriticalAlertsUIOpen, devopsRole } from '$lib/stores'
|
||||
import Modal from '../common/modal/Modal.svelte'
|
||||
|
||||
export let open: boolean = false
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
const withSuperadminLogic = (superadminFunction, workspaceFunction) => {
|
||||
return async (params = {}) => {
|
||||
if (!$superadmin || workspaceContext) {
|
||||
if (!$devopsRole || workspaceContext) {
|
||||
return workspaceFunction({
|
||||
...params,
|
||||
workspace: $workspaceStore
|
||||
@@ -81,7 +81,7 @@
|
||||
numUnacknowledgedCriticalAlerts === 0 &&
|
||||
unacknowledged.length > 0 &&
|
||||
sendToast &&
|
||||
(($superadmin && !muteSettings.global) || (!$superadmin && !muteSettings.workspace))
|
||||
(($devopsRole && !muteSettings.global) || (!$devopsRole && !muteSettings.workspace))
|
||||
) {
|
||||
sendUserToast(
|
||||
'Critical Alert:',
|
||||
|
||||
@@ -2,18 +2,11 @@
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import {
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
CheckSquare2,
|
||||
AlertTriangle,
|
||||
} from 'lucide-svelte'
|
||||
import { CheckCircle2, AlertCircle, RefreshCw, CheckSquare2, AlertTriangle } from 'lucide-svelte'
|
||||
import type { CriticalAlert } from '$lib/gen'
|
||||
import { onMount } from 'svelte'
|
||||
import { instanceSettingsSelectedTab } from '$lib/stores'
|
||||
import { devopsRole, workspaceStore, instanceSettingsSelectedTab, superadmin, userStore } from '$lib/stores'
|
||||
import { goto } from '$app/navigation'
|
||||
import { superadmin, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
|
||||
@@ -35,7 +28,10 @@
|
||||
|
||||
$: muteSettings
|
||||
$: {
|
||||
if (initialMuteSettings.workspace !== muteSettings.workspace || initialMuteSettings.global !== muteSettings.global) {
|
||||
if (
|
||||
initialMuteSettings.workspace !== muteSettings.workspace ||
|
||||
initialMuteSettings.global !== muteSettings.global
|
||||
) {
|
||||
saveMuteSettings()
|
||||
}
|
||||
}
|
||||
@@ -199,7 +195,7 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if $superadmin}
|
||||
{#if $devopsRole}
|
||||
<div class="flex flex-row py-2 pb-3">
|
||||
<Toggle
|
||||
bind:checked={workspaceContext}
|
||||
@@ -209,29 +205,27 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Section
|
||||
label="Mute Settings"
|
||||
collapsable={true}
|
||||
small={true}
|
||||
>
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $userStore?.is_admin}
|
||||
<Section label="Mute Settings" collapsable={true} small={true}>
|
||||
{#if $superadmin}
|
||||
<div class="flex flex-row pb-1">
|
||||
<Toggle
|
||||
bind:checked={muteSettings.global}
|
||||
options={{ right: 'Mute critical alerts instance wide' }}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row pb-1">
|
||||
<Toggle
|
||||
bind:checked={muteSettings.global}
|
||||
options={{ right: 'Mute critical alerts instance wide' }}
|
||||
bind:checked={muteSettings.workspace}
|
||||
options={{ right: 'Mute critical alerts for current workspace' }}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row pb-1">
|
||||
<Toggle
|
||||
bind:checked={muteSettings.workspace}
|
||||
options={{ right: 'Mute critical alerts for current workspace' }}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<div class="pt-2 flex justify-between items-center">
|
||||
<div class="pr-2">
|
||||
@@ -261,7 +255,7 @@
|
||||
<th class="w-[60px] px-4 py-2 text-center">Type</th>
|
||||
<th class="px-4 py-2 text-center">Message</th>
|
||||
<th class="w-[150px] px-4 py-2 text-center">Created At</th>
|
||||
{#if $superadmin}
|
||||
{#if $devopsRole}
|
||||
<th class="w-[80px] px-4 py-2 text-center">Workspace</th>
|
||||
{/if}
|
||||
<th class="w-[180px] px-4 py-2 text-center">Acknowledge</th>
|
||||
@@ -285,7 +279,7 @@
|
||||
<td class="border px-4 py-2">{message}</td>
|
||||
<!-- Flexible width -->
|
||||
<td class="border px-4 py-2 w-[150px]">{formatDate(created_at)}</td>
|
||||
{#if $superadmin}
|
||||
{#if $devopsRole}
|
||||
<td class="border px-4 py-2 w-[150px]">{workspace_id ? workspace_id : 'global'}</td>
|
||||
{/if}
|
||||
<td class="border px-4 py-2 w-[180px]">
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
userStore,
|
||||
workspaceStore,
|
||||
isCriticalAlertsUIOpen,
|
||||
enterpriseLicense
|
||||
enterpriseLicense,
|
||||
|
||||
devopsRole
|
||||
|
||||
} from '$lib/stores'
|
||||
import { SIDEBAR_SHOW_SCHEDULES } from '$lib/consts'
|
||||
import {
|
||||
@@ -187,7 +190,7 @@
|
||||
],
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
$superadmin || $userStore?.is_admin
|
||||
$devopsRole || $userStore?.is_admin
|
||||
? {
|
||||
label: 'Logs',
|
||||
icon: Logs,
|
||||
@@ -197,7 +200,7 @@
|
||||
href: `${base}/audit_logs`,
|
||||
icon: Eye
|
||||
},
|
||||
...($superadmin
|
||||
...($devopsRole
|
||||
? [
|
||||
{
|
||||
label: 'Service Logs',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { get } from 'svelte/store'
|
||||
import { UserService } from '$lib/gen'
|
||||
import { superadmin } from './stores.js'
|
||||
import { superadmin, devopsRole } from './stores.js'
|
||||
import { goto } from '$lib/navigation'
|
||||
|
||||
export async function refreshSuperadmin(): Promise<void> {
|
||||
@@ -17,4 +17,18 @@ export async function refreshSuperadmin(): Promise<void> {
|
||||
goto('/user/logout')
|
||||
}
|
||||
}
|
||||
|
||||
if (get(devopsRole) == undefined) {
|
||||
try {
|
||||
const me = await UserService.globalWhoami()
|
||||
if (me.devops || me.super_admin) {
|
||||
devopsRole.set(me.email)
|
||||
} else {
|
||||
devopsRole.set(false)
|
||||
}
|
||||
} catch {
|
||||
devopsRole.set(false)
|
||||
goto('/user/logout')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const isPremiumStore = writable<boolean>(false)
|
||||
export const starStore = writable(1)
|
||||
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undefined)
|
||||
export const superadmin = writable<string | false | undefined>(undefined)
|
||||
export const devopsRole = writable<string | false | undefined>(undefined)
|
||||
export const lspTokenStore = writable<string | undefined>(undefined)
|
||||
export const hubBaseUrlStore = writable<string>('https://hub.windmill.dev')
|
||||
export const userWorkspaces: Readable<
|
||||
@@ -126,4 +127,4 @@ export const dbSchemas = writable<DBSchemas>({})
|
||||
|
||||
export const instanceSettingsSelectedTab = writable('Core')
|
||||
|
||||
export const isCriticalAlertsUIOpen = writable(false)
|
||||
export const isCriticalAlertsUIOpen = writable(false)
|
||||
|
||||
@@ -28,7 +28,10 @@
|
||||
type UserExt,
|
||||
defaultScripts,
|
||||
hubBaseUrlStore,
|
||||
usedTriggerKinds
|
||||
usedTriggerKinds,
|
||||
|
||||
devopsRole
|
||||
|
||||
} from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation'
|
||||
@@ -286,7 +289,7 @@
|
||||
setContext('openSearchWithPrefilledText', openSearchModal)
|
||||
|
||||
$: {
|
||||
if ($enterpriseLicense && $workspaceStore && $userStore && ($superadmin || $userStore.is_admin)) {
|
||||
if ($enterpriseLicense && $workspaceStore && $userStore && ($devopsRole || $userStore.is_admin)) {
|
||||
mountModal = true
|
||||
loadCriticalAlertsMuted()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import ServiceLogsInner from '$lib/components/ServiceLogsInner.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { superadmin } from '$lib/stores'
|
||||
import { devopsRole } from '$lib/stores'
|
||||
import { Search, AlertTriangle } from 'lucide-svelte'
|
||||
|
||||
let searchTerm = $page.url.searchParams.get('query') ?? ''
|
||||
@@ -23,9 +23,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !$superadmin}
|
||||
{#if !$devopsRole}
|
||||
<Alert title="Service logs are only available to superadmins" type="warning">
|
||||
Service logs are only available to superadmins
|
||||
Service logs are only available to superadmins (or devops)
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="m-1 px-2 flex flex-row gap-1 items-center border-2 rounded-lg">
|
||||
|
||||
Reference in New Issue
Block a user