From 3359e6722cec9e79ef794c9e7b5f0d914fe1c430 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 15 May 2026 12:20:54 -0500 Subject: [PATCH] Remove anonymous WebUI access --- src/rust/lqos_config/src/authentication.rs | 77 ++++++++++++++----- src/rust/lqosd/src/node_manager/auth.rs | 45 +++++------ .../node_manager/js_build/src/first-run.js | 2 - .../src/node_manager/static2/first-run.html | 21 ++--- 4 files changed, 84 insertions(+), 61 deletions(-) diff --git a/src/rust/lqos_config/src/authentication.rs b/src/rust/lqos_config/src/authentication.rs index ba457db5..7bd29fb3 100644 --- a/src/rust/lqos_config/src/authentication.rs +++ b/src/rust/lqos_config/src/authentication.rs @@ -110,8 +110,6 @@ pub struct WebUsers { #[serde(default = "default_auth_epoch")] auth_epoch: u64, #[serde(default)] - allow_unauthenticated_to_view: bool, - #[serde(default)] users: Vec, #[serde(skip)] base_path_override: Option, @@ -122,7 +120,6 @@ impl Default for WebUsers { Self { version: AUTH_FILE_VERSION, auth_epoch: INITIAL_AUTH_EPOCH, - allow_unauthenticated_to_view: false, users: Vec::new(), base_path_override: None, } @@ -240,7 +237,11 @@ impl WebUsers { Ok(()) } - fn migrate_if_needed(&mut self, loaded_from: &Path) -> Result<(), AuthenticationError> { + fn migrate_if_needed( + &mut self, + loaded_from: &Path, + removed_anonymous_setting_present: bool, + ) -> Result<(), AuthenticationError> { let current_path = Self::primary_path_for(&self.resolved_base_path()?); let loaded_from_legacy_path = loaded_from != current_path; let mut needs_rewrite = false; @@ -259,6 +260,10 @@ impl WebUsers { needs_rewrite = true; } + if removed_anonymous_setting_present { + needs_rewrite = true; + } + if needs_rewrite { self.bump_auth_epoch(); self.save_to_disk()?; @@ -279,12 +284,13 @@ impl WebUsers { error!("Unable to read auth file {:?}: {e}", path); AuthenticationError::UnableToRead })?; + let removed_anonymous_setting_present = auth_file_has_removed_anonymous_setting(&raw); let mut users: Self = toml_edit::de::from_str(&raw).map_err(|e| { error!("Unable to deserialize auth file {:?}: {e}", path); AuthenticationError::UnableToParse })?; users.base_path_override = Some(Self::default_base_path()?); - users.migrate_if_needed(&path)?; + users.migrate_if_needed(&path, removed_anonymous_setting_present)?; Ok(users) } else { let new_users = Self::default(); @@ -311,12 +317,13 @@ impl WebUsers { error!("Unable to read auth file {:?}: {e}", path); AuthenticationError::UnableToRead })?; + let removed_anonymous_setting_present = auth_file_has_removed_anonymous_setting(&raw); let mut users: Self = toml_edit::de::from_str(&raw).map_err(|e| { error!("Unable to deserialize auth file {:?}: {e}", path); AuthenticationError::UnableToParse })?; users.base_path_override = Some(base_path.to_path_buf()); - users.migrate_if_needed(&path)?; + users.migrate_if_needed(&path, removed_anonymous_setting_present)?; Ok(users) } else { let new_users = Self { @@ -483,21 +490,12 @@ impl WebUsers { pub fn get_users(&self) -> Vec { self.users.clone() } +} - /// Sets the "allow unauthenticated users" field. If true, - /// unauthenticated users gain read-only access. This is useful - /// for demonstration purposes. - pub fn allow_anonymous(&mut self, allow: bool) -> Result<(), AuthenticationError> { - self.allow_unauthenticated_to_view = allow; - self.bump_auth_epoch(); - self.save_to_disk()?; - Ok(()) - } - - /// Do we allow unauthenticated users to read site data? - pub fn do_we_allow_anonymous(&self) -> bool { - self.allow_unauthenticated_to_view - } +fn auth_file_has_removed_anonymous_setting(raw: &str) -> bool { + raw.parse::() + .map(|document| document.contains_key("allow_unauthenticated_to_view")) + .unwrap_or(false) } /// Errors that can occur while managing web-UI authentication. @@ -530,3 +528,42 @@ pub enum AuthenticationError { #[error("Invalid Login")] InvalidLogin, } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + fn temp_auth_dir(test_name: &str) -> PathBuf { + std::env::temp_dir().join(format!("libreqos-auth-{test_name}-{}", Uuid::new_v4())) + } + + #[test] + fn load_removes_legacy_anonymous_access_setting() { + let dir = temp_auth_dir("anonymous-setting"); + fs::create_dir_all(&dir).expect("create auth test directory"); + let path = dir.join(CURRENT_AUTH_FILE_NAME); + fs::write( + &path, + r#"version = 2 +auth_epoch = 1 +allow_unauthenticated_to_view = true + +[[users]] +username = "support" +password_hash = "legacy" +role = "ReadOnly" +"#, + ) + .expect("write auth file"); + + let users = WebUsers::load_or_create_in(&dir).expect("load auth file"); + assert_eq!(users.auth_epoch(), 2); + assert_eq!(users.get_users().len(), 1); + + let saved = fs::read_to_string(&path).expect("read migrated auth file"); + assert!(!saved.contains("allow_unauthenticated_to_view")); + + fs::remove_dir_all(&dir).expect("remove auth test directory"); + } +} diff --git a/src/rust/lqosd/src/node_manager/auth.rs b/src/rust/lqosd/src/node_manager/auth.rs index 68a1d137..716fa887 100644 --- a/src/rust/lqosd/src/node_manager/auth.rs +++ b/src/rust/lqosd/src/node_manager/auth.rs @@ -55,7 +55,6 @@ struct CachedAuthSnapshot { struct AuthSnapshot { bootstrap_state: AuthBootstrapState, auth_epoch: u64, - allow_anonymous: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -334,7 +333,6 @@ fn auth_snapshot() -> AuthSnapshot { return AuthSnapshot { bootstrap_state: AuthBootstrapState::CorruptUsersFile, auth_epoch: 0, - allow_anonymous: false, }; } }; @@ -350,7 +348,6 @@ fn auth_snapshot() -> AuthSnapshot { None => AuthSnapshot { bootstrap_state: AuthBootstrapState::MissingUsersFile, auth_epoch: 0, - allow_anonymous: false, }, Some(_) => match WebUsers::load_or_create() { Ok(users) => AuthSnapshot { @@ -360,14 +357,12 @@ fn auth_snapshot() -> AuthSnapshot { AuthBootstrapState::Ready }, auth_epoch: users.auth_epoch(), - allow_anonymous: users.do_we_allow_anonymous(), }, Err(e) => { warn!("Unable to load auth state: {e}"); AuthSnapshot { bootstrap_state: AuthBootstrapState::CorruptUsersFile, auth_epoch: 0, - allow_anonymous: false, } } }, @@ -597,14 +592,14 @@ pub async fn get_username(jar: &CookieJar) -> String { } } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum LoginResult { Admin, ReadOnly, Denied, } -fn login_result_for_session(user: Option, allow_anonymous: bool) -> LoginResult { +fn login_result_for_session(user: Option) -> LoginResult { match user { Some(SessionUser { role: UserRole::Admin, @@ -614,7 +609,6 @@ fn login_result_for_session(user: Option, allow_anonymous: bool) -> role: UserRole::ReadOnly, .. }) => LoginResult::ReadOnly, - None if allow_anonymous => LoginResult::ReadOnly, None => LoginResult::Denied, } } @@ -649,7 +643,7 @@ fn runtime_onboarding_exempt_path(path: &str) -> bool { /// Checks an incoming request for a `User-Token` cookie. If found, /// it validates the request against the signed session and current auth epoch. /// Missing or empty auth state redirects to first-run; invalid sessions redirect -/// to login unless anonymous read-only is enabled. +/// to login. pub async fn auth_layer( jar: CookieJar, mut req: axum::extract::Request, @@ -667,7 +661,7 @@ pub async fn auth_layer( } let login_result = match session_from_cookie(&jar, &snapshot) { - Ok(user) => login_result_for_session(user, snapshot.allow_anonymous), + Ok(user) => login_result_for_session(user), Err(status) => return (status, "Unable to validate session").into_response(), }; @@ -703,7 +697,7 @@ pub async fn login_from_token(token: &str) -> LoginResult { }; let login_result = match verify_signed_session(&key, token, &snapshot) { - Ok(user) => login_result_for_session(user, snapshot.allow_anonymous), + Ok(user) => login_result_for_session(user), Err(e) => { warn!("Unable to verify websocket session token: {e}"); LoginResult::Denied @@ -881,7 +875,6 @@ pub async fn try_login( pub struct FirstUser { username: String, password: String, - allow_anonymous: bool, } pub async fn first_user( @@ -924,19 +917,6 @@ pub async fn first_user( }), ) })?; - users - .allow_anonymous(new_user.allow_anonymous) - .map_err(|e| { - warn!("Unable to set anonymous auth policy: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(LoginResponse { - ok: false, - reason: Some("auth_corrupt"), - message: Some("Unable to update auth settings.".to_string()), - }), - ) - })?; users .add_or_update_user(&new_user.username, &new_user.password, UserRole::Admin) .map_err(|e| { @@ -995,6 +975,21 @@ pub async fn first_user( mod tests { use super::*; + #[test] + fn missing_session_is_denied() { + assert_eq!(login_result_for_session(None), LoginResult::Denied); + } + + #[test] + fn authenticated_read_only_session_keeps_read_only_role() { + let user = SessionUser { + username: "support".to_string(), + role: UserRole::ReadOnly, + }; + + assert_eq!(login_result_for_session(Some(user)), LoginResult::ReadOnly); + } + #[test] fn login_rate_limiter_blocks_after_failed_attempt_limit() { let mut limiter = LoginRateLimiter::new(); diff --git a/src/rust/lqosd/src/node_manager/js_build/src/first-run.js b/src/rust/lqosd/src/node_manager/js_build/src/first-run.js index a605adb1..d435fe6b 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/first-run.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/first-run.js @@ -1,7 +1,6 @@ $("#btnCreateUser").on('click', () => { let username = ($("#username").val() || "").trim(); let password = $("#password").val(); - let anon = document.getElementById("allowAnonymous").checked; if (username === "") { alert("You must enter a username"); return; @@ -12,7 +11,6 @@ $("#btnCreateUser").on('click', () => { } let login = { - allow_anonymous: anon, username: username, password: password } diff --git a/src/rust/lqosd/src/node_manager/static2/first-run.html b/src/rust/lqosd/src/node_manager/static2/first-run.html index 7f4c89f2..3ca744f9 100644 --- a/src/rust/lqosd/src/node_manager/static2/first-run.html +++ b/src/rust/lqosd/src/node_manager/static2/first-run.html @@ -42,23 +42,16 @@ This site will use a cookie to store your identification. If that's not ok, please don't use the site.

-

Let's create a new user, and set some parameters:

+

Let's create a new admin user:

- - - - - + +
- - + + Your Username
- Your Username - - -
+ +
Your password