gateway: serve the hive dashboard on <hive>.hives.<swarm> behind authelia, drop htpasswd
The dashboard, its API, the swagger UI and the agent routes move to their own vhost, `swarm.dashboardDomain` = `<hive>.hives.<swarm.domain>`, under the swarm domain so authelia's session cookie reaches it. `/`, `/api/` and `/api/docs/` ask authelia through `auth_request`, and authelia's `operatorsOnly` rule for each hive's dashboard name lets only the admins group in. The hive domain keeps matrix discovery and redirects every other path to the new name. The name rides as a SAN on the hive's own gateway leaf: the per-hive bao role allows it, and hive-tls asks for it and checks coverage. HTTP Basic auth is gone: the htpasswd writer and its test, the `hivectl gateway` verbs and host-socket requests, the `gateway.auth` options (now a removed-option error), the unauthorized page and the bcrypt dependency. tmpfiles deletes a leftover gateway.htpasswd, which held bcrypt hashes at 0644. c0re hands out dashboard and agent URLs on the new name (HYPERHIVE_DASHBOARD_DOMAIN), and the swarm controller's hive roster carries each hive's dashboard name for the swarm UI's hive list. Closes #3469
This commit is contained in:
parent
09ad7386b2
commit
92559af8e7
47 changed files with 351 additions and 623 deletions
|
|
@ -45,13 +45,6 @@ pub enum Cmd {
|
|||
#[command(subcommand)]
|
||||
cmd: MatrixCmd,
|
||||
},
|
||||
/// Gateway htpasswd user management.
|
||||
///
|
||||
/// Add, remove, or list users for the gateway's HTTP Basic auth.
|
||||
Gateway {
|
||||
#[command(subcommand)]
|
||||
cmd: GatewayCmd,
|
||||
},
|
||||
/// Lifecycle actions on ONE managed agent container. Needs the
|
||||
/// hive-c0re daemon running.
|
||||
///
|
||||
|
|
@ -178,7 +171,7 @@ pub enum Cmd {
|
|||
/// Which hive web surface `hivectl open` targets.
|
||||
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
|
||||
pub enum OpenTarget {
|
||||
/// The operator dashboard (`https://<domain>/`).
|
||||
/// The operator dashboard (`https://<hive>.hives.<swarm domain>/`).
|
||||
Home,
|
||||
/// The forge (Forgejo) web UI.
|
||||
Forge,
|
||||
|
|
@ -279,34 +272,6 @@ pub enum MatrixCmd {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum GatewayCmd {
|
||||
/// Add a user or update an existing user's password in the gateway
|
||||
/// htpasswd.
|
||||
///
|
||||
/// Use `--password-stdin` to keep the password out of shell history.
|
||||
CreateUser {
|
||||
/// Username to add or update.
|
||||
username: String,
|
||||
/// Set the password inline. WARNING: visible in shell history and
|
||||
/// process listings — prefer `--password-stdin` for sensitive input.
|
||||
/// Mutually exclusive with `--password-stdin`.
|
||||
#[arg(long, conflicts_with = "password_stdin")]
|
||||
password: Option<String>,
|
||||
/// Read the password from stdin (single line, trailing newline
|
||||
/// stripped). Mutually exclusive with `--password`.
|
||||
#[arg(long)]
|
||||
password_stdin: bool,
|
||||
},
|
||||
/// Remove a user from the gateway htpasswd.
|
||||
DeleteUser {
|
||||
/// Username to remove.
|
||||
username: String,
|
||||
},
|
||||
/// List all gateway htpasswd usernames, one per line.
|
||||
ListUsers,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum WgCmd {
|
||||
/// Generate this hive's WireGuard key (if absent) and print its public
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
//! `hivectl gateway` — htpasswd user management for the gateway's HTTP
|
||||
//! Basic auth. The daemon owns the bcrypt hash + htpasswd write (see the
|
||||
//! `Gateway*User` `HostRequest`s); hivectl resolves the password
|
||||
//! client-side and relays the request over the host admin socket.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::util::{daemon_request, resolve_password};
|
||||
|
||||
pub(crate) async fn gateway_create_user(
|
||||
socket: &Path,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
password_stdin: bool,
|
||||
) -> Result<()> {
|
||||
let pw = resolve_password(password, password_stdin)?.ok_or_else(|| {
|
||||
anyhow::anyhow!("a password is required — pass --password or --password-stdin")
|
||||
})?;
|
||||
daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::GatewayCreateUser {
|
||||
username: username.to_owned(),
|
||||
password: pw,
|
||||
},
|
||||
"gateway",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn gateway_delete_user(socket: &Path, username: &str) -> Result<()> {
|
||||
daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::GatewayDeleteUser {
|
||||
username: username.to_owned(),
|
||||
},
|
||||
"gateway",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn gateway_list_users(socket: &Path) -> Result<()> {
|
||||
daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::GatewayListUsers,
|
||||
"gateway",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -26,13 +26,11 @@ mod client;
|
|||
/// Rebuild-queue node progress rendering (`wait_for_nodes` + the spinner /
|
||||
/// plain renderers), split out to keep this file manageable.
|
||||
mod dag_progress;
|
||||
use cli::{Cli, Cmd, ForgeCmd, GatewayCmd, WgCmd};
|
||||
use cli::{Cli, Cmd, ForgeCmd, WgCmd};
|
||||
mod completions;
|
||||
mod quota;
|
||||
mod util;
|
||||
use completions::generate_completions;
|
||||
mod gateway;
|
||||
use gateway::{gateway_create_user, gateway_delete_user, gateway_list_users};
|
||||
mod matrix;
|
||||
use matrix::run_matrix_cmd;
|
||||
mod open;
|
||||
|
|
@ -68,15 +66,6 @@ async fn main() -> Result<()> {
|
|||
} => forge_reconcile_config(&socket, &agent, from, verbose).await,
|
||||
},
|
||||
Cmd::Matrix { cmd } => run_matrix_cmd(&socket, cmd).await,
|
||||
Cmd::Gateway { cmd } => match cmd {
|
||||
GatewayCmd::CreateUser {
|
||||
username,
|
||||
password,
|
||||
password_stdin,
|
||||
} => gateway_create_user(&socket, &username, password.as_deref(), password_stdin).await,
|
||||
GatewayCmd::DeleteUser { username } => gateway_delete_user(&socket, &username).await,
|
||||
GatewayCmd::ListUsers => gateway_list_users(&socket).await,
|
||||
},
|
||||
Cmd::Agent { name, cmd } => run_agent(&socket, &name, cmd).await,
|
||||
Cmd::ListAgents { json } => agents_list(&socket, json).await,
|
||||
Cmd::QuotaEnable => quota::quota_enable(&socket).await,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub(crate) async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
|
|||
let (url, hint) = match target {
|
||||
OpenTarget::Home => (
|
||||
urls.home,
|
||||
"the dashboard URL needs `services.hyperhive.domain` to be set",
|
||||
"the dashboard URL needs `services.hyperhive.swarm.domain` set and this hive in `swarm.hives`",
|
||||
),
|
||||
OpenTarget::Forge => (
|
||||
urls.forge,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Shared helpers used across hivectl's command handling: daemon
|
||||
//! request/response plumbing (`daemon_request`, `render`,
|
||||
//! `render_lifecycle`) and password input (`resolve_password`).
|
||||
//! `render_lifecycle`).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ pub(crate) fn parse_ident(name: &str) -> Result<hive_types::Ident> {
|
|||
|
||||
/// Send a provisioning request to the daemon and print its result lines.
|
||||
/// The daemon owns the provisioning logic; hivectl just relays the outcome,
|
||||
/// prefixing any error with `label` (e.g. `forge` / `gateway`).
|
||||
/// prefixing any error with `label` (e.g. `forge` / `quota`).
|
||||
pub(crate) async fn daemon_request(
|
||||
socket: &Path,
|
||||
req: hive_host_sock::HostRequest,
|
||||
|
|
@ -40,35 +40,6 @@ pub(crate) async fn daemon_request(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve the password the caller asked for, or return `None` to fall
|
||||
/// back to a random throwaway. `--password <PW>` wins outright;
|
||||
/// `--password-stdin` reads one line off stdin (trailing newline
|
||||
/// stripped). Empty stdin is treated as an error so the caller doesn't
|
||||
/// silently provision an empty-password account.
|
||||
pub(crate) fn resolve_password(
|
||||
password: Option<&str>,
|
||||
password_stdin: bool,
|
||||
) -> Result<Option<String>> {
|
||||
if let Some(p) = password {
|
||||
return Ok(Some(p.to_owned()));
|
||||
}
|
||||
if password_stdin {
|
||||
use std::io::BufRead as _;
|
||||
let stdin = std::io::stdin();
|
||||
let mut line = String::new();
|
||||
stdin
|
||||
.lock()
|
||||
.read_line(&mut line)
|
||||
.context("read password from stdin")?;
|
||||
let trimmed = line.trim_end_matches(['\r', '\n']).to_owned();
|
||||
if trimmed.is_empty() {
|
||||
bail!("--password-stdin: empty input");
|
||||
}
|
||||
return Ok(Some(trimmed));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Pretty-print a `HostResponse` as JSON and bail on failure. Used by the
|
||||
/// agent-lifecycle verbs that just relay a daemon result verbatim.
|
||||
pub(crate) fn render(resp: hive_host_sock::HostResponse) -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue