From 2f36fdc597ebbaeb13e0a53f71cd5ca7a28178d3 Mon Sep 17 00:00:00 2001 From: Sven Rademakers Date: Fri, 3 Nov 2023 13:30:34 +0000 Subject: [PATCH] authentication: auto reload users Reload the internal cache when an users are added or removed. This prevents granting stale users access the API + UI. Implementation is based on `inotify` subsystem. --- Cargo.lock | 23 +++++++++ Cargo.toml | 1 + src/authentication/authentication_context.rs | 7 +++ src/authentication/linux_authenticator.rs | 54 ++++++++++++++++++-- 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0515637..2c4df28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -533,6 +533,7 @@ dependencies = [ "humansize", "humantime", "if-addrs", + "inotify", "log", "nix 0.27.1", "once_cell", @@ -1267,6 +1268,28 @@ dependencies = [ "serde", ] +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index f7e6e22..4795f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ humansize = "2.1.3" actix-multipart = "0.6.1" async-trait = "0.1.74" humantime = "2.1.0" +inotify = "0.10.2" [dev-dependencies] tempdir = "0.3.7" diff --git a/src/authentication/authentication_context.rs b/src/authentication/authentication_context.rs index ae07086..d906511 100644 --- a/src/authentication/authentication_context.rs +++ b/src/authentication/authentication_context.rs @@ -55,6 +55,13 @@ where } } + pub fn reload_password_cache( + &mut self, + password_entries: impl Iterator, + ) { + self.passwds = HashMap::from_iter(password_entries); + } + /// This function piggy-backs removes of expired tokens on an authentication /// request. This imposes a small penalty on each request. Its deemed not /// significant enough to justify optimization given the expected volume diff --git a/src/authentication/linux_authenticator.rs b/src/authentication/linux_authenticator.rs index 35d1394..4d54b9c 100644 --- a/src/authentication/linux_authenticator.rs +++ b/src/authentication/linux_authenticator.rs @@ -20,6 +20,9 @@ use actix_web::{ dev::{Service, ServiceRequest, ServiceResponse, Transform}, Error, }; +use futures::StreamExt; +use inotify::WatchMask; +use inotify::{EventMask, Inotify}; use std::{ future::{ready, Ready}, io, @@ -32,6 +35,8 @@ use tokio::{ sync::Mutex, }; +const SHADOW_FILE: &str = "/etc/shadow"; + type LinuxContext = AuthenticationContext; pub struct LinuxAuthenticator { @@ -48,7 +53,8 @@ impl LinuxAuthenticator { authentication_attemps: usize, ) -> io::Result { let password_entries = Self::parse_shadow_file().await?; - Ok(Self { + + let instance = Self { context: Arc::new(Mutex::new(LinuxContext::with_unix_validator( password_entries, authentication_token_duration, @@ -56,13 +62,55 @@ impl LinuxAuthenticator { ))), authentication_path, realm, - }) + }; + + if let Err(e) = instance.auto_reload().await { + log::warn!("auto reloading of password-cache disabled: {}", e); + } + + Ok(instance) } } impl LinuxAuthenticator { + /// Watches for any changes in the shadow file and reloads the password + /// cache when a change is detected. + async fn auto_reload(&self) -> std::io::Result<()> { + let inotify = Inotify::init()?; + let mask = WatchMask::DELETE_SELF | WatchMask::CLOSE_WRITE; + + inotify.watches().add(SHADOW_FILE, mask)?; + let buffer = [0; 256]; + let mut event_stream = inotify.into_event_stream(buffer)?; + + let context = self.context.clone(); + tokio::spawn(async move { + while let Some(Ok(event)) = event_stream.next().await { + if EventMask::DELETE_SELF == event.mask { + event_stream + .watches() + .add(SHADOW_FILE, mask) + .expect("error rebinding shadow file watcher"); + continue; + } + + let mut lock = context.lock().await; + Self::parse_shadow_file().await.map_or_else( + |e| log::error!("error parsing {}:{}", SHADOW_FILE, e), + |entries| { + lock.reload_password_cache(entries); + log::info!("reloaded user cache"); + }, + ); + } + log::warn!("exited /etc/shadow watcher"); + }); + + Ok(()) + } + async fn parse_shadow_file() -> io::Result> { - let file = OpenOptions::new().read(true).open("/etc/shadow").await?; + let file = OpenOptions::new().read(true).open(SHADOW_FILE).await?; let mut password_hashes: Vec<(String, String)> = Vec::new(); let mut read_buffer = BufReader::new(file);