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:
wendrul
2024-11-22 14:55:48 +01:00
committed by GitHub
parent e854318f29
commit 4facf3ca3e
45 changed files with 255 additions and 328 deletions
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "?column?",
"name": "bool",
"type_info": "Bool"
}
],
@@ -72,8 +72,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -52,8 +52,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -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"
}
@@ -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"
}
@@ -58,8 +58,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -57,8 +57,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "?column?",
"name": "bool",
"type_info": "Bool"
}
],
@@ -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"
}
@@ -52,8 +52,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -30,8 +30,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -57,8 +57,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -52,8 +52,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -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"
}
@@ -38,8 +38,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -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"
}
@@ -78,8 +78,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -57,8 +57,7 @@
"php",
"bunnative",
"rust",
"ansible",
"csharp"
"ansible"
]
}
}
+1 -1
View File
@@ -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;
+4
View File
@@ -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:
+4 -7
View File
@@ -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
+3 -4
View File
@@ -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
}
+17 -4
View File
@@ -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",
+14 -2
View File
@@ -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();
}
+5 -5
View File
@@ -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())
}
}
+14
View File
@@ -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" {
+11
View File
@@ -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',
+15 -1
View File
@@ -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')
}
}
}
+2 -1
View File
@@ -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">