From ec2d00c3741cfe7153a7ada5defda2e03f441938 Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Tue, 29 Oct 2024 09:26:45 +0100 Subject: [PATCH] improve key renewal (#4519) * improve key renewal * sqlx and fix * fix build * improve everything * nits * nits * update ee ref * nit --- ...ee6d098a2758d16ede000809bd9a09660b604.json | 20 ----- ...b031862e1a18ad9204e02a0194817db26d6a.json} | 12 ++- ...cb5537995f2499f6cb8f0f316fb62902d542.json} | 12 ++- ...18c321c422705f79e69550bb60f377083bcef.json | 65 +++++++++++++++ ...0304333536768527525a43bdd45aedffa3c4f.json | 22 ----- ...e5f544f654d7cddc13acb5d3f4fdc12a8875f.json | 20 ----- backend/ee-repo-ref.txt | 2 +- backend/src/ee.rs | 12 +-- backend/src/main.rs | 81 ++++++++++++++++--- backend/src/monitor.rs | 47 ++++------- backend/windmill-api/openapi.yaml | 7 ++ backend/windmill-api/src/ee.rs | 2 +- backend/windmill-api/src/lib.rs | 8 +- backend/windmill-api/src/settings.rs | 10 ++- backend/windmill-api/src/users.rs | 47 ++++++++--- backend/windmill-common/src/ee.rs | 7 +- backend/windmill-common/src/stats_ee.rs | 1 - backend/windmill-common/src/utils.rs | 8 ++ .../lib/components/InstanceSettings.svelte | 19 ++++- .../lib/components/SuperadminSettings.svelte | 72 ++++++++++++----- 20 files changed, 313 insertions(+), 161 deletions(-) delete mode 100644 backend/.sqlx/query-19f0abd79372698f378cb6deea3ee6d098a2758d16ede000809bd9a09660b604.json rename backend/.sqlx/{query-c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2.json => query-1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a.json} (75%) rename backend/.sqlx/{query-8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37.json => query-2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542.json} (77%) create mode 100644 backend/.sqlx/query-2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef.json delete mode 100644 backend/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json delete mode 100644 backend/.sqlx/query-9ed77e78e6295c62745ac3ac3b7e5f544f654d7cddc13acb5d3f4fdc12a8875f.json diff --git a/backend/.sqlx/query-19f0abd79372698f378cb6deea3ee6d098a2758d16ede000809bd9a09660b604.json b/backend/.sqlx/query-19f0abd79372698f378cb6deea3ee6d098a2758d16ede000809bd9a09660b604.json deleted file mode 100644 index 8796b86902..0000000000 --- a/backend/.sqlx/query-19f0abd79372698f378cb6deea3ee6d098a2758d16ede000809bd9a09660b604.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT created_at FROM metrics WHERE id = 'telemetry' ORDER BY created_at DESC LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "19f0abd79372698f378cb6deea3ee6d098a2758d16ede000809bd9a09660b604" -} diff --git a/backend/.sqlx/query-c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2.json b/backend/.sqlx/query-1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a.json similarity index 75% rename from backend/.sqlx/query-c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2.json rename to backend/.sqlx/query-1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a.json index 6a08bbeab3..b8c5b89128 100644 --- a/backend/.sqlx/query-c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2.json +++ b/backend/.sqlx/query-1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT email, login_type::text, verified, super_admin, name, company, username from password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2", + "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", "describe": { "columns": [ { @@ -37,6 +37,11 @@ "ordinal": 6, "name": "username", "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "operator_only", + "type_info": "Bool" } ], "parameters": { @@ -52,8 +57,9 @@ false, true, true, - true + true, + null ] }, - "hash": "c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2" + "hash": "1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a" } diff --git a/backend/.sqlx/query-8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37.json b/backend/.sqlx/query-2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542.json similarity index 77% rename from backend/.sqlx/query-8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37.json rename to backend/.sqlx/query-2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542.json index ce8adcd5b7..93017f5aec 100644 --- a/backend/.sqlx/query-8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37.json +++ b/backend/.sqlx/query-2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username FROM password WHERE email = $1", + "query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE email = $1", "describe": { "columns": [ { @@ -37,6 +37,11 @@ "ordinal": 6, "name": "username", "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "operator_only", + "type_info": "Bool" } ], "parameters": { @@ -51,8 +56,9 @@ false, true, true, - true + true, + null ] }, - "hash": "8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37" + "hash": "2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542" } diff --git a/backend/.sqlx/query-2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef.json b/backend/.sqlx/query-2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef.json new file mode 100644 index 0000000000..f3eecb9006 --- /dev/null +++ b/backend/.sqlx/query-2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef.json @@ -0,0 +1,65 @@ +{ + "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", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "operator_only", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "login_type", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "verified", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "super_admin", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "company", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + null, + null, + false, + false, + true, + true, + true + ] + }, + "hash": "2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef" +} diff --git a/backend/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json b/backend/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json deleted file mode 100644 index 4187b628b7..0000000000 --- a/backend/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT pg_try_advisory_xact_lock($1)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pg_try_advisory_xact_lock", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f" -} diff --git a/backend/.sqlx/query-9ed77e78e6295c62745ac3ac3b7e5f544f654d7cddc13acb5d3f4fdc12a8875f.json b/backend/.sqlx/query-9ed77e78e6295c62745ac3ac3b7e5f544f654d7cddc13acb5d3f4fdc12a8875f.json deleted file mode 100644 index 9c7af05c42..0000000000 --- a/backend/.sqlx/query-9ed77e78e6295c62745ac3ac3b7e5f544f654d7cddc13acb5d3f4fdc12a8875f.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT created_at FROM metrics WHERE id = 'license_key_renewal' ORDER BY created_at DESC LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "9ed77e78e6295c62745ac3ac3b7e5f544f654d7cddc13acb5d3f4fdc12a8875f" -} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 67f8651560..2c2cd13f33 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -ee638ab91623392cdbf6a139f2b54b9b527585ea \ No newline at end of file +f667fcebdb5a2d1c22c1928d6a316ba57bb43d72 \ No newline at end of file diff --git a/backend/src/ee.rs b/backend/src/ee.rs index ef944b984b..91816cd1ba 100644 --- a/backend/src/ee.rs +++ b/backend/src/ee.rs @@ -1,16 +1,8 @@ -use anyhow::anyhow; -#[cfg(feature = "enterprise")] -use windmill_common::error::{Error, Result}; - -pub async fn set_license_key(_license_key: String) -> anyhow::Result<()> { +pub async fn set_license_key(_license_key: String) -> () { // Implementation is not open source - Err(anyhow!("License cannot be set in Windmill CE")) } #[cfg(feature = "enterprise")] -pub async fn verify_license_key() -> Result<()> { +pub async fn verify_license_key() -> () { // Implementation is not open source - Err(Error::InternalErr( - "License always invalid in Windmill CE".to_string(), - )) } diff --git a/backend/src/main.rs b/backend/src/main.rs index f240961734..e0a43b7d56 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -27,7 +27,7 @@ use uuid::Uuid; use windmill_api::HTTP_CLIENT; #[cfg(feature = "enterprise")] -use windmill_common::ee::schedule_key_renewal; +use windmill_common::ee::{maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICENSE_KEY_VALID}; use windmill_common::{ global_settings::{ @@ -307,7 +307,8 @@ async fn windmill_main() -> anyhow::Result<()> { Mode::Standalone }); - let num_workers = if mode == Mode::Server || mode == Mode::Indexer { + #[allow(unused_mut)] + let mut num_workers = if mode == Mode::Server || mode == Mode::Indexer { 0 } else { std::env::var("NUM_WORKERS") @@ -422,6 +423,50 @@ Windmill Community Edition {GIT_VERSION} display_config(&ENV_SETTINGS); + if let Err(e) = reload_base_url_setting(&db).await { + tracing::error!("Error loading base url: {:?}", e) + } + + if let Err(e) = reload_critical_error_channels_setting(&db).await { + tracing::error!("Could loading critical error emails setting: {:?}", e); + } + + #[cfg(feature = "enterprise")] + { + // load the license key and check if it's valid + // if not valid and not server mode just quit + // if not expired and server mode then force renewal + // if key still invalid and num_workers > 0, set to 0 + if let Err(err) = reload_license_key(&db).await { + tracing::error!("Failed to reload license key: {err:#}"); + } + let valid_key = *LICENSE_KEY_VALID.read().await; + if !valid_key && !server_mode { + panic!("Invalid license key, workers require a valid license key"); + } + if server_mode { + // only force renewal if invalid but not empty (= expired) + let renewed_now = maybe_renew_license_key_on_start( + &HTTP_CLIENT, + &db, + !valid_key && !LICENSE_KEY_ID.read().await.is_empty(), + ) + .await; + if renewed_now { + if let Err(err) = reload_license_key(&db).await { + tracing::error!("Failed to reload license key: {err:#}"); + } + } + if num_workers > 0 { + let valid_key = *LICENSE_KEY_VALID.read().await; + if !valid_key { + tracing::warn!("License key invalid, setting num_workers to 0"); + num_workers = 0; + } + } + } + } + let worker_mode = num_workers > 0; if server_mode || worker_mode || indexer_mode { @@ -448,7 +493,16 @@ Windmill Community Edition {GIT_VERSION} initial_load(&db, killpill_tx.clone(), worker_mode, server_mode, is_agent).await; - monitor_db(&db, &base_internal_url, rsmq.clone(), server_mode, true).await; + monitor_db( + &db, + &base_internal_url, + rsmq.clone(), + server_mode, + worker_mode, + true, + killpill_tx.clone(), + ) + .await; monitor_pool(&db).await; @@ -578,7 +632,9 @@ Windmill Community Edition {GIT_VERSION} &base_internal_url, rsmq.clone(), server_mode, - false + worker_mode, + false, + tx.clone(), ) .await; }, @@ -621,7 +677,15 @@ Windmill Community Edition {GIT_VERSION} }, LICENSE_KEY_SETTING => { if let Err(e) = reload_license_key(&db).await { - tracing::error!(error = %e, "Could not reload license key setting"); + tracing::error!("Failed to reload license key: {e:#}"); + } + #[cfg(feature = "enterprise")] + if worker_mode { + let valid_key = *LICENSE_KEY_VALID.read().await; + if !valid_key { + tracing::error!("Invalid license key, exiting..."); + tx.send(()).expect("send"); + } } }, DEFAULT_TAGS_PER_WORKSPACE_SETTING => { @@ -767,15 +831,10 @@ Windmill Community Edition {GIT_VERSION} Ok(()) as anyhow::Result<()> }; - if mode == Mode::Server || mode == Mode::Standalone { + if server_mode { schedule_stats(&db, &HTTP_CLIENT).await; } - #[cfg(feature = "enterprise")] - if mode == Mode::Server || mode == Mode::Standalone { - schedule_key_renewal(&HTTP_CLIENT, &db).await; - } - futures::try_join!( shutdown_signal, workers_f, diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 17dd833d62..0618cfd8d8 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -27,7 +27,7 @@ use windmill_api::{ DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN, }; #[cfg(feature = "enterprise")] -use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts}; +use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts, LICENSE_KEY_VALID}; use windmill_common::{ auth::JWT_SECRET, ee::CriticalErrorChannel, @@ -76,9 +76,6 @@ use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING; #[cfg(feature = "enterprise")] use crate::ee::verify_license_key; -#[cfg(feature = "enterprise")] -use windmill_common::ee::LICENSE_KEY_VALID; - use crate::ee::set_license_key; lazy_static::lazy_static! { @@ -152,18 +149,10 @@ pub async fn initial_load( tracing::error!("Error reloading custom tags: {:?}", e) } - if let Err(e) = reload_base_url_setting(db).await { - tracing::error!("Error reloading base url: {:?}", e) - } - if let Err(e) = reload_hub_base_url_setting(db, server_mode).await { tracing::error!("Error reloading hub base url: {:?}", e) } - if let Err(e) = reload_critical_error_channels_setting(&db).await { - tracing::error!("Could not reload critical error emails setting: {:?}", e); - } - if let Err(e) = reload_jwt_secret_setting(&db).await { tracing::error!("Could not reload jwt secret setting: {:?}", e); } @@ -182,11 +171,6 @@ pub async fn initial_load( reload_scim_token_setting(&db).await; } - #[cfg(feature = "enterprise")] - if let Err(e) = reload_license_key(&db).await { - tracing::error!("Error reloading license key: {:?}", e) - } - if worker_mode { reload_extra_pip_index_url_setting(&db).await; reload_pip_index_url_setting(&db).await; @@ -854,8 +838,10 @@ pub async fn reload_request_size(db: &DB) { } } -pub async fn reload_license_key(db: &DB) -> error::Result<()> { - let q = load_value_from_global_settings(db, LICENSE_KEY_SETTING).await?; +pub async fn reload_license_key(db: &DB) -> anyhow::Result<()> { + let q = load_value_from_global_settings(db, LICENSE_KEY_SETTING) + .await + .map_err(|err| anyhow::anyhow!("Error reloading license key: {}", err.to_string()))?; let mut value = std::env::var("LICENSE_KEY") .ok() @@ -873,9 +859,7 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> { tracing::error!("Could not parse LICENSE_KEY found: {:#?}", &q); } }; - - set_license_key(value).await?; - + set_license_key(value).await; Ok(()) } @@ -1014,7 +998,9 @@ pub async fn monitor_db( base_internal_url: &str, rsmq: Option, server_mode: bool, + _worker_mode: bool, initial_load: bool, + _killpill_tx: tokio::sync::broadcast::Sender<()>, ) { let zombie_jobs_f = async { if server_mode && !initial_load { @@ -1035,15 +1021,14 @@ pub async fn monitor_db( let verify_license_key_f = async { #[cfg(feature = "enterprise")] - if let Err(e) = verify_license_key().await { - tracing::error!("Error verifying license key: {:?}", e); - let mut l = LICENSE_KEY_VALID.write().await; - *l = false; - } else { - let is_valid = LICENSE_KEY_VALID.read().await.clone(); - if !is_valid { - let mut l = LICENSE_KEY_VALID.write().await; - *l = true; + if !initial_load { + verify_license_key().await; + if _worker_mode { + let valid_key = *LICENSE_KEY_VALID.read().await; + if !valid_key { + tracing::error!("Invalid license key, exiting..."); + _killpill_tx.send(()).expect("send"); + } } } }; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1fcae0cb9f..d8ab6a8696 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1467,6 +1467,11 @@ paths: parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" + - name: active_only + in: query + description: filter only active users + schema: + type: boolean responses: "200": description: user @@ -11791,6 +11796,8 @@ components: type: string username: type: string + operator_only: + type: boolean required: - email diff --git a/backend/windmill-api/src/ee.rs b/backend/windmill-api/src/ee.rs index b20521e8b4..cddb639e95 100644 --- a/backend/windmill-api/src/ee.rs +++ b/backend/windmill-api/src/ee.rs @@ -4,7 +4,7 @@ use std::sync::Arc; #[cfg(feature = "enterprise")] use tokio::sync::RwLock; -pub async fn validate_license_key(_license_key: String) -> anyhow::Result { +pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> { // Implementation is not open source Err(anyhow!("License can't be validated in Windmill CE")) } diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 45a0ea7196..dc9dbd2529 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -456,9 +456,13 @@ async fn ee_license() -> &'static str { #[cfg(feature = "enterprise")] async fn ee_license() -> String { - use windmill_common::ee::LICENSE_KEY_ID; + use windmill_common::ee::{LICENSE_KEY_ID, LICENSE_KEY_VALID}; - LICENSE_KEY_ID.read().await.clone() + if *LICENSE_KEY_VALID.read().await { + LICENSE_KEY_ID.read().await.clone() + } else { + "".to_string() + } } async fn openapi() -> &'static str { diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 8a72a5459a..e8c000c788 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -163,8 +163,13 @@ pub async fn test_license_key( Json(TestKey { license_key }): Json, ) -> error::Result { require_super_admin(&db, &authed.email).await?; - validate_license_key(license_key).await?; - Ok("Sent test email".to_string()) + let (_, expired) = validate_license_key(license_key).await?; + + if expired { + Err(error::Error::BadRequest("Expired license key".to_string())) + } else { + Ok("Valid license key".to_string()) + } } pub async fn get_local_settings( @@ -301,7 +306,6 @@ pub async fn send_stats(Extension(db): Extension, authed: ApiAuthed) -> Resu windmill_common::stats_ee::send_stats( &HTTP_CLIENT, &db, - true, windmill_common::stats_ee::SendStatsReason::Manual, ) .await?; diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index f753e4d9d2..d3dabcc68c 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -789,6 +789,8 @@ pub struct GlobalUserInfo { name: Option, company: Option, username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + operator_only: Option, } #[derive(Serialize, Debug)] @@ -1001,24 +1003,48 @@ async fn list_user_usage( Ok(Json(rows)) } +#[derive(Deserialize)] +struct ActiveUsersOnly { + active_only: Option, +} + async fn list_users_as_super_admin( authed: ApiAuthed, Extension(db): Extension, Query(pagination): Query, + Query(ActiveUsersOnly { active_only }): Query, ) -> JsonResult> { require_super_admin(&db, &authed.email).await?; let per_page = pagination.per_page.unwrap_or(10000).max(1); let offset = (pagination.page.unwrap_or(1).max(1) - 1) * per_page; - let rows = sqlx::query_as!( - GlobalUserInfo, - "SELECT email, login_type::text, verified, super_admin, name, company, username from password ORDER BY super_admin DESC, email LIMIT \ - $1 OFFSET $2", - per_page as i32, - offset as i32 - ) - .fetch_all(&db) - .await?; + let rows = if active_only.is_some_and(|x| x) { + sqlx::query_as!( + 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 + FROM password + WHERE email IN (SELECT email FROM active_users) + ORDER BY super_admin DESC + LIMIT $1 OFFSET $2", + per_page as i32, + offset as i32 + ) + .fetch_all(&db) + .await? + } 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 \ + $1 OFFSET $2", + per_page as i32, + offset as i32 + ) + .fetch_all(&db) + .await? + }; + Ok(Json(rows)) } @@ -1173,7 +1199,7 @@ async fn global_whoami( ) -> JsonResult { let user = sqlx::query_as!( GlobalUserInfo, - "SELECT email, login_type::TEXT, super_admin, verified, name, company, username FROM password WHERE \ + "SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE \ email = $1", email ) @@ -1192,6 +1218,7 @@ async fn global_whoami( name: None, company: None, username: None, + operator_only: None, })) } else { Err(user.unwrap_err()) diff --git a/backend/windmill-common/src/ee.rs b/backend/windmill-common/src/ee.rs index 6df93aca78..e25d36bee1 100644 --- a/backend/windmill-common/src/ee.rs +++ b/backend/windmill-common/src/ee.rs @@ -48,8 +48,13 @@ pub async fn send_critical_alert( } #[cfg(feature = "enterprise")] -pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () { +pub async fn maybe_renew_license_key_on_start( + _http_client: &reqwest::Client, + _db: &crate::db::DB, + force_renew_now: bool, +) -> bool { // Implementation is not open source + force_renew_now } #[cfg(feature = "enterprise")] diff --git a/backend/windmill-common/src/stats_ee.rs b/backend/windmill-common/src/stats_ee.rs index c6dc9946bf..5d2dc82b82 100644 --- a/backend/windmill-common/src/stats_ee.rs +++ b/backend/windmill-common/src/stats_ee.rs @@ -28,7 +28,6 @@ pub enum SendStatsReason { pub async fn send_stats( _http_client: &reqwest::Client, _db: &DB, - _skip_job_usage: bool, _reason: SendStatsReason, ) -> Result<()> { // stats details are closed source diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index d5f9c5c099..3feab853dd 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -206,6 +206,14 @@ pub async fn get_uid<'c, E: sqlx::Executor<'c, Database = Postgres>>(db: E) -> R Ok(uid) } +pub fn map_string_to_number(s: &str, max_number: u64) -> u64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + hasher.finish() % (max_number + 1) +} + #[derive(Serialize, Deserialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] pub enum Mode { diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index c924fc8b3d..8709a9f478 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -277,9 +277,7 @@
Setting SMTP unlocks sending emails upon adding new users to the workspace or the instance or sending critical alerts. - Learn moreLearn more
{:else if category == 'Registries'} @@ -653,24 +651,33 @@ {@const attemptedAt = new Date( latestKeyRenewalAttempt.attempted_at ).toLocaleString()} + {@const isTrial = + latestKeyRenewalAttempt.result.startsWith('error: trial:')}
{#if latestKeyRenewalAttempt.result === 'success'} {:else} - + {/if} {latestKeyRenewalAttempt.result === 'success' ? 'Latest key renewal succeeded' + : isTrial + ? 'Latest key renewal ignored because in trial' : 'Latest key renewal failed'} on {attemptedAt} @@ -680,6 +687,10 @@ Latest key renewal succeeded on {attemptedAt} + {:else if isTrial} + + License key cannot be renewed during trial ({attemptedAt}) + {:else} Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace( diff --git a/frontend/src/lib/components/SuperadminSettings.svelte b/frontend/src/lib/components/SuperadminSettings.svelte index 498c708cec..70033df7a5 100644 --- a/frontend/src/lib/components/SuperadminSettings.svelte +++ b/frontend/src/lib/components/SuperadminSettings.svelte @@ -22,11 +22,12 @@ import ChangeInstanceUsername from './ChangeInstanceUsername.svelte' import { isCloudHosted } from '$lib/cloud' import InstanceNameEditor from './InstanceNameEditor.svelte' + import Toggle from './Toggle.svelte' let drawer: Drawer let filter = '' export function openDrawer() { - listUsers() + listUsers(activeOnly) drawer?.openDrawer?.() } @@ -45,11 +46,14 @@ let users: GlobalUserInfo[] = [] let filteredUsers: GlobalUserInfo[] = [] let deleteConfirmedCallback: (() => void) | undefined = undefined + let activeOnly = false - async function listUsers(): Promise { - users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 }) + async function listUsers(activeOnly: boolean): Promise { + users = await UserService.listUsersAsSuperAdmin({ perPage: 100000, activeOnly: activeOnly }) } + $: listUsers(activeOnly) + let tab: 'users' | string = 'users' let nbDisplayed = 50 @@ -70,7 +74,7 @@ }) getAutomateUsernameCreationSetting() sendUserToast('Automatic username creation enabled') - listUsers() + listUsers(activeOnly) } async function updateName(name: string | undefined, email: string) { @@ -82,7 +86,7 @@ } }) sendUserToast('User updated') - listUsers() + listUsers(activeOnly) } catch (e) { sendUserToast('Error updating user', true) } @@ -96,7 +100,12 @@ f={(x) => x.email + ' ' + x.name + ' ' + x.company} /> - + listUsers(activeOnly)} + size="1200px" + on:close={removeHash} +>
@@ -130,11 +139,16 @@
{#if !automateUsernameCreation && !isCloudHosted()}
-

- Automatic username creation -

+

Automatic username creation

- Automatically create a username for new users based on their email, shared across workspaces. Learn more + Automatically create a username for new users based on their email, shared + across workspaces. Learn more
-

All instance users

+
+

All instance users

+ +
@@ -181,12 +205,15 @@ {#if automateUsernameCreation} username {/if} + {#if activeOnly} + kind + {/if} {#if filteredUsers && users} - {#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username } (email)} + {#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username, operator_only } (email)} {email} {login_type} @@ -203,13 +230,22 @@ {email} isConflict on:renamed={() => { - listUsers() + listUsers(activeOnly) }} /> {/key} {/if} {/if} + {#if activeOnly} + + {#if operator_only} + Operator only + {:else} + Developer + {/if} + + {/if} @@ -244,7 +280,7 @@ updateName(e.detail, email) }} on:renamed={() => { - listUsers() + listUsers(activeOnly) }} {automateUsernameCreation} /> @@ -258,7 +294,7 @@ deleteConfirmedCallback = async () => { await UserService.globalUserDelete({ email }) sendUserToast(`User ${email} removed`) - listUsers() + listUsers(activeOnly) } }} >