//! Wire types for the `swarm-authelia-bridge` socket. //! //! Both `swarm-authelia-bridge` (server) and `swarm-controller` (client) //! import these so the shapes stay in sync. No server or client protocol //! logic lives here, only the JSON contract carried as the body of the //! bridge's one HTTP endpoint (`POST /requests`, bearer-authenticated) — //! see `swarm-authelia-bridge`'s own docs for the transport. //! //! # Why a bridge at all, and why this shape //! //! `swarm-authelia`'s users database (`users.yml`) is owned by the //! `authelia-swarm` system user, a different uid than `swarm-controller`'s //! own — so `swarm-controller` cannot write it directly without either root //! (`CAP_CHOWN`) or a shared group, both rejected for the same reason //! `swarmctl`'s own README already rejected them for this exact file. The //! fix taken instead: run this bridge's own //! systemd unit as `User = "authelia-swarm";` — the literal name //! `swarm-authelia.nix` already derives, resolved by systemd at start, no //! numeric uid ever hand-pinned into nix eval — so the bridge simply *owns* //! the file it writes. Fully ordinary permissions, no capabilities, no root. //! //! **Per-operation, not wholesale-replace.** [`BridgeRequest::EnsureAgentIdentity`] //! asks for one user to exist; the bridge owns both `users.json` (canonical) //! and rendering `users.yml` internally. A caller never sends rendered YAML //! or a file blob — that would invite a last-writer-wins race between //! independent callers and duplicate the rendering logic on both sides of //! the wire. use serde::{Deserialize, Serialize}; /// A request to the bridge. One variant today — see the module doc for why /// this isn't a file-replace API. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum BridgeRequest { /// Idempotently ensure `name` exists as an authelia subject — /// `swarm-controller`'s `SwarmNodeKind::CreateIdentity` node's entire /// job. Idempotence is load-bearing (agent creation's own design /// consensus): re-running this for an agent that already has an /// identity is a /// genuine no-op, reported as [`BridgeResponse::AlreadyExists`] — no /// password re-mint, no `users.yml` rewrite, nothing for authelia's /// `file.watch` to react to. EnsureAgentIdentity { /// The agent's name — becomes the authelia username verbatim. The /// bridge validates this server-side (same conservative charset /// `swarmctl::users::validate_username` already enforces); this /// crate carries the wire shape only, not the validation rule. name: String, }, } /// The bridge's answer to a [`BridgeRequest`]. /// /// `#[serde(tag = "status")]` rather than a bare `Result`-shaped wrapper: an /// external tag reads directly as one of three named outcomes on the wire /// (`{"status":"created",...}`), with no separate "was this an error" /// boolean to keep in sync with which variant it is. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "status", rename_all = "snake_case")] pub enum BridgeResponse { /// The agent had no identity yet; one was minted and `users.yml` was /// rewritten. Created, /// The agent already had an identity. No write happened — the /// idempotent no-op path. AlreadyExists, /// The request was rejected or the write failed. Carries a message /// for the caller to log/propagate, not a typed error enum: the /// failure modes here (bad username, authelia binary failed, disk /// full) have no caller-actionable distinction today — see /// `PrivResponse` in `hive-priv-sock` for the same reasoning. Error { message: String }, } #[cfg(test)] mod tests { use super::{BridgeRequest, BridgeResponse}; /// Pins the external-tag wire shape — a reader off the wire (or a log /// line) should be able to tell the three outcomes apart without /// cross-referencing this crate's source. #[test] fn response_variants_tag_on_status() { let created = serde_json::to_value(BridgeResponse::Created).unwrap(); assert_eq!(created, serde_json::json!({"status": "created"})); let exists = serde_json::to_value(BridgeResponse::AlreadyExists).unwrap(); assert_eq!(exists, serde_json::json!({"status": "already_exists"})); let err = serde_json::to_value(BridgeResponse::Error { message: "boom".to_owned(), }) .unwrap(); assert_eq!( err, serde_json::json!({"status": "error", "message": "boom"}) ); } #[test] fn request_round_trips() { let req = BridgeRequest::EnsureAgentIdentity { name: "atlas".to_owned(), }; let json = serde_json::to_string(&req).unwrap(); let back: BridgeRequest = serde_json::from_str(&json).unwrap(); let BridgeRequest::EnsureAgentIdentity { name } = back; assert_eq!(name, "atlas"); } }