From 610cd70a136f27b4fa26e46cf667fcd94875fafb Mon Sep 17 00:00:00 2001 From: Hengfei Yang Date: Thu, 8 Oct 2026 09:56:01 +0000 Subject: [PATCH] fix: generate and store the ext auth salt on a new install instead of refusing to start (#15191) --- .env.example | 2 - .github/workflows/api-testing.yml | 1 - .github/workflows/db-testing.yml | 1 - .github/workflows/mobile-sdk-e2e.yml | 1 - .github/workflows/playwright.yml | 1 - .../playwright_bugfix_regression.yml | 1 - .github/workflows/unit-tests.yml | 2 - CONTRIBUTING.md | 5 +- README.md | 2 - deploy/k8s/statefulset.yaml | 3 - src/api/common/src/auth/validator.rs | 20 +- src/api/management/src/request/users/mod.rs | 2 +- src/config/src/config.rs | 38 ++- .../src/alerts/notifications/chart/mod.rs | 2 +- .../src/alerts/notifications/chart/payload.rs | 7 +- src/core/src/bootstrap.rs | 248 ++++++++++++++++-- src/core/src/users.rs | 14 +- src/db/src/metas.rs | 49 +++- src/infra/src/lib.rs | 9 - src/migration/mod.rs | 137 ---------- .../test_sqlite_invalid_utf8_meta_startup.py | 1 - tests/fresh_install_salt.rs | 47 ---- tests/integration_test.rs | 1 - 23 files changed, 320 insertions(+), 274 deletions(-) delete mode 100644 tests/fresh_install_salt.rs diff --git a/.env.example b/.env.example index ba2da7bc46..31a6c09977 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,5 @@ ZO_ROOT_USER_EMAIL = "root@example.com" ZO_ROOT_USER_PASSWORD = "Complexpass#123" -# required on a new install: 16 to 48 random bytes, e.g. from `openssl rand -hex 24` -ZO_EXT_AUTH_SALT = "" ZO_MAX_FILE_SIZE_ON_DISK = 32 ZO_FILE_PUSH_INTERVAL = 10 ZO_S3_BUCKET_NAME = "zinc-dev" diff --git a/.github/workflows/api-testing.yml b/.github/workflows/api-testing.yml index e38cac139a..ebd6651a68 100644 --- a/.github/workflows/api-testing.yml +++ b/.github/workflows/api-testing.yml @@ -20,7 +20,6 @@ env: COLUMNS: 150 ZO_ROOT_USER_EMAIL: root@example.com ZO_ROOT_USER_PASSWORD: Complexpass#123 - ZO_EXT_AUTH_SALT: ci-only-ext-auth-salt-0123456789abcdef ZO_BASE_URL: http://localhost:5080/ ZO_BASE_URL_SC: http://localhost:5080/ WS_ZO_BASE_URL: ws://localhost:5080/ diff --git a/.github/workflows/db-testing.yml b/.github/workflows/db-testing.yml index 8825d8746e..bc567ab115 100644 --- a/.github/workflows/db-testing.yml +++ b/.github/workflows/db-testing.yml @@ -20,7 +20,6 @@ env: COLUMNS: 150 ZO_ROOT_USER_EMAIL: root@example.com ZO_ROOT_USER_PASSWORD: Complexpass#123 - ZO_EXT_AUTH_SALT: ci-only-ext-auth-salt-0123456789abcdef ZO_BASE_URL: http://localhost:5080/ ZO_INTERNAL_GRPC_TOKEN: fake_token_for_testing ZO_MEM_PERSIST_INTERVAL: 1 diff --git a/.github/workflows/mobile-sdk-e2e.yml b/.github/workflows/mobile-sdk-e2e.yml index b91a9e5734..0d1411d36d 100644 --- a/.github/workflows/mobile-sdk-e2e.yml +++ b/.github/workflows/mobile-sdk-e2e.yml @@ -84,7 +84,6 @@ env: # Non-secret defaults for a fresh local OpenObserve (identical to the other test workflows). ZO_ROOT_USER_EMAIL: root@example.com ZO_ROOT_USER_PASSWORD: Complexpass#123 - ZO_EXT_AUTH_SALT: ci-only-ext-auth-salt-0123456789abcdef # RUM-ingestion parity with the proven playwright.yml suite. Without these a fresh # OpenObserve can silently reject/misfile RUM ingest (loopback SSRF guard, missing org, # dynamic schema, past-timestamp window). The iOS binary inherits these from the job env; diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f6e981bdfd..f6960415ba 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -21,7 +21,6 @@ env: COLUMNS: 150 ZO_ROOT_USER_EMAIL: root@example.com ZO_ROOT_USER_PASSWORD: Complexpass#123 - ZO_EXT_AUTH_SALT: ci-only-ext-auth-salt-0123456789abcdef ZO_BASE_URL: http://localhost:5080 WS_ZO_BASE_URL: ws://localhost:5080 ZO_BASE_URL_SC: http://localhost:5080 diff --git a/.github/workflows/playwright_bugfix_regression.yml b/.github/workflows/playwright_bugfix_regression.yml index 0a021616dc..1c3b20c56b 100644 --- a/.github/workflows/playwright_bugfix_regression.yml +++ b/.github/workflows/playwright_bugfix_regression.yml @@ -18,7 +18,6 @@ env: COLUMNS: 150 ZO_ROOT_USER_EMAIL: root@example.com ZO_ROOT_USER_PASSWORD: Complexpass#123 - ZO_EXT_AUTH_SALT: ci-only-ext-auth-salt-0123456789abcdef ZO_BASE_URL: http://localhost:5080 WS_ZO_BASE_URL: ws://localhost:5080 ZO_BASE_URL_SC: http://localhost:5080 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1098c1d308..9f3a99821a 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -112,7 +112,6 @@ jobs: contents: read env: ZO_INTERNAL_GRPC_TOKEN: fake - ZO_EXT_AUTH_SALT: ci-only-ext-auth-salt-0123456789abcdef ZO_TEXT_DATA_TYPE: text ZO_META_STORE: postgres ZO_META_POSTGRES_DSN: postgres://postgres:password@localhost:5432/postgres @@ -187,7 +186,6 @@ jobs: contents: read env: ZO_INTERNAL_GRPC_TOKEN: fake - ZO_EXT_AUTH_SALT: ci-only-ext-auth-salt-0123456789abcdef ZO_TEXT_DATA_TYPE: text ZO_META_STORE: sqlite ZO_META_POSTGRES_DSN: "" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 238fc88482..41c9fa5a3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,18 +89,17 @@ Create a `.env` file in the root `openobserve` directory (if not already present ```env ZO_WEB_URL="http://localhost:8081" ZO_CORS_ALLOWED_ORIGINS="http://localhost:3000, http://localhost:5173, http://localhost:8081" -ZO_EXT_AUTH_SALT="" ``` Start the backend server: ```shell -ZO_ROOT_USER_EMAIL="root@example.com" ZO_ROOT_USER_PASSWORD="Complexpass#123" ZO_EXT_AUTH_SALT="<16 to 48 random bytes, e.g. from openssl rand -hex 24>" cargo run +ZO_ROOT_USER_EMAIL="root@example.com" ZO_ROOT_USER_PASSWORD="Complexpass#123" cargo run ``` This will start the openobserve API server on port 5080 -environment variables ZO_ROOT_USER_EMAIL and ZO_ROOT_USER_PASSWORD can be used first time to configure default admin user when openobserve is started. A new install refuses to start until ZO_EXT_AUTH_SALT is set to a random secret of 16 to 48 bytes (for example `openssl rand -hex 24`); keep the same value across restarts and nodes. +environment variables ZO_ROOT_USER_EMAIL and ZO_ROOT_USER_PASSWORD can be used first time to configure default admin user when openobserve is started. A new install generates the salt for presigned and ext-token logins and stores it in the meta db; set ZO_EXT_AUTH_SALT to a random secret of 16 to 48 bytes (for example `openssl rand -hex 24`) only if you want it kept out of the db, and then keep the same value across restarts and nodes. ### UI diff --git a/README.md b/README.md index 64a08f8728..539721a12f 100644 --- a/README.md +++ b/README.md @@ -81,14 +81,12 @@ Get started in minutes without managing infrastructure. The free tier includes u ### 🐳 Docker ```bash -export ZO_EXT_AUTH_SALT="$(openssl rand -hex 24)" # save it: the same value must be used on every restart docker run -d \ --name openobserve \ -v $PWD/data:/data \ -p 5080:5080 \ -e ZO_ROOT_USER_EMAIL="root@example.com" \ -e ZO_ROOT_USER_PASSWORD="Complexpass#123" \ - -e ZO_EXT_AUTH_SALT="$ZO_EXT_AUTH_SALT" \ public.ecr.aws/zinclabs/openobserve:latest ``` diff --git a/deploy/k8s/statefulset.yaml b/deploy/k8s/statefulset.yaml index 77fa386739..e58ecea936 100644 --- a/deploy/k8s/statefulset.yaml +++ b/deploy/k8s/statefulset.yaml @@ -51,9 +51,6 @@ spec: value: root@example.com - name: ZO_ROOT_USER_PASSWORD value: Complexpass#123 - # required on a new install: 16 to 48 random bytes, e.g. from `openssl rand -hex 24` - - name: ZO_EXT_AUTH_SALT - value: "" - name: ZO_DATA_DIR value: /data # command: ["/bin/bash", "-c", "while true; do sleep 1; done"] diff --git a/src/api/common/src/auth/validator.rs b/src/api/common/src/auth/validator.rs index 6d95b3c1ba..d500275084 100644 --- a/src/api/common/src/auth/validator.rs +++ b/src/api/common/src/auth/validator.rs @@ -757,8 +757,7 @@ pub async fn validate_credentials_ext( auth_token: AuthTokensExt, method: &str, ) -> Result { - let cfg = get_config(); - let password_ext_salt = cfg.auth.ext_auth_salt.as_str(); + let password_ext_salt = &config::get_ext_auth_salt(); // Strip leading slash if present let path = path.strip_prefix('/').unwrap_or(path); let mut path_columns = path.split('/').collect::>(); @@ -1042,8 +1041,14 @@ pub async fn validate_user( let db_user = db::user::get_user_record(user_id) .await .map(|user| DBUser::from(&user)); - let cfg = get_config(); - validate_user_from_db(db_user, user_password, None, 0, &cfg.auth.ext_auth_salt).await + validate_user_from_db( + db_user, + user_password, + None, + 0, + &config::get_ext_auth_salt(), + ) + .await } pub async fn validate_user_for_query_params( @@ -1053,13 +1058,12 @@ pub async fn validate_user_for_query_params( exp_in: i64, ) -> Result { let db_user = db::user::get_db_user(user_id).await; - let cfg = get_config(); validate_user_from_db( db_user, user_password, req_time, exp_in, - &cfg.auth.ext_auth_salt, + &config::get_ext_auth_salt(), ) .await } @@ -2097,7 +2101,7 @@ mod tests { .is_valid ); assert!(validate_user(init_user, pwd).await.unwrap().is_valid); - let stored_ext = get_hash(pwd, &get_config().auth.ext_auth_salt); + let stored_ext = get_hash(pwd, &config::get_ext_auth_salt()); for (path, method) in [ ("default/_bulk", Method::POST), ("default/streams", Method::GET), @@ -2253,7 +2257,7 @@ mod tests { request_time: 1_700_000_000, expires_in: 300, }; - let salt = get_config().auth.ext_auth_salt.clone(); + let salt = config::get_ext_auth_salt(); let unbacked = password_ext_credential("", "1700000000", 300, &salt); assert!( !validate_credentials_ext(email, &unbacked, "default/streams", auth_token, "GET") diff --git a/src/api/management/src/request/users/mod.rs b/src/api/management/src/request/users/mod.rs index 17678232e4..d4e761a105 100644 --- a/src/api/management/src/request/users/mod.rs +++ b/src/api/management/src/request/users/mod.rs @@ -848,7 +848,7 @@ pub async fn get_presigned_url( let cfg = get_config(); let time = chrono::Utc::now().timestamp(); - let password_ext_salt = cfg.auth.ext_auth_salt.as_str(); + let password_ext_salt = &config::get_ext_auth_salt(); let base_url = format!("{}{}", cfg.common.web_url, cfg.common.base_uri); let url = generate_presigned_url( diff --git a/src/config/src/config.rs b/src/config/src/config.rs index 9ef9eefbd9..133e9d4556 100644 --- a/src/config/src/config.rs +++ b/src/config/src/config.rs @@ -412,6 +412,9 @@ pub static NATS_KV_WATCH_MODULES: Lazy> = Lazy::new(|| { pub static CONFIG: Lazy> = Lazy::new(|| ArcSwap::from(Arc::new(init()))); static INSTANCE_ID: Lazy> = Lazy::new(Default::default); static STORED_GRPC_TOKEN: Lazy> = Lazy::new(Default::default); +static STORED_EXT_AUTH_SALT: Lazy> = Lazy::new(Default::default); +/// Installs older than the stored salt hash with this value: rotating it breaks their logins. +pub const LEGACY_EXT_AUTH_SALT: &str = "openobserve"; pub fn get_config() -> Arc { CONFIG.load().clone() @@ -577,6 +580,30 @@ pub fn get_stored_grpc_token() -> String { STORED_GRPC_TOKEN.load().to_string() } +/// Caches the ext auth salt stored in the meta db; empty means none is stored. +pub fn cache_stored_ext_auth_salt(salt: &str) { + STORED_EXT_AUTH_SALT.store(Arc::new(salt.to_owned())); +} + +pub fn get_stored_ext_auth_salt() -> String { + STORED_EXT_AUTH_SALT.load().to_string() +} + +pub fn get_ext_auth_salt() -> String { + select_ext_auth_salt( + &get_config().auth.ext_auth_salt, + &get_stored_ext_auth_salt(), + ) +} + +fn select_ext_auth_salt(env_salt: &str, stored_salt: &str) -> String { + [env_salt, stored_salt] + .into_iter() + .find(|s| !s.is_empty()) + .unwrap_or(LEGACY_EXT_AUTH_SALT) + .to_string() +} + pub fn calculate_config_file_hash(path: &PathBuf) -> Result { let content = std::fs::read_to_string(path)?; Ok(digest(content)) @@ -1488,8 +1515,8 @@ pub struct Auth { pub cookie_same_site_lax: bool, #[env_config(name = "ZO_COOKIE_SECURE_ONLY", default = false)] pub cookie_secure_only: bool, - /// Secret for presigned and ext-token logins; a new install refuses to start with the default. - #[env_config(name = "ZO_EXT_AUTH_SALT", default = "openobserve")] + /// Empty: a new install generates one and stores it in the meta db. + #[env_config(name = "ZO_EXT_AUTH_SALT", default = "")] pub ext_auth_salt: String, #[env_config( name = "ZO_ALERT_CHART_SIGNING_KEY", @@ -6362,4 +6389,11 @@ mod tests { let result = deverbatim(p); assert_eq!(result, r"C:\data\openobserve"); } + + #[test] + fn test_select_ext_auth_salt_prefers_env_then_stored_then_legacy() { + assert_eq!(select_ext_auth_salt("env", "stored"), "env"); + assert_eq!(select_ext_auth_salt("", "stored"), "stored"); + assert_eq!(select_ext_auth_salt("", ""), LEGACY_EXT_AUTH_SALT); + } } diff --git a/src/core/src/alerts/notifications/chart/mod.rs b/src/core/src/alerts/notifications/chart/mod.rs index cdc73a8110..0122a7b625 100644 --- a/src/core/src/alerts/notifications/chart/mod.rs +++ b/src/core/src/alerts/notifications/chart/mod.rs @@ -52,7 +52,7 @@ pub async fn signing_key() -> Option<&'static [u8]> { SIGNING_KEY .get_or_try_init(|| async { let cfg = config::get_config(); - let salt = cfg.auth.ext_auth_salt.as_str(); + let salt = &config::get_ext_auth_salt(); if !cfg.auth.alert_chart_signing_key.is_empty() { return Ok(payload::derive_signing_key( &cfg.auth.alert_chart_signing_key, diff --git a/src/core/src/alerts/notifications/chart/payload.rs b/src/core/src/alerts/notifications/chart/payload.rs index 933ba2cdf7..ec91845e78 100644 --- a/src/core/src/alerts/notifications/chart/payload.rs +++ b/src/core/src/alerts/notifications/chart/payload.rs @@ -73,12 +73,7 @@ pub struct ChartPayload { pub trigger_ts: u64, } -/// Derive the chart-signing key from per-instance secret material (IKM) and -/// the deployment salt. HKDF-extract (one HMAC application, salt as key) with -/// a versioned domain-separation label — sufficient without a new dependency. -/// -/// Callers must fail closed on empty IKM: `ZO_EXT_AUTH_SALT` defaults to the -/// public constant "openobserve", so the salt alone MUST NOT produce a key. +/// Callers must fail closed on empty IKM: the public legacy salt alone must not produce a key. pub fn derive_signing_key(ikm: &str, salt: &str) -> Vec { let mut mac = Hmac::::new_from_slice(salt.as_bytes()).expect("HMAC accepts any key length"); diff --git a/src/core/src/bootstrap.rs b/src/core/src/bootstrap.rs index 14c6113777..149cba0153 100644 --- a/src/core/src/bootstrap.rs +++ b/src/core/src/bootstrap.rs @@ -16,16 +16,25 @@ use std::sync::Arc; use ::common::infra::wal; -use config::{cache_instance_id, cache_stored_grpc_token, get_config, ider}; +use config::{ + LEGACY_EXT_AUTH_SALT, cache_instance_id, cache_stored_ext_auth_salt, cache_stored_grpc_token, + get_config, ider, +}; use db::metas; #[cfg(feature = "enterprise")] use crate::self_reporting::CoreAuditPublisher; use crate::self_reporting::persistence::CoreBatchPublisher; +const MIN_EXT_AUTH_SALT_LEN: usize = 16; +// argon2 hashes with a salt of 8 to 48 bytes and get_hash panics outside that +const MIN_ARGON2_SALT_LEN: usize = 8; +const MAX_EXT_AUTH_SALT_LEN: usize = 48; + struct InstanceIdentity { instance_id: String, grpc_token: Option, + ext_auth_salt: Option, } struct CoreOrganizationProvisioner; @@ -60,13 +69,21 @@ impl schema::OrganizationProvisioner for CoreOrganizationProvisioner { /// The instance id must stay stable for the lifetime of the deployment: /// generate a new one only when it is genuinely absent, never because the db /// errored on the read. -async fn get_or_create_instance_identity() -> Result { +async fn get_or_create_instance_identity( + env_ext_auth_salt: &str, +) -> Result { const MAX_RETRIES: usize = 5; let mut last_err = None; for attempt in 1..=MAX_RETRIES { match read_instance_identity().await { - Ok(Some(identity)) => return Ok(identity), - Ok(None) => return create_instance_identity().await, + Ok(Some(identity)) => { + check_ext_auth_salt(false, env_ext_auth_salt)?; + return Ok(identity); + } + Ok(None) => { + check_ext_auth_salt(true, env_ext_auth_salt)?; + return create_instance_identity(env_ext_auth_salt).await; + } Err(e) => { log::warn!( "error in getting instance id (attempt {attempt}/{MAX_RETRIES}): {e}, retrying..." @@ -86,33 +103,88 @@ async fn read_instance_identity() -> infra::errors::Result Result { +/// Existing installs only warn on a weak usable salt: rotating it breaks logins derived from it. +fn check_ext_auth_salt(fresh_install: bool, salt: &str) -> anyhow::Result<()> { + // unset: a new install generates and stores one, an existing one keeps the legacy salt + if salt.is_empty() { + return Ok(()); + } + if salt.len() > MAX_EXT_AUTH_SALT_LEN { + return Err(anyhow::anyhow!( + "ZO_EXT_AUTH_SALT is {} bytes, but argon2 accepts at most {MAX_EXT_AUTH_SALT_LEN}; set it to a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes (for example `openssl rand -hex 24`)", + salt.len() + )); + } + if salt != LEGACY_EXT_AUTH_SALT && salt.len() >= MIN_EXT_AUTH_SALT_LEN { + return Ok(()); + } + if fresh_install { + return Err(anyhow::anyhow!( + "ZO_EXT_AUTH_SALT is the public legacy salt or shorter than {MIN_EXT_AUTH_SALT_LEN} bytes; on a new install set a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes (for example `openssl rand -hex 24`) kept the same on every node and restart, or leave it unset to have one generated and stored in the meta db" + )); + } + if salt.len() < MIN_ARGON2_SALT_LEN { + return Err(anyhow::anyhow!( + "ZO_EXT_AUTH_SALT is {} bytes, but argon2 needs at least {MIN_ARGON2_SALT_LEN}; set it to a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes (for example `openssl rand -hex 24`)", + salt.len() + )); + } + log::warn!( + "ZO_EXT_AUTH_SALT is the public legacy salt or shorter than {MIN_EXT_AUTH_SALT_LEN} bytes; set a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes before the first presigned or ext-token login" + ); + Ok(()) +} + +/// With `ZO_EXT_AUTH_SALT` set the operator keeps the salt out of the meta db: nothing is stored. +async fn create_instance_identity( + env_ext_auth_salt: &str, +) -> Result { log::info!("Generating new instance id"); + // stored before the instance id, so a node that reads the id also reads the secrets + let ext_auth_salt = if env_ext_auth_salt.is_empty() { + let candidate = config::utils::rand::random_hex(24); + Some(metas::ext_auth_salt::get_or_create(&candidate).await?) + } else { + None + }; let candidate = config::utils::rand::random_hex(32); - // stored before the instance id, so a node that reads the id also reads the token let grpc_token = metas::grpc_token::get_or_create(&candidate).await?; let instance_id = ider::generate(); metas::instance::set(&instance_id).await?; Ok(InstanceIdentity { instance_id, grpc_token: Some(grpc_token), + ext_auth_salt, }) } -fn cache_instance_identity(identity: &InstanceIdentity) { +fn cache_instance_identity(identity: &InstanceIdentity, env_ext_auth_salt: &str) { cache_instance_id(&identity.instance_id); cache_stored_grpc_token(identity.grpc_token.as_deref().unwrap_or_default()); + cache_stored_ext_auth_salt(identity.ext_auth_salt.as_deref().unwrap_or_default()); if identity.grpc_token.is_none() && get_config().grpc.internal_grpc_token.is_empty() { log::warn!( "ZO_INTERNAL_GRPC_TOKEN is not set and this install has no stored internal gRPC token, so the instance id is used as the token; set ZO_INTERNAL_GRPC_TOKEN to the same secret on every node" ); } + match identity.ext_auth_salt.as_deref() { + None if env_ext_auth_salt.is_empty() => log::warn!( + "ZO_EXT_AUTH_SALT is not set and this install has no stored ext auth salt, so the public legacy salt is used; set ZO_EXT_AUTH_SALT to the same random secret of 16 to 48 bytes on every node" + ), + Some(stored) if !env_ext_auth_salt.is_empty() && stored != env_ext_auth_salt => log::warn!( + "ZO_EXT_AUTH_SALT differs from the ext auth salt stored in the meta db; the env value is used, and presigned or ext-token logins derived from the stored salt will fail" + ), + _ => {} + } } pub async fn init() -> Result<(), anyhow::Error> { @@ -124,7 +196,11 @@ pub async fn init() -> Result<(), anyhow::Error> { audit::set_audit_publisher(Arc::new(CoreAuditPublisher)) .map_err(|_| anyhow::anyhow!("audit publisher is already initialized"))?; - cache_instance_identity(&get_or_create_instance_identity().await?); + let env_ext_auth_salt = get_config().auth.ext_auth_salt.clone(); + cache_instance_identity( + &get_or_create_instance_identity(&env_ext_auth_salt).await?, + &env_ext_auth_salt, + ); // _meta used to appear only once self-reporting wrote its first stream into it, // so a deployment that reports nothing never had the org its history pages read. @@ -149,7 +225,7 @@ mod tests { async fn reset_identity() { infra::db::create_table().await.unwrap(); let db = infra::db::get_db().await; - for key in ["/instance/", "/internal_grpc_token/"] { + for key in ["/instance/", "/internal_grpc_token/", "/ext_auth_salt/"] { db.delete_if_exists(key, false, false).await.unwrap(); } } @@ -158,7 +234,7 @@ mod tests { async fn test_fresh_install_gets_a_random_grpc_token() { let _guard = META_KEYS.lock().await; reset_identity().await; - let identity = get_or_create_instance_identity().await.unwrap(); + let identity = get_or_create_instance_identity("").await.unwrap(); let token = identity.grpc_token.expect("a fresh install stores a token"); assert_eq!(token.len(), 64); assert!(token.chars().all(|c| c.is_ascii_hexdigit())); @@ -169,7 +245,7 @@ mod tests { ); reset_identity().await; - let other = get_or_create_instance_identity().await.unwrap(); + let other = get_or_create_instance_identity("").await.unwrap(); assert_ne!(other.grpc_token, Some(token)); } @@ -177,11 +253,13 @@ mod tests { async fn test_grpc_token_is_stable_across_restarts() { let _guard = META_KEYS.lock().await; reset_identity().await; - let first = get_or_create_instance_identity().await.unwrap(); - let second = get_or_create_instance_identity().await.unwrap(); + let first = get_or_create_instance_identity("").await.unwrap(); + let second = get_or_create_instance_identity("").await.unwrap(); assert!(first.grpc_token.is_some()); + assert!(first.ext_auth_salt.is_some()); assert_eq!(first.instance_id, second.instance_id); assert_eq!(first.grpc_token, second.grpc_token); + assert_eq!(first.ext_auth_salt, second.ext_auth_salt); } #[tokio::test] @@ -189,33 +267,155 @@ mod tests { let _guard = META_KEYS.lock().await; reset_identity().await; metas::instance::set("existing-instance").await.unwrap(); - let identity = get_or_create_instance_identity().await.unwrap(); + let identity = get_or_create_instance_identity("").await.unwrap(); assert_eq!(identity.instance_id, "existing-instance"); assert_eq!(identity.grpc_token, None); assert_eq!(metas::grpc_token::get().await.unwrap(), None); cache_stored_grpc_token("left-over"); - cache_instance_identity(&identity); + cache_stored_ext_auth_salt("left-over"); + cache_instance_identity(&identity, ""); assert_eq!(config::get_stored_grpc_token(), ""); + assert_eq!(config::get_stored_ext_auth_salt(), ""); assert_eq!(config::get_instance_id(), "existing-instance"); } + #[tokio::test] + async fn test_fresh_install_without_env_salt_stores_a_random_one() { + let _guard = META_KEYS.lock().await; + reset_identity().await; + let identity = get_or_create_instance_identity("").await.unwrap(); + let salt = identity + .ext_auth_salt + .expect("a fresh install stores a salt"); + assert_eq!(salt.len(), 48); + assert!(salt.chars().all(|c| c.is_ascii_hexdigit())); + assert!( + ::config::utils::hash::try_get_passcode_hash("p", &salt).is_some(), + "argon2 must accept the generated salt" + ); + assert_eq!( + metas::ext_auth_salt::get().await.unwrap().as_deref(), + Some(salt.as_str()) + ); + } + + #[tokio::test] + async fn test_fresh_install_with_env_salt_stores_none() { + let _guard = META_KEYS.lock().await; + reset_identity().await; + let identity = get_or_create_instance_identity("env-salt-0123456789abcdef") + .await + .unwrap(); + assert_eq!(identity.ext_auth_salt, None); + assert_eq!(metas::ext_auth_salt::get().await.unwrap(), None); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn test_concurrent_first_boots_agree_on_the_grpc_token() { + async fn test_concurrent_first_boots_agree_on_the_stored_secrets() { let _guard = META_KEYS.lock().await; reset_identity().await; let boots = (0..4) - .map(|_| tokio::spawn(get_or_create_instance_identity())) + .map(|_| tokio::spawn(get_or_create_instance_identity(""))) .collect::>(); - let mut tokens = Vec::new(); + let mut secrets = Vec::new(); for boot in boots { - tokens.push(boot.await.unwrap().unwrap().grpc_token); + let identity = boot.await.unwrap().unwrap(); + secrets.push((identity.grpc_token, identity.ext_auth_salt)); } - let stored = metas::grpc_token::get().await.unwrap(); - assert!(stored.is_some()); + let stored = ( + metas::grpc_token::get().await.unwrap(), + metas::ext_auth_salt::get().await.unwrap(), + ); + assert!(stored.0.is_some() && stored.1.is_some()); assert!( - tokens.iter().all(|t| *t == stored), - "{tokens:?} vs {stored:?}" + secrets.iter().all(|s| *s == stored), + "{secrets:?} vs {stored:?}" ); } + + #[test] + fn a_new_install_refuses_the_legacy_or_a_short_ext_auth_salt() { + for salt in [LEGACY_EXT_AUTH_SALT, "short-secret"] { + let err = check_ext_auth_salt(true, salt).unwrap_err().to_string(); + assert!(err.contains("ZO_EXT_AUTH_SALT"), "{salt}: {err}"); + } + } + + #[test] + fn an_unset_ext_auth_salt_is_accepted_on_any_install() { + for fresh_install in [true, false] { + assert!(check_ext_auth_salt(fresh_install, "").is_ok()); + } + } + + #[test] + fn a_new_install_accepts_a_random_ext_auth_salt() { + assert!(check_ext_auth_salt(true, "3f9c2a7e5b1d8c4f6a0e2b9d7c5a3f1e").is_ok()); + } + + #[test] + fn an_accepted_ext_auth_salt_is_one_argon2_can_hash_with() { + // 0 is "unset": the install hashes with a generated or legacy salt instead + for len in 1..=96 { + let salt = "a".repeat(len); + let hashable = + ::config::utils::hash::try_get_passcode_hash("Complexpass#123", &salt).is_some(); + for fresh_install in [true, false] { + let accepted = check_ext_auth_salt(fresh_install, &salt).is_ok(); + assert!( + !accepted || hashable, + "len {len}, fresh {fresh_install}: accepted but argon2 refuses it" + ); + } + } + } + + #[test] + fn the_recommended_ext_auth_salt_is_accepted_and_a_hex_32_one_is_refused() { + let hex_24 = "9f2c4e6a8b0d1f3e5c7a9b1d3f5e7c9a0b2d4f6e8a1c3e5f"; + assert_eq!(hex_24.len(), 48); + assert!(check_ext_auth_salt(true, hex_24).is_ok()); + assert!(::config::utils::hash::try_get_passcode_hash("p", hex_24).is_some()); + let hex_32 = "9f2c4e6a8b0d1f3e5c7a9b1d3f5e7c9a0b2d4f6e8a1c3e5f7a9b0c1d2e3f4a5b"; + assert_eq!(hex_32.len(), 64); + for fresh_install in [true, false] { + let err = check_ext_auth_salt(fresh_install, hex_32) + .unwrap_err() + .to_string(); + assert!(err.contains("ZO_EXT_AUTH_SALT"), "{err}"); + assert!(err.contains("openssl rand -hex 24"), "{err}"); + } + } + + #[test] + fn the_new_install_refusal_recommends_a_salt_that_fits() { + let err = check_ext_auth_salt(true, LEGACY_EXT_AUTH_SALT) + .unwrap_err() + .to_string(); + assert!(err.contains("openssl rand -hex 24"), "{err}"); + } + + #[test] + fn an_existing_install_keeps_starting_with_the_default_or_a_short_usable_ext_auth_salt() { + for salt in [LEGACY_EXT_AUTH_SALT, "eight888", "short-secret"] { + assert!(check_ext_auth_salt(false, salt).is_ok(), "{salt:?}"); + } + } + + #[test] + fn an_existing_install_refuses_an_ext_auth_salt_argon2_cannot_use() { + let too_long = "a".repeat(MAX_EXT_AUTH_SALT_LEN + 1); + for salt in ["a", "abc", "seven77", too_long.as_str()] { + assert!( + ::config::utils::hash::try_get_passcode_hash("p", salt).is_none(), + "{salt:?} is usable" + ); + let err = check_ext_auth_salt(false, salt) + .expect_err(salt) + .to_string(); + assert!(err.contains("ZO_EXT_AUTH_SALT"), "{salt:?}: {err}"); + assert!(err.contains("openssl rand -hex 24"), "{salt:?}: {err}"); + } + } } diff --git a/src/core/src/users.rs b/src/core/src/users.rs index 861be805fa..a28349d7c7 100644 --- a/src/core/src/users.rs +++ b/src/core/src/users.rs @@ -28,7 +28,7 @@ use common::meta::user::{InviteStatus, UserInvite, UserInviteList}; #[cfg(feature = "enterprise")] use config::meta::ratelimit::CachedUserRoles; use config::{ - DEFAULT_ORG, META_ORG_ID, get_config, ider, + DEFAULT_ORG, META_ORG_ID, ider, meta::user::{DBUser, User, UserOrg, UserRole}, utils::rand::generate_random_string, }; @@ -81,7 +81,6 @@ pub async fn post_user( if !is_valid_email(&usr_req.email) { return Ok(MetaHttpResponse::bad_request("Invalid email")); } - let cfg = get_config(); usr_req.email = usr_req.email.to_lowercase(); if let Some(_custom_roles) = &usr_req.role.custom_role { #[cfg(not(feature = "enterprise"))] @@ -165,7 +164,7 @@ pub async fn post_user( } let salt = ider::uuid(); let password = get_hash(&usr_req.password, &salt); - let password_ext = get_hash(&usr_req.password, &cfg.auth.ext_auth_salt); + let password_ext = get_hash(&usr_req.password, &config::get_ext_auth_salt()); let token = generate_random_string(16); let token_for_response = token.clone(); let rum_token = format!("rum{}", generate_random_string(16)); @@ -309,8 +308,7 @@ pub async fn update_user( let mut old_role = None; let mut new_role = None; - let conf = get_config(); - let password_ext_salt = conf.auth.ext_auth_salt.as_str(); + let password_ext_salt = &config::get_ext_auth_salt(); let Ok(existing_user) = existing_user else { return Ok(MetaHttpResponse::not_found("User not found")); @@ -1342,10 +1340,9 @@ pub async fn list_user_invites(user_id: &str, only_pending: bool) -> Result Result<(), anyhow::Error> { - let cfg = get_config(); let salt = ider::uuid(); let password = get_hash(&user_req.password, &salt); - let password_ext = get_hash(&user_req.password, &cfg.auth.ext_auth_salt); + let password_ext = get_hash(&user_req.password, &config::get_ext_auth_salt()); let token = user_req .token .clone() @@ -1457,8 +1454,7 @@ pub async fn create_service_account_if_not_exists(email: &str) -> Result<(), any let random_password = generate_random_string(32); let salt = ider::uuid(); let password_hash = get_hash(&random_password, &salt); - let cfg = get_config(); - let password_ext = get_hash(&random_password, &cfg.auth.ext_auth_salt); + let password_ext = get_hash(&random_password, &config::get_ext_auth_salt()); let now = chrono::Utc::now().timestamp_micros(); let user_record = infra::table::users::UserRecord { email: email.to_string(), diff --git a/src/db/src/metas.rs b/src/db/src/metas.rs index 7c4c03e566..b8bb3dea58 100644 --- a/src/db/src/metas.rs +++ b/src/db/src/metas.rs @@ -65,41 +65,68 @@ pub mod instance { } pub mod grpc_token { + use infra::errors::Result; + + // its own module, not a key under /instance/: NATS get falls back to a prefix scan there + const KEY: &str = "/internal_grpc_token/"; + + pub async fn get() -> Result> { + super::secret::get(KEY).await + } + + pub async fn get_or_create(candidate: &str) -> Result { + super::secret::get_or_create(KEY, candidate).await + } +} + +pub mod ext_auth_salt { + use infra::errors::Result; + + const KEY: &str = "/ext_auth_salt/"; + + pub async fn get() -> Result> { + super::secret::get(KEY).await + } + + pub async fn get_or_create(candidate: &str) -> Result { + super::secret::get_or_create(KEY, candidate).await + } +} + +mod secret { use bytes::Bytes; use infra::{ db::{NO_NEED_WATCH, get_db}, errors::{Error, Result}, }; - // its own module, not a key under /instance/: NATS get falls back to a prefix scan there - const KEY: &str = "/internal_grpc_token/"; - - pub async fn get() -> Result> { - let value = get_db().await.get_if_exists(KEY).await?; + pub(super) async fn get(key: &str) -> Result> { + let value = get_db().await.get_if_exists(key).await?; Ok(value .map(|v| String::from_utf8_lossy(&v).trim().to_string()) .filter(|v| !v.is_empty())) } - /// Stores `candidate` unless a token is already stored, and returns the stored token. - pub async fn get_or_create(candidate: &str) -> Result { + /// Returns the stored value, which is `candidate` only when nothing was stored yet. + pub(super) async fn get_or_create(key: &str, candidate: &str) -> Result { let value = Bytes::from(candidate.to_string()); + let owned_key = key.to_string(); get_db() .await .get_for_update( - KEY, + key, NO_NEED_WATCH, None, Box::new(move |existing| { Ok(match existing { Some(_) => None, - None => Some((None, Some((KEY.to_string(), value, None)))), + None => Some((None, Some((owned_key, value, None)))), }) }), ) .await?; - get() + get(key) .await? - .ok_or_else(|| Error::Message("internal grpc token is missing after create".into())) + .ok_or_else(|| Error::Message(format!("{key} is missing after create"))) } } diff --git a/src/infra/src/lib.rs b/src/infra/src/lib.rs index 46589945dc..b8d2768054 100644 --- a/src/infra/src/lib.rs +++ b/src/infra/src/lib.rs @@ -59,15 +59,6 @@ pub async fn get_db_schema_version() -> Result { Ok(k) } -/// Whether a node already booted on this meta store: every boot since 2023 stores the instance id. -pub async fn db_has_data() -> Result { - match db::get_db().await.count("/instance/").await { - Ok(rows) => Ok(rows > 0), - Err(e) if is_db_schema_version_missing(&e) => Ok(false), - Err(e) => Err(e.into()), - } -} - /// Whether the error means the version is genuinely not stored yet (vs the /// database being unreachable or overloaded). fn is_db_schema_version_missing(e: &errors::Error) -> bool { diff --git a/src/migration/mod.rs b/src/migration/mod.rs index 107f8cf81a..60ddaed8cb 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -31,12 +31,6 @@ mod progress; pub use config::MigrationConfig; pub use migrator::{run_file_list, run_meta}; -const DEFAULT_EXT_AUTH_SALT: &str = "openobserve"; -const MIN_EXT_AUTH_SALT_LEN: usize = 16; -// argon2 hashes with a salt of 8 to 48 bytes and get_hash panics outside that -const MIN_ARGON2_SALT_LEN: usize = 8; -const MAX_EXT_AUTH_SALT_LEN: usize = 48; - pub async fn init_db() -> std::result::Result<(), anyhow::Error> { // warm both pools before the migration starts hitting them get_orm_client_ro().await; @@ -67,9 +61,6 @@ pub async fn init_db() -> std::result::Result<(), anyhow::Error> { "failed to get db schema version after {MAX_RETRIES} attempts: {e}; refusing to assume a fresh install and run the db upgrade against an unhealthy database" )); } - // a missing version alone also matches installs older than the version key - let fresh_install = db_schema_version == 0 && !infra::db_has_data().await?; - check_ext_auth_salt(fresh_install, &::config::get_config().auth.ext_auth_salt)?; if db_schema_version == DB_SCHEMA_VERSION { // if version matches, we do not need to run update commands log::info!("DB_SCHEMA_VERSION match, skipping db upgrade"); @@ -115,131 +106,3 @@ pub async fn init_db() -> std::result::Result<(), anyhow::Error> { Ok(()) } - -/// Existing installs only warn on a weak usable salt: rotating it breaks logins derived from it. -fn check_ext_auth_salt(fresh_install: bool, salt: &str) -> anyhow::Result<()> { - if salt.len() > MAX_EXT_AUTH_SALT_LEN { - return Err(anyhow::anyhow!( - "ZO_EXT_AUTH_SALT is {} bytes, but argon2 accepts at most {MAX_EXT_AUTH_SALT_LEN}; set it to a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes (for example `openssl rand -hex 24`)", - salt.len() - )); - } - if salt != DEFAULT_EXT_AUTH_SALT && salt.len() >= MIN_EXT_AUTH_SALT_LEN { - return Ok(()); - } - if fresh_install { - return Err(anyhow::anyhow!( - "ZO_EXT_AUTH_SALT must be set to a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes on a new install (for example `openssl rand -hex 24`), and kept the same on every node and restart" - )); - } - if salt.len() < MIN_ARGON2_SALT_LEN { - return Err(anyhow::anyhow!( - "ZO_EXT_AUTH_SALT is {} bytes, but argon2 needs at least {MIN_ARGON2_SALT_LEN}; set it to a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes (for example `openssl rand -hex 24`)", - salt.len() - )); - } - log::warn!( - "ZO_EXT_AUTH_SALT is the public default or shorter than {MIN_EXT_AUTH_SALT_LEN} bytes; set a random secret of {MIN_EXT_AUTH_SALT_LEN} to {MAX_EXT_AUTH_SALT_LEN} bytes before the first presigned or ext-token login" - ); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn a_new_install_refuses_a_missing_default_or_short_ext_auth_salt() { - for salt in ["", DEFAULT_EXT_AUTH_SALT, "short-secret"] { - let err = check_ext_auth_salt(true, salt).unwrap_err().to_string(); - assert!(err.contains("ZO_EXT_AUTH_SALT"), "{salt}: {err}"); - } - } - - #[test] - fn a_new_install_accepts_a_random_ext_auth_salt() { - assert!(check_ext_auth_salt(true, "3f9c2a7e5b1d8c4f6a0e2b9d7c5a3f1e").is_ok()); - } - - #[test] - fn an_accepted_ext_auth_salt_is_one_argon2_can_hash_with() { - for len in 0..=96 { - let salt = "a".repeat(len); - let hashable = - ::config::utils::hash::try_get_passcode_hash("Complexpass#123", &salt).is_some(); - for fresh_install in [true, false] { - let accepted = check_ext_auth_salt(fresh_install, &salt).is_ok(); - assert!( - !accepted || hashable, - "len {len}, fresh {fresh_install}: accepted but argon2 refuses it" - ); - } - } - } - - #[test] - fn the_recommended_ext_auth_salt_is_accepted_and_a_hex_32_one_is_refused() { - let hex_24 = "9f2c4e6a8b0d1f3e5c7a9b1d3f5e7c9a0b2d4f6e8a1c3e5f"; - assert_eq!(hex_24.len(), 48); - assert!(check_ext_auth_salt(true, hex_24).is_ok()); - assert!(::config::utils::hash::try_get_passcode_hash("p", hex_24).is_some()); - let hex_32 = "9f2c4e6a8b0d1f3e5c7a9b1d3f5e7c9a0b2d4f6e8a1c3e5f7a9b0c1d2e3f4a5b"; - assert_eq!(hex_32.len(), 64); - for fresh_install in [true, false] { - let err = check_ext_auth_salt(fresh_install, hex_32) - .unwrap_err() - .to_string(); - assert!(err.contains("ZO_EXT_AUTH_SALT"), "{err}"); - assert!(err.contains("openssl rand -hex 24"), "{err}"); - } - } - - #[test] - fn the_new_install_refusal_recommends_a_salt_that_fits() { - let err = check_ext_auth_salt(true, "").unwrap_err().to_string(); - assert!(err.contains("openssl rand -hex 24"), "{err}"); - } - - #[test] - fn the_shipped_example_ext_auth_salts_are_refused_on_a_new_install() { - let env_example = include_str!("../../.env.example") - .lines() - .find_map(|line| line.trim().strip_prefix("ZO_EXT_AUTH_SALT")) - .and_then(|rest| rest.trim().strip_prefix('=')) - .map(|value| value.trim().trim_matches('"').to_string()) - .expect(".env.example sets ZO_EXT_AUTH_SALT"); - let k8s = include_str!("../../deploy/k8s/statefulset.yaml"); - let k8s = k8s - .lines() - .skip_while(|line| !line.contains("name: ZO_EXT_AUTH_SALT")) - .find_map(|line| line.trim().strip_prefix("value:")) - .map(|value| value.trim().trim_matches('"').to_string()) - .expect("statefulset.yaml sets ZO_EXT_AUTH_SALT"); - for salt in [env_example, k8s] { - assert!(check_ext_auth_salt(true, &salt).is_err(), "{salt:?}"); - } - } - - #[test] - fn an_existing_install_keeps_starting_with_the_default_or_a_short_usable_ext_auth_salt() { - for salt in [DEFAULT_EXT_AUTH_SALT, "eight888", "short-secret"] { - assert!(check_ext_auth_salt(false, salt).is_ok(), "{salt:?}"); - } - } - - #[test] - fn an_existing_install_refuses_an_ext_auth_salt_argon2_cannot_use() { - let too_long = "a".repeat(MAX_EXT_AUTH_SALT_LEN + 1); - for salt in ["", "a", "abc", "seven77", too_long.as_str()] { - assert!( - ::config::utils::hash::try_get_passcode_hash("p", salt).is_none(), - "{salt:?} is usable" - ); - let err = check_ext_auth_salt(false, salt) - .expect_err(salt) - .to_string(); - assert!(err.contains("ZO_EXT_AUTH_SALT"), "{salt:?}: {err}"); - assert!(err.contains("openssl rand -hex 24"), "{salt:?}: {err}"); - } - } -} diff --git a/tests/api-testing/tests/regression/test_sqlite_invalid_utf8_meta_startup.py b/tests/api-testing/tests/regression/test_sqlite_invalid_utf8_meta_startup.py index 8ac3402eb8..5ccf15bf64 100644 --- a/tests/api-testing/tests/regression/test_sqlite_invalid_utf8_meta_startup.py +++ b/tests/api-testing/tests/regression/test_sqlite_invalid_utf8_meta_startup.py @@ -40,7 +40,6 @@ class _Server: env.update({ "ZO_ROOT_USER_EMAIL": self.auth[0], "ZO_ROOT_USER_PASSWORD": self.auth[1], - "ZO_EXT_AUTH_SALT": "test-only-ext-auth-salt-0123456789", "ZO_HTTP_PORT": str(self.http_port), "ZO_GRPC_PORT": str(self.grpc_port), "ZO_DATA_DIR": str(self.data_dir), diff --git a/tests/fresh_install_salt.rs b/tests/fresh_install_salt.rs deleted file mode 100644 index 261b5af90d..0000000000 --- a/tests/fresh_install_salt.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2026 OpenObserve Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use openobserve::migration; - -#[tokio::test] -async fn only_an_empty_db_counts_as_a_fresh_install_for_the_ext_auth_salt() { - let data_dir = tempfile::tempdir().unwrap(); - unsafe { - std::env::set_var("ZO_DATA_DIR", format!("{}/", data_dir.path().display())); - std::env::set_var("ZO_META_STORE", "sqlite"); - std::env::set_var("ZO_LOCAL_MODE", "true"); - std::env::set_var("ZO_EXT_AUTH_SALT", "openobserve"); - } - - for attempt in 1..=2 { - let err = migration::init_db().await.unwrap_err().to_string(); - assert!(err.contains("ZO_EXT_AUTH_SALT"), "attempt {attempt}: {err}"); - } - - // an install older than the schema-version key: meta rows, no version - infra::db::create_table().await.unwrap(); - infra::db::get_db() - .await - .put("/instance/", "7f3a9c".into(), false, None) - .await - .unwrap(); - assert_eq!(infra::get_db_schema_version().await.unwrap(), 0); - - migration::init_db().await.unwrap(); - assert_eq!( - infra::get_db_schema_version().await.unwrap(), - config::DB_SCHEMA_VERSION - ); -} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 52c09db182..0e5488f10c 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -86,7 +86,6 @@ mod tests { START.call_once(|| unsafe { env::set_var("ZO_ROOT_USER_EMAIL", "root@example.com"); env::set_var("ZO_ROOT_USER_PASSWORD", "Complexpass#123"); - env::set_var("ZO_EXT_AUTH_SALT", "test-only-ext-auth-salt-0123456789"); env::set_var("ZO_LOCAL_MODE", "true"); env::set_var("ZO_MAX_FILE_SIZE_ON_DISK", "1"); env::set_var("ZO_FILE_PUSH_INTERVAL", "1");