fix(swarm-authelia-bridge): lock the load-insert-publish sequence

This commit is contained in:
damocles 2026-08-16 22:30:57 +02:00 committed by mara
commit 405ed85550

View file

@ -86,6 +86,17 @@ fn read_secret_file(path: &str) -> Result<String> {
struct AppState {
config: Config,
http: reqwest::Client,
/// Serializes `handle`'s load → insert → publish sequence. Without this,
/// two `EnsureAgentIdentity` requests landing close together (real: the
/// controller's job worker claims and spawns nodes without waiting for
/// each to finish, and `SwarmResourceKind` declares no resource dep
/// between two `CreateIdentity` jobs, so they run concurrently) both
/// `load_store` the same snapshot, both insert their own agent, and
/// 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`.
write_lock: tokio::sync::Mutex<()>,
}
#[tokio::main]
@ -101,6 +112,7 @@ async fn main() -> Result<()> {
let state = Arc::new(AppState {
config,
http: reqwest::Client::new(),
write_lock: tokio::sync::Mutex::new(()),
});
let app = Router::new()
@ -187,6 +199,14 @@ async fn authorize(state: &AppState, headers: &HeaderMap) -> Result<(), Response
async fn handle(state: &AppState, name: String) -> Result<BridgeResponse> {
store::validate_username(&name)?;
let cfg = &state.config;
// Held across the whole load → insert → publish sequence, not just the
// write — two concurrent `EnsureAgentIdentity` calls must not both read
// the same on-disk snapshot before either publishes. See the field's
// doc comment on `AppState::write_lock` for why this is reachable in
// practice, not just theoretically.
let _write_guard = state.write_lock.lock().await;
let mut user_store = store::load_store(&cfg.store_path, &cfg.users_file)?;
if user_store.users.contains_key(&name) {
return Ok(BridgeResponse::AlreadyExists);