swarm-controller: CreateIdentity node, auth-bridge client, POST /api/agents
This commit is contained in:
parent
c1eb6b9834
commit
d30f149338
5 changed files with 289 additions and 55 deletions
105
swarm-controller/src/auth.rs
Normal file
105
swarm-controller/src/auth.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//! Client for `swarm-authelia-bridge` — the only writer of the swarm's
|
||||
//! authelia users database (see that crate's README for why this daemon
|
||||
//! cannot write it directly).
|
||||
//!
|
||||
//! Authenticated with THIS daemon's own queue OIDC identity
|
||||
//! (`SWARM_CONTROLLER_OIDC_*`, the same one `swarm-queue-client` mints for
|
||||
//! the queue connection) — "one identity per principal" means a second
|
||||
//! op that needs to prove who this process is reuses the identity it
|
||||
//! already has rather than provisioning a new one. A fresh token is
|
||||
//! minted per call for the same reason the queue client mints one per
|
||||
//! connection attempt: no window in which this process holds a token
|
||||
//! that outlives its intended use.
|
||||
//!
|
||||
//! `None` when this deployment did not wire a bridge up — the bridge only
|
||||
//! exists on hosts that also run `swarm-authelia`, so a controller split
|
||||
//! from it simply has no identity-creation capability yet
|
||||
//! (`SwarmNodeKind::CreateIdentity` fails such a job explicitly rather
|
||||
//! than this module papering over the gap).
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use swarm_authelia_bridge_sock::{BridgeRequest, BridgeResponse};
|
||||
|
||||
/// A configured connection to `swarm-authelia-bridge`.
|
||||
#[derive(Clone)]
|
||||
pub struct AuthBridge {
|
||||
http: reqwest::Client,
|
||||
base_url: String,
|
||||
queue_cfg: swarm_queue_client::QueueConfig,
|
||||
}
|
||||
|
||||
impl AuthBridge {
|
||||
/// Read `SWARM_CONTROLLER_AUTH_BRIDGE_URL`; `Ok(None)` when unset.
|
||||
///
|
||||
/// The queue identity (`SWARM_CONTROLLER_OIDC_*`) is not optional once
|
||||
/// the bridge URL is set: the nix module sets `queueEnv` unconditionally
|
||||
/// for every controller (the queue is required, not just co-located
|
||||
/// service), so a bridge URL with no queue identity to authenticate
|
||||
/// with is a deployment bug, not an absent-feature case — hence the
|
||||
/// hard error rather than a second `None`.
|
||||
pub fn from_env() -> Result<Option<Self>> {
|
||||
let Ok(base_url) = std::env::var("SWARM_CONTROLLER_AUTH_BRIDGE_URL") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let queue_cfg = swarm_queue_client::QueueConfig::from_env("SWARM_CONTROLLER")
|
||||
.context("reading the queue OIDC identity the auth bridge authenticates with")?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"SWARM_CONTROLLER_AUTH_BRIDGE_URL is set but SWARM_CONTROLLER_OIDC_* is \
|
||||
not — the bridge is authenticated with this daemon's queue identity, so \
|
||||
that identity must exist first"
|
||||
)
|
||||
})?;
|
||||
Ok(Some(Self {
|
||||
http: reqwest::Client::new(),
|
||||
base_url,
|
||||
queue_cfg,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Idempotently ensure `name` exists as an authelia subject.
|
||||
pub async fn ensure_agent_identity(&self, name: &str) -> Result<BridgeResponse> {
|
||||
// `mint_token_for` builds its own HTTP client (trusting the queue's
|
||||
// configured CA, if any) — deliberately not `self.http`, which is
|
||||
// the bridge's own client and has nothing to do with authelia's
|
||||
// token endpoint's trust anchors.
|
||||
let token = swarm_queue_client::mint_token_for(&self.queue_cfg)
|
||||
.await
|
||||
.context("minting a bearer token for swarm-authelia-bridge")?;
|
||||
|
||||
let response = self
|
||||
.http
|
||||
.post(format!("{}/requests", self.base_url))
|
||||
.bearer_auth(token)
|
||||
.json(&BridgeRequest::EnsureAgentIdentity {
|
||||
name: name.to_owned(),
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("calling swarm-authelia-bridge")?;
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("swarm-authelia-bridge refused the request ({status}): {body}");
|
||||
}
|
||||
|
||||
serde_json::from_str(&body).context("parsing swarm-authelia-bridge's response")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The common case: no bridge wired up for this deployment.
|
||||
#[test]
|
||||
fn absent_url_is_not_an_error() {
|
||||
assert!(std::env::var("SWARM_CONTROLLER_AUTH_BRIDGE_URL").is_err());
|
||||
assert!(
|
||||
AuthBridge::from_env()
|
||||
.expect("absent is not an error")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue