swarmctl and the authelia bridge serialise users.yml writes with a shared flock
This commit is contained in:
parent
b4c5954491
commit
be744d45b9
7 changed files with 436 additions and 5 deletions
|
|
@ -98,7 +98,8 @@ struct AppState {
|
|||
/// whichever `publish`es second silently drops the first agent's entry —
|
||||
/// the store is a plain file, not a database with its own concurrency
|
||||
/// control. `tokio::sync::Mutex`, not `std`'s: held across the `.await`s
|
||||
/// in `generate_password` and `publish`.
|
||||
/// in `generate_password` and `publish`. It covers this process only;
|
||||
/// `store::lock_users_file` is the lock against `swarmctl`.
|
||||
write_lock: tokio::sync::Mutex<()>,
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +317,10 @@ async fn handle(
|
|||
// doc comment on `AppState::write_lock` for why this is reachable in
|
||||
// practice, not just theoretically.
|
||||
let _write_guard = write_lock.lock().await;
|
||||
// The in-process lock does not cover `swarmctl`, which rewrites the same
|
||||
// file from the host: the `flock` does, and is held to the end of the
|
||||
// function like the guard above.
|
||||
let _file_lock = store::lock_users_file(&cfg.users_file, store::USERS_LOCK_WAIT).await?;
|
||||
|
||||
let mut user_store = store::load_store(&cfg.users_file)?;
|
||||
if user_store.users.contains_key(&name) {
|
||||
|
|
@ -443,7 +448,7 @@ async fn generate_password(bin: &std::path::Path) -> Result<String> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
BridgeRequest, Config, Refusal, StatusCode, classify, introspect::Verdict, respond,
|
||||
BridgeRequest, Config, Refusal, StatusCode, classify, handle, introspect::Verdict, respond,
|
||||
};
|
||||
|
||||
const CONTROLLER: &str = "swarm-controller";
|
||||
|
|
@ -586,4 +591,59 @@ mod tests {
|
|||
);
|
||||
let _ = std::fs::remove_dir_all(config.users_file.parent().unwrap());
|
||||
}
|
||||
|
||||
/// `handle` waits for the `flock` `swarmctl` takes, not only for the
|
||||
/// in-process lock: it is held here while `handle` runs with a lock of
|
||||
/// its own that nothing contends.
|
||||
#[tokio::test]
|
||||
async fn an_identity_request_waits_for_the_users_file_lock() {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
use std::time::Duration;
|
||||
|
||||
let mut config = config_with_one_operator();
|
||||
let dir = config.users_file.parent().unwrap().to_owned();
|
||||
let authelia = dir.join("authelia");
|
||||
std::fs::write(
|
||||
&authelia,
|
||||
"#!/bin/sh\nprintf 'Random Password: pw\\nDigest: $argon2id$x\\n'\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::set_permissions(&authelia, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
config.authelia_bin = authelia;
|
||||
|
||||
let held = crate::store::lock_users_file(&config.users_file, Duration::from_secs(1))
|
||||
.await
|
||||
.unwrap();
|
||||
let write_lock = std::sync::Arc::new(tokio::sync::Mutex::new(()));
|
||||
let request = {
|
||||
let config = std::sync::Arc::new(config);
|
||||
let write_lock = std::sync::Arc::clone(&write_lock);
|
||||
let users_file = config.users_file.clone();
|
||||
(
|
||||
tokio::spawn(async move {
|
||||
handle(&config, &write_lock, "atlas".to_owned())
|
||||
.await
|
||||
.map(|_| ())
|
||||
}),
|
||||
users_file,
|
||||
)
|
||||
};
|
||||
let (mut request, users_file) = request;
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(300), &mut request)
|
||||
.await
|
||||
.is_err(),
|
||||
"the request finished while another process held the lock"
|
||||
);
|
||||
drop(held);
|
||||
request.await.unwrap().expect("request after release");
|
||||
assert!(
|
||||
crate::store::load_store(&users_file)
|
||||
.unwrap()
|
||||
.users
|
||||
.contains_key("atlas")
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(users_file.parent().unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue