Skip to content

Commit

Permalink
authentication: auto reload users
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
svenrademakers committed Nov 3, 2023
1 parent 3ed7b5c commit 2f36fdc
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 3 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions src/authentication/authentication_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ where
}
}

pub fn reload_password_cache(
&mut self,
password_entries: impl Iterator<Item = (String, String)>,
) {
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
Expand Down
54 changes: 51 additions & 3 deletions src/authentication/linux_authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,6 +35,8 @@ use tokio::{
sync::Mutex,
};

const SHADOW_FILE: &str = "/etc/shadow";

type LinuxContext = AuthenticationContext<UnixValidator>;

pub struct LinuxAuthenticator {
Expand All @@ -48,21 +53,64 @@ impl LinuxAuthenticator {
authentication_attemps: usize,
) -> io::Result<Self> {
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,
authentication_attemps,
))),
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<impl Iterator<Item = (String, String)>> {
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);
Expand Down

0 comments on commit 2f36fdc

Please sign in to comment.