nix: make swarm.authelia.url non-nullable, trim its docs

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.
This commit is contained in:
atlas 2026-09-21 18:08:56 +02:00 committed by mara
commit 5ec0ce90fd
10 changed files with 21 additions and 154 deletions

View file

@ -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 = ''

View file

@ -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.

View file

@ -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 =

View file

@ -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.
'';
}
{

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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