From 165dae030b2ef59955c3d64e2bdda8a1e2265812 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Mon, 9 Jan 2023 18:55:14 +0000 Subject: [PATCH] Adds an authentication system. * The new Rust utility "webusers" manages /opt/libreqos/webusers.toml. * You can add/update/remove/list users from that tool. * The "allow anonymous" option in webusers.toml permits access for unauthenticated users, but won't let them change anything. This is for payne demonstrations. * All web APIs and pages should now be secured, requiring a login. * The login requires cookies. Signed-off-by: Herbert Wolverson --- .gitignore | 2 + src/build_rust.sh | 2 +- src/rust/Cargo.lock | 52 ++++- src/rust/Cargo.toml | 1 + src/rust/lqos_config/Cargo.toml | 2 + src/rust/lqos_config/src/authentication.rs | 205 ++++++++++++++++++ src/rust/lqos_config/src/lib.rs | 2 + src/rust/lqos_node_manager/src/auth_guard.rs | 128 +++++++++++ .../lqos_node_manager/src/config_control.rs | 10 +- src/rust/lqos_node_manager/src/main.rs | 7 + src/rust/lqos_node_manager/src/queue_info.rs | 7 +- .../lqos_node_manager/src/shaped_devices.rs | 14 +- .../lqos_node_manager/src/static_pages.rs | 31 ++- src/rust/lqos_node_manager/src/tracker/mod.rs | 18 +- .../lqos_node_manager/src/unknown_devices.rs | 8 +- .../static/circuit_queue.html | 1 + src/rust/lqos_node_manager/static/config.html | 165 ++++++++------ .../lqos_node_manager/static/first_run.html | 92 ++++++++ src/rust/lqos_node_manager/static/login.html | 72 ++++++ src/rust/lqos_node_manager/static/lqos.js | 21 ++ src/rust/lqos_node_manager/static/main.html | 1 + .../lqos_node_manager/static/shaped-add.html | 2 +- src/rust/lqos_node_manager/static/shaped.html | 2 +- .../lqos_node_manager/static/unknown-ip.html | 2 +- src/rust/webusers/Cargo.toml | 9 + src/rust/webusers/src/main.rs | 59 +++++ 26 files changed, 802 insertions(+), 113 deletions(-) create mode 100644 src/rust/lqos_config/src/authentication.rs create mode 100644 src/rust/lqos_node_manager/src/auth_guard.rs create mode 100644 src/rust/lqos_node_manager/static/first_run.html create mode 100644 src/rust/lqos_node_manager/static/login.html create mode 100644 src/rust/webusers/Cargo.toml create mode 100644 src/rust/webusers/src/main.rs diff --git a/.gitignore b/.gitignore index 2c401668..9c0c855b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ src/tinsStats.json src/linux_tc.txt src/lastRun.txt src/liblqos_python.so +src/webusers.toml # Ignore Rust build artifacts src/rust/target @@ -57,6 +58,7 @@ src/bin/lqtop src/bin/xdp_iphash_to_cpu_cmdline src/bin/xdp_pping src/bin/lqos_node_manager +src/bin/webusers src/bin/Rocket.toml diff --git a/src/build_rust.sh b/src/build_rust.sh index 3b88c988..082aa419 100755 --- a/src/build_rust.sh +++ b/src/build_rust.sh @@ -7,7 +7,7 @@ # automatically. # # Don't forget to setup `/etc/lqos` -PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager" +PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager webusers" mkdir -p bin/static pushd rust #cargo clean diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index b5a6b92b..be90d57d 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -205,6 +205,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.3" @@ -377,7 +386,7 @@ dependencies = [ "hmac", "percent-encoding", "rand", - "sha2", + "sha2 0.10.6", "subtle", "time", "version_check", @@ -598,13 +607,22 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer", + "block-buffer 0.10.3", "crypto-common", "subtle", ] @@ -912,7 +930,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.6", ] [[package]] @@ -1216,7 +1234,9 @@ dependencies = [ "ip_network", "ip_network_table", "serde", + "sha2 0.9.9", "toml", + "uuid", ] [[package]] @@ -2078,6 +2098,19 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.6" @@ -2086,7 +2119,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.10.6", ] [[package]] @@ -2556,6 +2589,8 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ + "getrandom", + "rand", "serde", ] @@ -2604,6 +2639,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "webusers" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.0.29", + "lqos_config", +] + [[package]] name = "which" version = "3.1.1" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 7a34f372..d4c5a2d0 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -20,4 +20,5 @@ members = [ "xdp_pping", # Rust port of cpumap's `xdp_pping` tool, for compatibility "lqos_node_manager", # A lightweight web interface for management and local monitoring "lqos_python", # Python bindings for using the Rust bus directly + "webusers", # CLI control for managing the web user list ] \ No newline at end of file diff --git a/src/rust/lqos_config/Cargo.toml b/src/rust/lqos_config/Cargo.toml index 0a1102ec..34335ee6 100644 --- a/src/rust/lqos_config/Cargo.toml +++ b/src/rust/lqos_config/Cargo.toml @@ -10,3 +10,5 @@ serde = { version = "1.0", features = [ "derive" ] } csv = "1" ip_network_table = "0" ip_network = "0" +sha2 = "0" +uuid = { version = "1", features = ["v4", "fast-rng" ] } diff --git a/src/rust/lqos_config/src/authentication.rs b/src/rust/lqos_config/src/authentication.rs new file mode 100644 index 00000000..d7abf473 --- /dev/null +++ b/src/rust/lqos_config/src/authentication.rs @@ -0,0 +1,205 @@ +use anyhow::{Error, Result}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::{ + fs::{read_to_string, OpenOptions, remove_file}, + io::Write, + path::{Path, PathBuf}, fmt::Display, +}; +use uuid::Uuid; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub enum UserRole { + ReadOnly, + Admin, +} + +impl From<&str> for UserRole { + fn from(s: &str) -> Self { + let s = s.to_lowercase(); + if s == "admin" { + UserRole::Admin + } else { + UserRole::ReadOnly + } + } +} + +impl Display for UserRole { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserRole::Admin => write!(f, "admin"), + UserRole::ReadOnly => write!(f, "read-only"), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct WebUser { + username: String, + password_hash: String, + role: UserRole, + token: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WebUsers { + allow_unauthenticated_to_view: bool, + users: Vec, +} + +impl Default for WebUsers { + fn default() -> Self { + Self { + users: Vec::new(), + allow_unauthenticated_to_view: false, + } + } +} + +impl WebUsers { + fn path() -> Result { + let base_path = crate::EtcLqos::load()?.lqos_directory; + let filename = Path::new(&base_path).join("webusers.toml"); + Ok(filename) + } + + fn save_to_disk(&self) -> Result<()> { + let path = Self::path()?; + let new_contents = toml::to_string(&self)?; + if path.exists() { + remove_file(&path)?; + } + let mut file = OpenOptions::new().write(true).create_new(true).open(path)?; + file.write_all(&new_contents.as_bytes())?; + Ok(()) + } + + pub fn does_users_file_exist() -> Result { + Ok(Self::path()?.exists()) + } + + pub fn load_or_create() -> Result { + let path = Self::path()?; + if !path.exists() { + // Create a new users file, save it and return the + // empty file + let new_users = Self::default(); + new_users.save_to_disk()?; + Ok(new_users) + } else { + // Load from disk + let raw = read_to_string(path)?; + let users = toml::from_str(&raw)?; + Ok(users) + } + } + + fn hash_password(password: &str) -> String { + let salted = format!("!x{password}_LibreQosLikesPasswordsForDinner"); + let mut sha256 = Sha256::new(); + sha256.update(salted); + format!("{:X}", sha256.finalize()) + } + + pub fn add_or_update_user( + &mut self, + username: &str, + password: &str, + role: UserRole, + ) -> Result { + let token ; // Assigned in a branch + if let Some(mut user) = self.users.iter_mut().find(|u| u.username == username) { + user.password_hash = Self::hash_password(password); + user.role = role; + token = user.token.clone(); + } else { + token = Uuid::new_v4().to_string(); + let new_user = WebUser { + username: username.to_string(), + password_hash: Self::hash_password(password), + role, + token: token.clone(), + }; + self.users.push(new_user); + } + + self.save_to_disk()?; + Ok(token) + } + + pub fn remove_user(&mut self, username: &str) -> Result<()> { + let old_len = self.users.len(); + self.users.retain(|u| u.username != username); + if old_len == self.users.len() { + return Err(Error::msg(format!("User {} was not found", username))); + } + self.save_to_disk()?; + Ok(()) + } + + pub fn login(&self, username: &str, password: &str) -> Result { + let hash = Self::hash_password(password); + if let Some(user) = self + .users + .iter() + .find(|u| u.username == username && u.password_hash == hash) + { + Ok(user.token.clone()) + } else { + if self.allow_unauthenticated_to_view { + Ok("default".to_string()) + } else { + Err(Error::msg("Invalid Login")) + } + } + } + + pub fn get_role_from_token(&self, token: &str) -> Result { + if let Some(user) = self + .users + .iter() + .find(|u| u.token == token) + { + Ok(user.role) + } else { + if self.allow_unauthenticated_to_view { + Ok(UserRole::ReadOnly) + } else { + Err(Error::msg("Unknown user token")) + } + } + } + + pub fn get_username(&self, token: &str) -> String { + if let Some(user) = self + .users + .iter() + .find(|u| u.token == token) + { + user.username.clone() + } else { + "Anonymous".to_string() + } + } + + pub fn print_users(&self) -> Result<()> { + self + .users + .iter() + .for_each(|u| { + println!("{:<40} {:<10}", u.username, u.role.to_string()); + }); + Ok(()) + } + + pub fn allow_anonymous(&mut self, allow: bool) -> Result<()> { + self.allow_unauthenticated_to_view = allow; + self.save_to_disk()?; + Ok(()) + } + + pub fn do_we_allow_anonymous(&self) -> bool { + self.allow_unauthenticated_to_view + } +} diff --git a/src/rust/lqos_config/src/lib.rs b/src/rust/lqos_config/src/lib.rs index 60fc7638..278a05a7 100644 --- a/src/rust/lqos_config/src/lib.rs +++ b/src/rust/lqos_config/src/lib.rs @@ -2,8 +2,10 @@ mod etc; mod libre_qos_config; mod shaped_devices; mod program_control; +mod authentication; pub use libre_qos_config::LibreQoSConfig; pub use shaped_devices::{ConfigShapedDevices, ShapedDevice}; pub use program_control::load_libreqos; pub use etc::{EtcLqos, BridgeConfig, Tunables, BridgeInterface, BridgeVlan}; +pub use authentication::{WebUsers, UserRole}; diff --git a/src/rust/lqos_node_manager/src/auth_guard.rs b/src/rust/lqos_node_manager/src/auth_guard.rs new file mode 100644 index 00000000..18644e4a --- /dev/null +++ b/src/rust/lqos_node_manager/src/auth_guard.rs @@ -0,0 +1,128 @@ +use anyhow::Error; +use lazy_static::*; +use lqos_config::{UserRole, WebUsers}; +use parking_lot::Mutex; +use rocket::{ + http::{Status, CookieJar, Cookie}, + request::{FromRequest, Outcome}, + Request +}; +use rocket::serde::{json::Json, Serialize, Deserialize}; + +lazy_static! { + static ref WEB_USERS: Mutex> = Mutex::new(None); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AuthGuard { + Admin, + ReadOnly, + FirstUse, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for AuthGuard { + type Error = anyhow::Error; // Decorated because Error=Error looks odd + + async fn from_request(request: &'r Request<'_>) -> Outcome { + let mut lock = WEB_USERS.lock(); + if lock.is_none() { + if WebUsers::does_users_file_exist().unwrap() { + *lock = Some(WebUsers::load_or_create().unwrap()); + } else { + // There is no user list, so we're redirecting to the + // new user page. + return Outcome::Success(AuthGuard::FirstUse); + } + } + + if let Some(users) = &*lock { + if let Some(token) = request.cookies().get("User-Token") { + match users.get_role_from_token(token.value()) { + Ok(UserRole::Admin) => return Outcome::Success(AuthGuard::Admin), + Ok(UserRole::ReadOnly) => return Outcome::Success(AuthGuard::ReadOnly), + _ => { + return Outcome::Failure(( + Status::Unauthorized, + Error::msg("Invalid token"), + )) + } + } + } else { + // If no login, do we allow anonymous? + if users.do_we_allow_anonymous() { + return Outcome::Success(AuthGuard::ReadOnly); + } + } + } + + Outcome::Failure((Status::Unauthorized, Error::msg("Access Denied"))) + } +} + +impl AuthGuard {} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(crate = "rocket::serde")] +pub struct FirstUser { + pub allow_anonymous: bool, + pub username: String, + pub password: String, +} + +#[post("/api/create_first_user", data="")] +pub fn create_first_user(cookies: &CookieJar, info: Json) -> Json { + if WebUsers::does_users_file_exist().unwrap() { + return Json("ERROR".to_string()); + } + let mut lock = WEB_USERS.lock(); + let mut users = WebUsers::load_or_create().unwrap(); + users.allow_anonymous(info.allow_anonymous).unwrap(); + let token = users.add_or_update_user(&info.username, &info.password, UserRole::Admin).unwrap(); + cookies.add(Cookie::new("User-Token", token)); + *lock = Some(users); + Json("OK".to_string()) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(crate = "rocket::serde")] +pub struct LoginAttempt { + pub username: String, + pub password: String, +} + +#[post("/api/login", data="")] +pub fn login(cookies: &CookieJar, info: Json) -> Json { + let mut lock = WEB_USERS.lock(); + if lock.is_none() { + if WebUsers::does_users_file_exist().unwrap() { + *lock = Some(WebUsers::load_or_create().unwrap()); + } + } + if let Some(users) = &*lock { + if let Ok(token) = users.login(&info.username, &info.password) { + cookies.add(Cookie::new("User-Token", token)); + return Json("OK".to_string()); + } + } + Json("ERROR".to_string()) +} + +#[get("/api/admin_check")] +pub fn admin_check(auth: AuthGuard) -> Json { + match auth { + AuthGuard::Admin => Json(true), + _ => Json(false) + } +} + +#[get("/api/username")] +pub fn username(auth: AuthGuard, cookies: &CookieJar) -> Json { + if let Some(token) = cookies.get("User-Token") { + let lock = WEB_USERS.lock(); + if let Some(users) = &*lock { + return Json(users.get_username(token.value())); + } + } + Json("Anonymous".to_string()) +} \ No newline at end of file diff --git a/src/rust/lqos_node_manager/src/config_control.rs b/src/rust/lqos_node_manager/src/config_control.rs index bfdd46fa..dcc3b6ea 100644 --- a/src/rust/lqos_node_manager/src/config_control.rs +++ b/src/rust/lqos_node_manager/src/config_control.rs @@ -1,17 +1,17 @@ use default_net::get_interfaces; use lqos_config::{LibreQoSConfig, EtcLqos}; use rocket::{fs::NamedFile, serde::json::Json}; -use crate::cache_control::NoCache; +use crate::{cache_control::NoCache, auth_guard::AuthGuard}; // Note that NoCache can be replaced with a cache option // once the design work is complete. #[get("/config")] -pub async fn config_page<'a>() -> NoCache> { +pub async fn config_page<'a>(_auth: AuthGuard) -> NoCache> { NoCache::new(NamedFile::open("static/config.html").await.ok()) } #[get("/api/list_nics")] -pub async fn get_nic_list<'a>() -> NoCache>> { +pub async fn get_nic_list<'a>(_auth: AuthGuard) -> NoCache>> { let mut result = Vec::new(); for eth in get_interfaces().iter() { let mac = if let Some(mac) = ð.mac_addr { @@ -29,14 +29,14 @@ pub async fn get_nic_list<'a>() -> NoCache>> } #[get("/api/python_config")] -pub async fn get_current_python_config() -> NoCache> { +pub async fn get_current_python_config(_auth: AuthGuard) -> NoCache> { let config = lqos_config::LibreQoSConfig::load().unwrap(); println!("{:#?}", config); NoCache::new(Json(config)) } #[get("/api/lqosd_config")] -pub async fn get_current_lqosd_config() -> NoCache> { +pub async fn get_current_lqosd_config(_auth: AuthGuard) -> NoCache> { let config = lqos_config::EtcLqos::load().unwrap(); println!("{:#?}", config); NoCache::new(Json(config)) diff --git a/src/rust/lqos_node_manager/src/main.rs b/src/rust/lqos_node_manager/src/main.rs index fb9f618d..ead8ec91 100644 --- a/src/rust/lqos_node_manager/src/main.rs +++ b/src/rust/lqos_node_manager/src/main.rs @@ -8,6 +8,7 @@ mod cache_control; use rocket_async_compression::Compression; mod queue_info; mod config_control; +mod auth_guard; #[launch] fn rocket() -> _ { @@ -18,6 +19,7 @@ fn rocket() -> _ { rocket::tokio::spawn(tracker::update_tracking()); }) })) + .register("/", catchers![static_pages::login]) .mount("/", routes![ static_pages::index, static_pages::shaped_devices_csv_page, @@ -54,6 +56,11 @@ fn rocket() -> _ { config_control::get_nic_list, config_control::get_current_python_config, config_control::get_current_lqosd_config, + auth_guard::create_first_user, + auth_guard::login, + auth_guard::admin_check, + static_pages::login_page, + auth_guard::username, // Supporting files static_pages::bootsrap_css, diff --git a/src/rust/lqos_node_manager/src/queue_info.rs b/src/rust/lqos_node_manager/src/queue_info.rs index 73382afc..75f6426d 100644 --- a/src/rust/lqos_node_manager/src/queue_info.rs +++ b/src/rust/lqos_node_manager/src/queue_info.rs @@ -3,12 +3,13 @@ use rocket::response::content::RawJson; use rocket::serde::json::Json; use rocket::tokio::io::{AsyncWriteExt, AsyncReadExt}; use rocket::tokio::net::TcpStream; +use crate::auth_guard::AuthGuard; use crate::cache_control::NoCache; use crate::tracker::SHAPED_DEVICES; use std::net::IpAddr; #[get("/api/circuit_name/")] -pub async fn circuit_name(circuit_id: String) -> NoCache> { +pub async fn circuit_name(circuit_id: String, _auth: AuthGuard) -> NoCache> { if let Some(device) = SHAPED_DEVICES.read().devices.iter().find(|d| d.circuit_id == circuit_id) { NoCache::new(Json(device.circuit_name.clone())) } else { @@ -18,7 +19,7 @@ pub async fn circuit_name(circuit_id: String) -> NoCache> { } #[get("/api/circuit_throughput/")] -pub async fn current_circuit_throughput(circuit_id: String) -> NoCache>> { +pub async fn current_circuit_throughput(circuit_id: String, _auth: AuthGuard) -> NoCache>> { let mut result = Vec::new(); // Get a list of host counts // This is really inefficient, but I'm struggling to find a better way. @@ -65,7 +66,7 @@ pub async fn current_circuit_throughput(circuit_id: String) -> NoCache")] -pub async fn raw_queue_by_circuit(circuit_id: String) -> NoCache> { +pub async fn raw_queue_by_circuit(circuit_id: String, _auth: AuthGuard) -> NoCache> { let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap(); let test = BusSession { auth_cookie: 1234, diff --git a/src/rust/lqos_node_manager/src/shaped_devices.rs b/src/rust/lqos_node_manager/src/shaped_devices.rs index bf56659a..7bde0cb7 100644 --- a/src/rust/lqos_node_manager/src/shaped_devices.rs +++ b/src/rust/lqos_node_manager/src/shaped_devices.rs @@ -3,6 +3,7 @@ use lqos_config::ShapedDevice; use rocket::serde::json::Json; use rocket::tokio::io::{AsyncWriteExt, AsyncReadExt}; use rocket::tokio::net::TcpStream; +use crate::auth_guard::AuthGuard; use crate::cache_control::NoCache; use crate::tracker::SHAPED_DEVICES; use lazy_static::*; @@ -13,24 +14,24 @@ lazy_static! { } #[get("/api/all_shaped_devices")] -pub fn all_shaped_devices() -> NoCache>> { +pub fn all_shaped_devices(_auth: AuthGuard) -> NoCache>> { NoCache::new(Json(SHAPED_DEVICES.read().devices.clone())) } #[get("/api/shaped_devices_count")] -pub fn shaped_devices_count() -> NoCache> { +pub fn shaped_devices_count(_auth: AuthGuard) -> NoCache> { NoCache::new(Json(SHAPED_DEVICES.read().devices.len())) } #[get("/api/shaped_devices_range//")] -pub fn shaped_devices_range(start: usize, end: usize) -> NoCache>> { +pub fn shaped_devices_range(start: usize, end: usize, _auth: AuthGuard) -> NoCache>> { let reader = SHAPED_DEVICES.read(); let result: Vec = reader.devices.iter().skip(start).take(end).cloned().collect(); NoCache::new(Json(result)) } #[get("/api/shaped_devices_search/")] -pub fn shaped_devices_search(term: String) -> NoCache>> { +pub fn shaped_devices_search(term: String, _auth: AuthGuard) -> NoCache>> { let term = term.trim().to_lowercase(); let reader = SHAPED_DEVICES.read(); let result: Vec = reader @@ -51,7 +52,10 @@ pub fn reload_required() -> NoCache> { } #[get("/api/reload_libreqos")] -pub async fn reload_libreqos() -> NoCache> { +pub async fn reload_libreqos(auth: AuthGuard) -> NoCache> { + if auth != AuthGuard::Admin { + return NoCache::new(Json("Not authorized".to_string())); + } // Send request to lqosd let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap(); let test = BusSession { diff --git a/src/rust/lqos_node_manager/src/static_pages.rs b/src/rust/lqos_node_manager/src/static_pages.rs index 9533617d..98febb59 100644 --- a/src/rust/lqos_node_manager/src/static_pages.rs +++ b/src/rust/lqos_node_manager/src/static_pages.rs @@ -1,38 +1,55 @@ use rocket::fs::NamedFile; -use crate::cache_control::{LongCache, NoCache}; +use crate::{cache_control::{LongCache, NoCache}, auth_guard::AuthGuard}; // Note that NoCache can be replaced with a cache option // once the design work is complete. #[get("/")] -pub async fn index<'a>() -> NoCache> { - NoCache::new(NamedFile::open("static/main.html").await.ok()) +pub async fn index<'a>(auth: AuthGuard) -> NoCache> { + match auth { + AuthGuard::FirstUse => NoCache::new(NamedFile::open("static/first_run.html").await.ok()), + _ => NoCache::new(NamedFile::open("static/main.html").await.ok()) + } +} + +// Note that NoCache can be replaced with a cache option +// once the design work is complete. +#[catch(401)] +pub async fn login<'a>() -> NoCache> { + NoCache::new(NamedFile::open("static/login.html").await.ok()) +} + +// Note that NoCache can be replaced with a cache option +// once the design work is complete. +#[get("/login")] +pub async fn login_page<'a>() -> NoCache> { + NoCache::new(NamedFile::open("static/login.html").await.ok()) } // Note that NoCache can be replaced with a cache option // once the design work is complete. #[get("/shaped")] -pub async fn shaped_devices_csv_page<'a>() -> NoCache> { +pub async fn shaped_devices_csv_page<'a>(_auth: AuthGuard) -> NoCache> { NoCache::new(NamedFile::open("static/shaped.html").await.ok()) } // Note that NoCache can be replaced with a cache option // once the design work is complete. #[get("/circuit_queue")] -pub async fn circuit_queue<'a>() -> NoCache> { +pub async fn circuit_queue<'a>(_auth: AuthGuard) -> NoCache> { NoCache::new(NamedFile::open("static/circuit_queue.html").await.ok()) } // Note that NoCache can be replaced with a cache option // once the design work is complete. #[get("/unknown")] -pub async fn unknown_devices_page<'a>() -> NoCache> { +pub async fn unknown_devices_page<'a>(_auth: AuthGuard) -> NoCache> { NoCache::new(NamedFile::open("static/unknown-ip.html").await.ok()) } // Note that NoCache can be replaced with a cache option // once the design work is complete. #[get("/shaped-add")] -pub async fn shaped_devices_add_page<'a>() -> NoCache> { +pub async fn shaped_devices_add_page<'a>(_auth: AuthGuard) -> NoCache> { NoCache::new(NamedFile::open("static/shaped-add.html").await.ok()) } diff --git a/src/rust/lqos_node_manager/src/tracker/mod.rs b/src/rust/lqos_node_manager/src/tracker/mod.rs index 9dde9337..75942cc5 100644 --- a/src/rust/lqos_node_manager/src/tracker/mod.rs +++ b/src/rust/lqos_node_manager/src/tracker/mod.rs @@ -5,7 +5,7 @@ pub use cache_manager::update_tracking; use std::net::IpAddr; use lqos_bus::{IpStats, TcHandle}; use rocket::serde::{json::Json, Serialize, Deserialize}; -use crate::tracker::cache::ThroughputPerSecond; +use crate::{tracker::cache::ThroughputPerSecond, auth_guard::AuthGuard}; use self::cache::{CURRENT_THROUGHPUT, THROUGHPUT_BUFFER, CPU_USAGE, MEMORY_USAGE, TOP_10_DOWNLOADERS, WORST_10_RTT, RTT_HISTOGRAM, HOST_COUNTS}; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -49,50 +49,50 @@ impl From<&IpStats> for IpStatsWithPlan { } #[get("/api/current_throughput")] -pub fn current_throughput() -> Json { +pub fn current_throughput(_auth: AuthGuard) -> Json { let result = CURRENT_THROUGHPUT.read().clone(); Json(result) } #[get("/api/throughput_ring")] -pub fn throughput_ring() -> Json> { +pub fn throughput_ring(_auth: AuthGuard) -> Json> { let result = THROUGHPUT_BUFFER.read().get_result(); Json(result) } #[get("/api/cpu")] -pub fn cpu_usage() -> Json> { +pub fn cpu_usage(_auth: AuthGuard) -> Json> { let cpu_usage = CPU_USAGE.read().clone(); Json(cpu_usage) } #[get("/api/ram")] -pub fn ram_usage() -> Json> { +pub fn ram_usage(_auth: AuthGuard) -> Json> { let ram_usage = MEMORY_USAGE.read().clone(); Json(ram_usage) } #[get("/api/top_10_downloaders")] -pub fn top_10_downloaders() -> Json> { +pub fn top_10_downloaders(_auth: AuthGuard) -> Json> { let tt : Vec = TOP_10_DOWNLOADERS.read().iter().map(|tt| tt.into()).collect(); Json(tt) } #[get("/api/worst_10_rtt")] -pub fn worst_10_rtt() -> Json> { +pub fn worst_10_rtt(_auth: AuthGuard) -> Json> { let tt : Vec = WORST_10_RTT.read().iter().map(|tt| tt.into()).collect(); Json(tt) } #[get("/api/rtt_histogram")] -pub fn rtt_histogram() -> Json> { +pub fn rtt_histogram(_auth: AuthGuard) -> Json> { Json(RTT_HISTOGRAM.read().clone()) } #[get("/api/host_counts")] -pub fn host_counts() -> Json<(u32, u32)> { +pub fn host_counts(_auth: AuthGuard) -> Json<(u32, u32)> { let shaped_reader = SHAPED_DEVICES.read(); let n_devices = shaped_reader.devices.len(); let host_counts = HOST_COUNTS.read(); diff --git a/src/rust/lqos_node_manager/src/unknown_devices.rs b/src/rust/lqos_node_manager/src/unknown_devices.rs index f39faaa2..3c03469a 100644 --- a/src/rust/lqos_node_manager/src/unknown_devices.rs +++ b/src/rust/lqos_node_manager/src/unknown_devices.rs @@ -1,19 +1,19 @@ use lqos_bus::IpStats; use rocket::serde::json::Json; -use crate::{cache_control::NoCache, tracker::UNKNOWN_DEVICES}; +use crate::{cache_control::NoCache, tracker::UNKNOWN_DEVICES, auth_guard::AuthGuard}; #[get("/api/all_unknown_devices")] -pub fn all_unknown_devices() -> NoCache>> { +pub fn all_unknown_devices(_auth: AuthGuard) -> NoCache>> { NoCache::new(Json(UNKNOWN_DEVICES.read().clone())) } #[get("/api/unknown_devices_count")] -pub fn unknown_devices_count() -> NoCache> { +pub fn unknown_devices_count(_auth: AuthGuard) -> NoCache> { NoCache::new(Json(UNKNOWN_DEVICES.read().len())) } #[get("/api/unknown_devices_range//")] -pub fn unknown_devices_range(start: usize, end: usize) -> NoCache>> { +pub fn unknown_devices_range(start: usize, end: usize, _auth: AuthGuard) -> NoCache>> { let reader = UNKNOWN_DEVICES.read(); let result: Vec = reader.iter().skip(start).take(end).cloned().collect(); NoCache::new(Json(result)) diff --git a/src/rust/lqos_node_manager/static/circuit_queue.html b/src/rust/lqos_node_manager/static/circuit_queue.html index cf470422..addbab68 100644 --- a/src/rust/lqos_node_manager/static/circuit_queue.html +++ b/src/rust/lqos_node_manager/static/circuit_queue.html @@ -23,6 +23,7 @@ + diff --git a/src/rust/lqos_node_manager/static/config.html b/src/rust/lqos_node_manager/static/config.html index 8f4d8671..7e047e70 100644 --- a/src/rust/lqos_node_manager/static/config.html +++ b/src/rust/lqos_node_manager/static/config.html @@ -23,7 +23,7 @@ - + @@ -56,7 +56,7 @@
Configuration
-
+
Save ispConfig.py  Save /etc/lqos  Reload LibreQoS  @@ -71,6 +71,7 @@ +
@@ -353,6 +354,10 @@ UISP Settings ...
+
+

LibreQos Web Interface Users

+
+
@@ -374,84 +379,100 @@ function start() { colorReloadButton(); updateHostCounts(); - $.get("/api/python_config", (data) => { - python_config = data; - $.get("/api/lqosd_config", (data) => { - lqosd_config = data; - $.get("/api/list_nics", (data) => { - nics = data; - console.log(nics); - fillNicList("nicCore", python_config.isp_interface); - fillNicList("nicInternet", python_config.internet_interface); + $.get("/api/admin_check", (is_admin) => { + if (!is_admin) { + $("#controls").html("

Interface Mapping

"; - html += ""; - html += ""; - html += ""; - for (let i=0; i"; - html += ""; - html += ""; + html += ""; - html += ""; - html += ""; + // Map Bifrost VLAN mappings + html = "

VLAN Mapping

"; + html += "
Input InterfaceOutput InterfaceScan VLANs?
" + buildNICList('bfOut_' + i, lqosd_config.bridge.interface_mapping[i].redirect_to) + ""; + html += "" + buildNICList('bfOut_' + i, lqosd_config.bridge.interface_mapping[i].redirect_to) + ""; - html += "
"; + html += ""; + html += ""; + for (let i=0; i"; + html += ""; + html += ""; + html += ""; + } + html += "
Parent InterfaceInput TagRemapped Tag
"; + $("#bifrostVlans").html(html); } - html += ""; - $("#bifrostVlans").html(html); - } - $("#sqmMode option[value='" + python_config.sqm + "']").prop("selected", true); - $("#maxDownload").val(python_config.total_download_mbps); - $("#maxUpload").val(python_config.total_upload_mbps); - $("#monitorMode").prop('checked', python_config.monitor_mode); - $("#generatedDownload").val(python_config.generated_download_mbps); - $("#generatedUpload").val(python_config.generated_upload_mbps); - $("#binpacking").prop('checked', python_config.use_binpacking); - $("#queuecheckms").val(lqosd_config.queue_check_period_ms); - $("#actualShellCommands").prop('checked', python_config.enable_shell_commands); - $("#useSudo").prop('checked', python_config.run_as_sudo); - $("#overrideQueues").val(python_config.override_queue_count); - $("#stopIrqBalance").prop('checked', lqosd_config.tuning.stop_irq_balance); - $("#netDevUsec").val(lqosd_config.tuning.netdev_budget_usecs); - $("#netDevPackets").val(lqosd_config.tuning.netdev_budget_packets); - $("#rxUsecs").val(lqosd_config.tuning.rx_usecs); - $("#txUsecs").val(lqosd_config.tuning.tx_usecs); - $("#disableRxVlan").prop('checked', lqosd_config.tuning.disable_rxvlan); - $("#disableTxVlan").prop('checked', lqosd_config.tuning.disable_txvlan); - let offloads = ""; - for (let i=0; i + + + + + + + LibreQoS - Local Node Manager + + + + + + +
+
+
+
+
+
+
First Login
+

+ No webusers.toml file was found. This is probably the first time you've run + the LibreQoS web system. If it isn't, then please check permissions on that file and + use the "bin/webusers" command to verify that your system is working. +

+ +

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

+ + + + + + + + + + + +
+ + +
+ Your Username + + +
Your password
+ Create User Account +
+
+
+
+
+
+ +
Copyright (c) 2022, LibreQoE LLC
+ + + + + + + \ No newline at end of file diff --git a/src/rust/lqos_node_manager/static/login.html b/src/rust/lqos_node_manager/static/login.html new file mode 100644 index 00000000..b1ac26c5 --- /dev/null +++ b/src/rust/lqos_node_manager/static/login.html @@ -0,0 +1,72 @@ + + + + + + + + LibreQoS - Local Node Manager + + + + + + +
+
+
+
+
+
+
Login
+

Please enter a username and password to access LibreQoS.

+

You can control access locally with bin/webusers from the console.

+ + + + + + + + + +
Username
Password
+ Login +
+
+
+
+
+
+ +
Copyright (c) 2022, LibreQoE LLC
+ + + + + + + \ No newline at end of file diff --git a/src/rust/lqos_node_manager/static/lqos.js b/src/rust/lqos_node_manager/static/lqos.js index cfab63a9..b06c2d87 100644 --- a/src/rust/lqos_node_manager/static/lqos.js +++ b/src/rust/lqos_node_manager/static/lqos.js @@ -44,12 +44,33 @@ function bindColorToggle() { }); } +function deleteAllCookies() { + const cookies = document.cookie.split(";"); + + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i]; + const eqPos = cookie.indexOf("="); + const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } + window.location.reload(); +} + function updateHostCounts() { $.get("/api/host_counts", (hc) => { $("#shapedCount").text(hc[0]); $("#unshapedCount").text(hc[1]); setTimeout(updateHostCounts, 5000); }); + $.get("/api/username", (un) => { + let html = ""; + if (un == "Anonymous") { + html = " Login"; + } else { + html = " Logout " + un + ""; + } + $("#currentLogin").html(html); + }) } function colorReloadButton() { diff --git a/src/rust/lqos_node_manager/static/main.html b/src/rust/lqos_node_manager/static/main.html index d14f060c..fcd79a4f 100644 --- a/src/rust/lqos_node_manager/static/main.html +++ b/src/rust/lqos_node_manager/static/main.html @@ -23,6 +23,7 @@ + diff --git a/src/rust/lqos_node_manager/static/shaped-add.html b/src/rust/lqos_node_manager/static/shaped-add.html index 3e55af6a..20860e7f 100644 --- a/src/rust/lqos_node_manager/static/shaped-add.html +++ b/src/rust/lqos_node_manager/static/shaped-add.html @@ -23,7 +23,7 @@ - + diff --git a/src/rust/lqos_node_manager/static/shaped.html b/src/rust/lqos_node_manager/static/shaped.html index 34b8aec5..4e8bb4ce 100644 --- a/src/rust/lqos_node_manager/static/shaped.html +++ b/src/rust/lqos_node_manager/static/shaped.html @@ -23,7 +23,7 @@ - + diff --git a/src/rust/lqos_node_manager/static/unknown-ip.html b/src/rust/lqos_node_manager/static/unknown-ip.html index 3b82b234..478d2571 100644 --- a/src/rust/lqos_node_manager/static/unknown-ip.html +++ b/src/rust/lqos_node_manager/static/unknown-ip.html @@ -23,7 +23,7 @@ - + diff --git a/src/rust/webusers/Cargo.toml b/src/rust/webusers/Cargo.toml new file mode 100644 index 00000000..a20bef3f --- /dev/null +++ b/src/rust/webusers/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "webusers" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4", features = ["derive"] } +lqos_config = { path = "../lqos_config" } +anyhow = "1" diff --git a/src/rust/webusers/src/main.rs b/src/rust/webusers/src/main.rs new file mode 100644 index 00000000..21102ca0 --- /dev/null +++ b/src/rust/webusers/src/main.rs @@ -0,0 +1,59 @@ +use std::process::exit; +use clap::{Parser, Subcommand}; +use anyhow::Result; +use lqos_config::{WebUsers, UserRole}; + +#[derive(Parser)] +#[command()] +struct Args { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Add or update a user + Add { + /// Username + #[arg(long)] + username: String, + + /// Role + #[arg(long)] + role: UserRole, + + /// CPU id to connect + #[arg(long)] + password: String, + }, + /// Remove a user + Del { + /// Username to remove + username: String, + }, + /// List all mapped IPs. + List, +} + +fn main() -> Result<()> { + let cli = Args::parse(); + let mut users = WebUsers::load_or_create()?; + match cli.command { + Some(Commands::Add { username, role, password }) => { + users.add_or_update_user(&username, &password, role)?; + } + Some(Commands::Del { username }) => { + users.remove_user(&username)?; + } + Some(Commands::List) => { + println!("All Users\n"); + users.print_users()?; + } + None => { + println!("Run with --help to see instructions"); + exit(0); + } + } + + Ok(()) +}