From 5ec0ce90fdf54e100760ccc6fd3cf8d1119c5eca Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 21 Sep 2026 18:08:56 +0200 Subject: [PATCH] nix: make swarm.authelia.url non-nullable, trim its docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review response on #4620: not having SSO is not a supported deployment, so the type should not permit it, and the docs paragraph explaining why SSO is always present is redundant once the type says so. - swarm.authelia.url drops types.nullOr. - Every consumer's null-arm is gone: two option defaults (swarm-controller's and swarm's own statusPublish.tokenEndpoint) that produced an empty/null placeholder when the URL was null now unconditionally compute the real derived URL. Five now-dead "assertion = ... != null" guards (swarm-authelia's bridge, swarm-grafana, swarm-otel, swarm-nats, hive-forge, hive-matrix) are removed as unreachable — in every case the same URL was already interpolated unconditionally a few lines below the guard. - grafanaNoSso, the module-eval fixture whose sole purpose was exercising the now-unsupported no-IdP refusal, is removed along with its dedicated test case; swarm.authelia.url = null is a type error now, not a value that reaches that assertion. - docs/swarm/services.md: cut the clause about setting the option to null and the sentence explaining why the URL is co-location- independent — both redundant now that the type enforces it. --- docs/swarm/services.md | 10 +++--- nix/host-modules/hive-forge/default.nix | 14 -------- nix/host-modules/hive-matrix.nix | 14 -------- nix/host-modules/swarm-authelia.nix | 19 ++--------- nix/host-modules/swarm-controller.nix | 13 ++++---- nix/host-modules/swarm-grafana.nix | 43 +++---------------------- nix/host-modules/swarm-nats.nix | 12 ------- nix/host-modules/swarm-otel.nix | 16 --------- nix/host-modules/swarm.nix | 4 +-- nix/module-eval/grafana.nix | 30 +---------------- 10 files changed, 21 insertions(+), 154 deletions(-) diff --git a/docs/swarm/services.md b/docs/swarm/services.md index 5e39a192..0ad85e84 100644 --- a/docs/swarm/services.md +++ b/docs/swarm/services.md @@ -69,13 +69,11 @@ provider, differentiated by roles and claims rather than by mechanism — there is one IdP and one auth path. - **`deploy.authelia`** — run the container here. -- **`swarm.authelia.url`** — where clients go to authenticate. - Present on **every** hive and the same value on all of them: +- **`swarm.authelia.url`** — where clients go to authenticate. Present + on **every** hive and the same value on all of them: `https://`, whether or not this host runs the - container. The name is what a client is given; resolution decides which - address it reaches. Set it explicitly when joining a swarm whose IdP is - under another name, or to `null` to say the swarm has no SSO — consumers - then refuse rather than guessing an address. + container. Set it explicitly when joining a swarm whose IdP is under + another name. swarm-controller writes the users database, not by hand: hive-c0re creates and destroys agents continuously, so the subject set is dynamic. diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 1b22d878..4a7f29de 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -573,20 +573,6 @@ in host: see docs/swarm/ for which secret goes where. ''; } - { - # Without a provider URL there is nothing to discover against, - # and the rendered unit would ask `null/.well-known/…`. - assertion = autheliaUrl != null; - message = '' - The forge's SSO login source requires - services.hyperhive.swarm.authelia.url — the base URL of the - swarm's SSO provider. - - It defaults to this host's own instance only when this host - runs authelia. A hive that federates with a swarm sets it - explicitly to wherever that provider lives. - ''; - } { assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl; message = '' diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 1304d9b2..0f4fc3cb 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -968,20 +968,6 @@ in host: see docs/swarm/ for which secret goes where. ''; } - { - # Without a provider URL there is nothing to discover against, and - # the rendered config would name `null` as its issuer. - assertion = autheliaUrl != null; - message = '' - This homeserver's SSO login flow requires - services.hyperhive.swarm.authelia.url — the base URL of the - swarm's SSO provider. - - It defaults to this host's own instance only when this host - runs authelia. A hive that federates with a swarm sets it - explicitly to wherever that provider lives. - ''; - } { # The callback URL must name the homeserver itself, and with no # gateway vhost there is no public name for it to be built from. diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index f95659c0..dcdafdc3 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -457,7 +457,7 @@ in }; url = lib.mkOption { - type = lib.types.nullOr lib.types.str; + type = lib.types.str; default = "https://${cfg.domain}"; defaultText = lib.literalExpression ''"https://''${domain}"''; example = "https://auth.example.com"; @@ -474,9 +474,8 @@ in wrong, the same way {option}`services.hyperhive.swarm.otel.domain` has none. - Still nullable, for a deployment that has to say "this swarm has - no IdP" explicitly; consumers refuse rather than invent an - address when it is null. + Not having SSO is not a supported deployment: every swarm has an + IdP, so this is never `null`. ''; }; @@ -953,18 +952,6 @@ in # would reject it, but three layers away and at boot — naming both # sources here is the cheaper failure. ++ [ - { - # The bridge introspects by name, so a null URL becomes a nix - # coercion error several files from its cause. Only reachable by - # enabling authelia and clearing `url` by hand — an assertion - # rather than a fallback, because a guessed URL that evaluates - # cleanly is worse than a refused build. - assertion = cfg.url != null; - message = - "services.hyperhive.swarm.authelia.url must not be null when authelia " - + "is enabled: swarm-authelia-bridge introspects at " - + "`\${url}/api/oidc/introspection` from inside its container."; - } { assertion = lib.length (lib.unique (map (c: c.id) cfg.oidc.clients)) == lib.length cfg.oidc.clients; message = diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 383e768e..fb11ad80 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -382,7 +382,7 @@ in tokenEndpoint = lib.mkOption { type = lib.types.str; - default = lib.optionalString (autheliaCfg.url != null) "${autheliaCfg.url}/api/oidc/token"; + default = "${autheliaCfg.url}/api/oidc/token"; defaultText = lib.literalExpression ''"''${swarm.authelia.url}/api/oidc/token"''; description = '' The OIDC token endpoint the controller mints its own access @@ -773,12 +773,13 @@ in { assertion = cfg.queue.tokenEndpoint != ""; message = '' - services.hyperhive.swarm.controller.queue.tokenEndpoint is unset, - which means services.hyperhive.swarm.authelia.url is null. + services.hyperhive.swarm.controller.queue.tokenEndpoint is unset. - The controller mints its own access token before it may connect - to the queue, so it needs to know where the swarm's identity - provider lives — set that URL, or set this endpoint directly. + It defaults from services.hyperhive.swarm.authelia.url, so this + only happens when it has been cleared by hand. The controller + mints its own access token before it may connect to the queue, + so it needs to know where the swarm's identity provider lives — + set this endpoint directly. ''; } { diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index 5e6ac674..006e3abf 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -109,18 +109,6 @@ let # operator sees, not a coercion error from here. domainBase = if swarmDomain == null then "invalid" else swarmDomain; - # Is SSO configured for this SWARM. Swarm-wide by construction — `swarm.*` is - # identical on every host — and the option's own description is what makes - # this the right question to ask: a null URL means "no SSO configured". - # - # 🩸 A subject of an assertion below, NOT a gate. Dropping the OIDC block when - # this is false looks conservative and is the outage: `disable_login_form` is - # unconditional a few hundred lines down, so a Grafana with no OIDC settings - # is a Grafana with no login of any kind, arrived at silently. SSO is a - # requirement of running this service, so an unconfigured swarm fails to - # build and says which option to set. - ssoConfigured = hyperhiveCfg.swarm.authelia.url != null; - # A reader of the store is defined by holding a certificate the store # accepts, never by standing next to it — the rule # ./glue-matrix-bao-token.nix states in full. @@ -448,34 +436,11 @@ in # host that runs authelia, and this whole block is gated on the host that # runs Grafana. ./glue-grafana-oidc-client.nix is where it moved to. - # Both arms are what used to be a silent gate, and both fire only where - # Grafana runs — this whole block is under `deploy.grafana.enable`. The - # binding itself is asserted rather than a copy of its formula, the way - # ./swarm-nats.nix's own `autheliaUrl` arm does it: two spellings of one - # boolean is two places for a future edit to land in only one. + # This arm fires only where Grafana runs — this whole block is under + # `deploy.grafana.enable`. SSO itself needs no assertion any more: + # `swarm.authelia.url` is non-nullable, so every swarm has one and the + # OIDC block below always names a real endpoint. assertions = [ - { - # SSO is not optional for this service, and the reason is a hundred - # lines below in `auth.disable_login_form = true`: Grafana ships an - # `admin`/`admin` account on a public vhost, so the password box is - # off whatever the topology. Emitting no OIDC block when the swarm - # names no IdP therefore produces a container with no way in at all — - # a state no log names, since nothing failed. Failing the build and - # naming the option is the only report that reaches anyone. - assertion = ssoConfigured; - message = '' - services.hyperhive.deploy.grafana.enable requires - services.hyperhive.swarm.authelia.url — Grafana's only login is SSO, - because its local login form is disabled unconditionally (it ships - an admin/admin account and its vhost is on the public gateway). - - It defaults to this host's own instance only when this host runs - authelia. A hive that federates with a swarm sets it explicitly to - wherever that provider lives. Grafana exchanges its authorization - code at `''${url}/api/oidc/token` from inside its container, so a - null URL leaves no endpoint to name. - ''; - } { # The other half of one login: the OIDC block names a `$__file{}` that # `swarm-bao-grafana-oidc.service` below writes, and that unit reads diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index a13974c3..c86a3c62 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -511,18 +511,6 @@ in to have this host mint all four. ''; } - { - assertion = autheliaUrl != null; - message = '' - services.hyperhive.deploy.nats.enable requires - services.hyperhive.swarm.authelia.url — the queue authenticates - clients by validating tokens that authelia issued. - - It defaults to this host's own instance only when this host - runs authelia. A hive that federates with a swarm sets it - explicitly to wherever that provider lives. - ''; - } ]; # One declaration, two readers. The queue knows which client id it diff --git a/nix/host-modules/swarm-otel.nix b/nix/host-modules/swarm-otel.nix index 945f79ea..868df8b7 100644 --- a/nix/host-modules/swarm-otel.nix +++ b/nix/host-modules/swarm-otel.nix @@ -1027,22 +1027,6 @@ in should not be described as both. ''; } - { - # A hive proves who it is with a token this provider mints, so - # there is no version of this collector that runs without one. - # Stated as an assertion rather than a fallback because a guessed - # issuer URL evaluates cleanly and refuses every hive at runtime. - assertion = hyperhiveCfg.swarm.authelia.url != null; - message = '' - services.hyperhive.deploy.swarm-otel.enable is true but - services.hyperhive.swarm.authelia.url is null: every hive - authenticates to this collector as itself, and the token comes - from the swarm's identity provider. - - Point authelia.url at the swarm's provider, or enable - services.hyperhive.swarm.authelia on the host that runs it. - ''; - } { # A port collision between two listeners on one host is a runtime # coin toss with nothing in any log — the failure this whole diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index fd1fe9c8..d0e36dcd 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -500,8 +500,8 @@ in options.services.hyperhive.swarm.statusPublish = { tokenEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; - default = if swarmCfg.authelia.url != null then "${swarmCfg.authelia.url}/api/oidc/token" else null; - defaultText = lib.literalExpression ''"''${swarm.authelia.url}/api/oidc/token" when that URL is set, else null''; + default = "${swarmCfg.authelia.url}/api/oidc/token"; + defaultText = lib.literalExpression ''"''${swarm.authelia.url}/api/oidc/token"''; example = "https://auth.example.com/api/oidc/token"; description = '' The swarm IdP's OAuth2 token endpoint. This hive mints a diff --git a/nix/module-eval/grafana.nix b/nix/module-eval/grafana.nix index 524d0622..ae825800 100644 --- a/nix/module-eval/grafana.nix +++ b/nix/module-eval/grafana.nix @@ -70,22 +70,6 @@ let swarm.authelia.url = "https://auth.example.invalid"; }; - # The mirror image: the identity is placed, and the swarm names no IdP. The - # other half of "SSO must always be configured", and isolated the same way — - # exactly one thing wrong, so the arm reads one refusal. - # - # The null is now written out: `swarm.authelia.url` defaults to the swarm's - # IdP name on every hive, so "this swarm has no IdP" is a thing an operator - # states rather than a thing not running the container produces. - grafanaNoSso = hive { - deploy.grafana.enable = true; - deploy.grafana.plugins = [ ]; - deploy.grafana.package = pkgs.emptyDirectory; - deploy.bao.clientCertFile = "/etc/pki/bao-client.pem"; - deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem"; - swarm.authelia.url = null; - }; - # Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its # two reasons. An assertion is a config VALUE until something forces it — # `.config` never throws — so a fixture in a state the module refuses is @@ -169,19 +153,7 @@ let && !(grafanaRefusedFor grafanaNoIdentity "swarm.authelia.url"); } { - # "SSO must always be configured", as an eval-time refusal rather than a - # gate. A null URL used to drop the OIDC block silently, and - # `disable_login_form` is unconditional a hundred lines below it, so that - # combination produced a Grafana with no SSO and no password box — an - # outage whose cause is a boolean that evaluated to false at build time - # and left no trace. Same isolation as the arm above, mirrored. - name = "a grafana host in a swarm with no IdP is refused, naming swarm.authelia.url"; - ok = - grafanaRefusedFor grafanaNoSso "services.hyperhive.swarm.authelia.url" - && !(grafanaRefusedFor grafanaNoSso "deploy.bao.clientCertFile"); - } - { - # Without this the two arms above prove nothing: a refusal that fires on + # Without this the arm above proves nothing: a refusal that fires on # every host is not a check, and both of these are hosts a swarm is # expected to have. Read through the same helper, so a message that # stopped naming its option would fail the arms above rather than pass