mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-04 16:03:06 +00:00
improve key renewal (#4519)
* improve key renewal * sqlx and fix * fix build * improve everything * nits * nits * update ee ref * nit
This commit is contained in:
-20
@@ -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"
|
||||
}
|
||||
+9
-3
@@ -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"
|
||||
}
|
||||
+9
-3
@@ -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"
|
||||
}
|
||||
+65
@@ -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"
|
||||
}
|
||||
-22
@@ -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"
|
||||
}
|
||||
-20
@@ -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"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
ee638ab91623392cdbf6a139f2b54b9b527585ea
|
||||
f667fcebdb5a2d1c22c1928d6a316ba57bb43d72
|
||||
+2
-10
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
+70
-11
@@ -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,
|
||||
|
||||
+16
-31
@@ -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<MultiplexedRsmq>,
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> {
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -163,8 +163,13 @@ pub async fn test_license_key(
|
||||
Json(TestKey { license_key }): Json<TestKey>,
|
||||
) -> error::Result<String> {
|
||||
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<DB>, authed: ApiAuthed) -> Resu
|
||||
windmill_common::stats_ee::send_stats(
|
||||
&HTTP_CLIENT,
|
||||
&db,
|
||||
true,
|
||||
windmill_common::stats_ee::SendStatsReason::Manual,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -789,6 +789,8 @@ pub struct GlobalUserInfo {
|
||||
name: Option<String>,
|
||||
company: Option<String>,
|
||||
username: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
operator_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -1001,24 +1003,48 @@ async fn list_user_usage(
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActiveUsersOnly {
|
||||
active_only: Option<bool>,
|
||||
}
|
||||
|
||||
async fn list_users_as_super_admin(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
Query(ActiveUsersOnly { active_only }): Query<ActiveUsersOnly>,
|
||||
) -> JsonResult<Vec<GlobalUserInfo>> {
|
||||
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<GlobalUserInfo> {
|
||||
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())
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -277,9 +277,7 @@
|
||||
<div class="text-secondary pb-4 text-xs"
|
||||
>Setting SMTP unlocks sending emails upon adding new users to the workspace or the
|
||||
instance or sending critical alerts.
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://www.windmill.dev/docs/advanced/instance_settings#smtp">Learn more</a
|
||||
<a target="_blank" href="https://www.windmill.dev/docs/misc/setup_smtp">Learn more</a
|
||||
></div
|
||||
>
|
||||
{:else if category == 'Registries'}
|
||||
@@ -653,24 +651,33 @@
|
||||
{@const attemptedAt = new Date(
|
||||
latestKeyRenewalAttempt.attempted_at
|
||||
).toLocaleString()}
|
||||
{@const isTrial =
|
||||
latestKeyRenewalAttempt.result.startsWith('error: trial:')}
|
||||
<div class="relative">
|
||||
<Popover notClickable>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
{#if latestKeyRenewalAttempt.result === 'success'}
|
||||
<BadgeCheck class="text-green-600" size={12} />
|
||||
{:else}
|
||||
<BadgeX class="text-red-600" size={12} />
|
||||
<BadgeX
|
||||
class={isTrial ? 'text-yellow-600' : 'text-red-600'}
|
||||
size={12}
|
||||
/>
|
||||
{/if}
|
||||
<span
|
||||
class={classNames(
|
||||
'text-xs',
|
||||
latestKeyRenewalAttempt.result === 'success'
|
||||
? 'text-green-600'
|
||||
: isTrial
|
||||
? 'text-yellow-600'
|
||||
: 'text-red-600'
|
||||
)}
|
||||
>
|
||||
{latestKeyRenewalAttempt.result === 'success'
|
||||
? 'Latest key renewal succeeded'
|
||||
: isTrial
|
||||
? 'Latest key renewal ignored because in trial'
|
||||
: 'Latest key renewal failed'}
|
||||
on {attemptedAt}
|
||||
</span>
|
||||
@@ -680,6 +687,10 @@
|
||||
<span class="text-green-300">
|
||||
Latest key renewal succeeded on {attemptedAt}
|
||||
</span>
|
||||
{:else if isTrial}
|
||||
<span class="text-yellow-300">
|
||||
License key cannot be renewed during trial ({attemptedAt})
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-red-300">
|
||||
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace(
|
||||
|
||||
@@ -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<void> {
|
||||
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 })
|
||||
async function listUsers(activeOnly: boolean): Promise<void> {
|
||||
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}
|
||||
/>
|
||||
|
||||
<Drawer bind:this={drawer} on:open={listUsers} size="1200px" on:close={removeHash}>
|
||||
<Drawer
|
||||
bind:this={drawer}
|
||||
on:open={() => listUsers(activeOnly)}
|
||||
size="1200px"
|
||||
on:close={removeHash}
|
||||
>
|
||||
<DrawerContent overflow_y={true} title="Instance settings" on:close={closeDrawer}>
|
||||
<div class="flex flex-col h-full w-full">
|
||||
<div>
|
||||
@@ -130,11 +139,16 @@
|
||||
<div class="h-full">
|
||||
{#if !automateUsernameCreation && !isCloudHosted()}
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-2">
|
||||
Automatic username creation
|
||||
</h3>
|
||||
<h3 class="mb-2"> Automatic username creation </h3>
|
||||
<div class="mb-2">
|
||||
<span class="text-primary text-sm">Automatically create a username for new users based on their email, shared across workspaces. <a target="_blank" href="https://www.windmill.dev/docs/advanced/instance_settings#automatic-username-creation">Learn more</a></span>
|
||||
<span class="text-primary text-sm"
|
||||
>Automatically create a username for new users based on their email, shared
|
||||
across workspaces. <a
|
||||
target="_blank"
|
||||
href="https://www.windmill.dev/docs/advanced/instance_settings#automatic-username-creation"
|
||||
>Learn more</a
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<Button
|
||||
btnClasses="w-auto"
|
||||
@@ -164,10 +178,20 @@
|
||||
{/if}
|
||||
|
||||
<div class="py-2 mb-4">
|
||||
<InviteGlobalUser on:new={listUsers} />
|
||||
<InviteGlobalUser on:new={() => listUsers(activeOnly)} />
|
||||
</div>
|
||||
|
||||
<h3>All instance users</h3>
|
||||
<div class="flex flex-row justify-between">
|
||||
<h3>All instance users</h3>
|
||||
<Toggle
|
||||
bind:checked={activeOnly}
|
||||
options={{
|
||||
left: 'Show active users only',
|
||||
leftTooltip:
|
||||
'An active user is a user who has performed at least one action in the last 30 days'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="pb-1" />
|
||||
<div>
|
||||
<input placeholder="Search users" bind:value={filter} class="input mt-1" />
|
||||
@@ -181,12 +205,15 @@
|
||||
{#if automateUsernameCreation}
|
||||
<th>username</th>
|
||||
{/if}
|
||||
{#if activeOnly}
|
||||
<th>kind</th>
|
||||
{/if}
|
||||
<th />
|
||||
<th />
|
||||
</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 } (email)}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username, operator_only } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{login_type}</td>
|
||||
@@ -203,13 +230,22 @@
|
||||
{email}
|
||||
isConflict
|
||||
on:renamed={() => {
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
{#if activeOnly}
|
||||
<td>
|
||||
{#if operator_only}
|
||||
Operator only
|
||||
{:else}
|
||||
Developer
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td>
|
||||
<ToggleButtonGroup
|
||||
selected={super_admin}
|
||||
@@ -217,7 +253,7 @@
|
||||
console.log('BAR')
|
||||
if (email == $userStore?.email) {
|
||||
sendUserToast('You cannot demote yourself', true)
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
return
|
||||
}
|
||||
await UserService.globalUserUpdate({
|
||||
@@ -227,7 +263,7 @@
|
||||
}
|
||||
})
|
||||
sendUserToast('User updated')
|
||||
listUsers()
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={false} size="xs" label="User" />
|
||||
@@ -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)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user