The queue's metrics were scraped over loopback from the exporter, declared only where the queue and the collector share a host, so a collector on any other host had no job for it. Its target is now declared on every host as `https://<swarm.nats.domain>/metrics`, and the queue's host gets a gateway vhost on that name serving `= /metrics` from the exporter behind the collector's auth subrequest (`/__nats_metrics_authz`, the forge's shape). Every other path there is 403; the monitoring endpoint is never proxied. The rules derived from `publishedScrapeTargets` admit the collector's client and deny every other subject. The loopback entry, and with it `glue-otel-scrape-targets.nix`, is gone. In self-signed mode the vhost serves the queue's own `swarm-nats` leaf (issued for exactly that name, `server_flag` set), copied 0640 root:nginx by `swarm-nats-gateway-cert`; until the leaf exists that unit writes a throwaway pair, so nginx never starts on a missing file. The name stays out of `swarm.serviceDomains`, so a host whose only swarm service is the queue fronts no services name and its gateway neither waits for nor needs the swarm-services leaf: no services-issuer pair has to be placed on it. With ACME or an operator cert the vhost uses the gateway's own TLS, as every other name does. The queue's host now enables the gateway (`mkDefault`), as every other swarm service's host does. Fixes #5205
477 lines
20 KiB
Nix
477 lines
20 KiB
Nix
# `checks.module-eval-forge-placement` — see ./lib.nix for the shared
|
|
# rationale (why this suite exists, naming convention, "evaluates
|
|
# not executes").
|
|
#
|
|
# Where the forge runs. A swarm has one, on the host with
|
|
# `deploy.forgejo.enable`; every other hive is a client of it, and the parts
|
|
# of a split deployment that must not assume every host runs a forge
|
|
# (the OIDC client, the controller's token, the CI runner) have to hold.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
shared,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
hive
|
|
runGroup
|
|
;
|
|
|
|
inherit (shared) bare;
|
|
allLocal = hive { deploy.singleHostSwarm = true; };
|
|
inherit (shared) servicesHere forgeHere;
|
|
|
|
# The shared services here, the forge somewhere else. Every derivation in
|
|
# ../host-modules/swarm-required-services.nix is `mkDefault`, so this stays
|
|
# expressible — and it is also the authelia-without-forge host the OIDC
|
|
# client case needs.
|
|
servicesForgeElsewhere = hive {
|
|
deploy.allSwarmServices = true;
|
|
deploy.forgejo.enable = false;
|
|
};
|
|
|
|
# The forge without authelia: the other half of the split, with its secret's
|
|
# path and its store identity chosen by hand.
|
|
forgeNoAuthelia = hive {
|
|
deploy.forgejo.enable = true;
|
|
deploy.forgejo.sso.clientSecretFile = "/var/lib/forgejo-oidc/by-hand.secret";
|
|
deploy.bao.forgePublisherClientCertFile = "/etc/pki/forge-publisher.pem";
|
|
deploy.bao.forgePublisherClientKeyFile = "/etc/pki/forge-publisher-key.pem";
|
|
};
|
|
|
|
# The forge with neither authelia nor the store, its store identity placed by
|
|
# hand: the deployment the store fetch exists for.
|
|
forgeAlone = hive {
|
|
deploy.forgejo.enable = true;
|
|
deploy.bao.forgePublisherClientCertFile = "/etc/pki/forge-publisher.pem";
|
|
deploy.bao.forgePublisherClientKeyFile = "/etc/pki/forge-publisher-key.pem";
|
|
};
|
|
|
|
# The runner alone: no hive, no forge, no store, its store identity placed
|
|
# by hand. `nix` is a label without a scheme.
|
|
ciAlone = hive {
|
|
deploy.hive-controller.enable = false;
|
|
deploy.ci-runner.enable = true;
|
|
deploy.ci-runner.labels = [
|
|
"hive-ci:host"
|
|
"nix"
|
|
];
|
|
deploy.bao.ciRunnerClientCertFile = "/etc/pki/bao-ci-runner.pem";
|
|
deploy.bao.ciRunnerClientKeyFile = "/etc/pki/bao-ci-runner-key.pem";
|
|
};
|
|
ciNoIdentity = hive {
|
|
deploy.hive-controller.enable = false;
|
|
deploy.ci-runner.enable = true;
|
|
};
|
|
|
|
# A host config that still names the runner under the forge, every option.
|
|
ciUnderForge = hive {
|
|
deploy.forgejo.ci.enable = true;
|
|
deploy.forgejo.ci.name = "old-runner";
|
|
deploy.forgejo.ci.concurrency = 3;
|
|
deploy.forgejo.ci.labels = [ "nix:host" ];
|
|
deploy.forgejo.ci.package = pkgs.hello;
|
|
deploy.forgejo.ci.jobTimeout = "2h";
|
|
deploy.bao.ciRunnerClientCertFile = "/etc/pki/bao-ci-runner.pem";
|
|
deploy.bao.ciRunnerClientKeyFile = "/etc/pki/bao-ci-runner-key.pem";
|
|
};
|
|
# The old name alone, with no store identity placed and none defaulted.
|
|
ciUnderForgeNoIdentity = hive {
|
|
deploy.hive-controller.enable = false;
|
|
deploy.forgejo.ci.enable = true;
|
|
};
|
|
# The older spelling, under the swarm-level forge options.
|
|
ciUnderSwarmForge = hive {
|
|
swarm.forge.ci.enable = true;
|
|
swarm.forge.ci.name = "older-runner";
|
|
swarm.forge.ci.concurrency = 2;
|
|
swarm.forge.ci.labels = [ "nix:host" ];
|
|
swarm.forge.ci.package = pkgs.hello;
|
|
deploy.bao.ciRunnerClientCertFile = "/etc/pki/bao-ci-runner.pem";
|
|
deploy.bao.ciRunnerClientKeyFile = "/etc/pki/bao-ci-runner-key.pem";
|
|
};
|
|
|
|
runsForge = m: m.services.hyperhive.deploy.forgejo.enable && m.containers ? hive-forge;
|
|
|
|
forgeClientIds =
|
|
m:
|
|
map (c: c.id) (
|
|
lib.filter (
|
|
c: c.id == m.services.hyperhive.swarm.forge.sso.clientId
|
|
) m.services.hyperhive.swarm.authelia.oidc.clients
|
|
);
|
|
|
|
# Matched on the option the message names, same reasoning as
|
|
# ./grafana.nix's `grafanaRefusedFor`.
|
|
refusedOver = m: needle: lib.any (a: !a.assertion && lib.hasInfix needle a.message) m.assertions;
|
|
|
|
publisher = m: m.systemd.services.swarm-bao-forge-publisher;
|
|
oidcFetch = m: m.systemd.services.swarm-bao-forge-oidc;
|
|
|
|
forgeSettings = forgeHere.containers.hive-forge.config.services.forgejo.settings;
|
|
|
|
runnerService = ciAlone.containers.swarm-ci-runner.config.systemd.services.gitea-runner-swarm;
|
|
registerService =
|
|
ciAlone.containers.swarm-ci-runner.config.systemd.services.swarm-ci-runner-register;
|
|
|
|
cases = [
|
|
{
|
|
# The absence the whole option exists for: a second forge in a swarm
|
|
# is a split brain nobody notices, so a hive that has not been told it
|
|
# is the forge's host runs none of its surface.
|
|
name = "a hive that is not the forge's host runs no forge";
|
|
ok =
|
|
!bare.services.hyperhive.deploy.forgejo.enable
|
|
&& !(bare.containers ? hive-forge)
|
|
&& !(bare.systemd.services ? swarm-bao-forge-publisher)
|
|
&& !(lib.elem "forge.t.local" bare.services.hyperhive.gateway.localNames)
|
|
&& !(refusedOver bare "deploy.forgejo.sso.clientSecretFile");
|
|
}
|
|
{
|
|
# All three are published targets; ./swarm-otel-identity.nix covers them.
|
|
name = "a host running the collector, authelia, grafana and nats scrapes none of them on loopback";
|
|
ok =
|
|
let
|
|
t = servicesHere.services.hyperhive.swarm.otel.scrapeTargets;
|
|
in
|
|
servicesHere.services.hyperhive.deploy.nats.enable
|
|
&& !(t ? authelia)
|
|
&& !(t ? grafana)
|
|
&& !(t ? nats);
|
|
}
|
|
{
|
|
name = "hosting the swarm's shared services runs the forge";
|
|
ok = runsForge servicesHere;
|
|
}
|
|
{
|
|
name = "the all-local mode runs the forge";
|
|
ok = runsForge allLocal && lib.elem "forge.t.local" allLocal.services.hyperhive.gateway.localNames;
|
|
}
|
|
{
|
|
name = "an explicit deploy.forgejo.enable runs the forge on its own";
|
|
ok = runsForge forgeHere && !forgeHere.services.hyperhive.deploy.allSwarmServices;
|
|
}
|
|
{
|
|
# `mkDefault`, not a plain assignment: the forge stays placeable on a
|
|
# host of its own. `nats` is the control, so the case cannot pass on a
|
|
# fixture where nothing came on.
|
|
name = "placing the forge elsewhere survives the switch that would enable it";
|
|
ok =
|
|
!(servicesForgeElsewhere.containers ? hive-forge)
|
|
&& servicesForgeElsewhere.services.hyperhive.deploy.nats.enable;
|
|
}
|
|
{
|
|
# A client is a row in authelia's config, so it is declared where
|
|
# authelia runs. With the forge's module gated, registering it from
|
|
# there would leave a split swarm's forge unknown to its IdP.
|
|
name = "the forge's OIDC client is registered wherever authelia runs, and only there";
|
|
ok =
|
|
forgeClientIds servicesForgeElsewhere == [ "forgejo" ]
|
|
&& forgeClientIds allLocal == [ "forgejo" ]
|
|
&& forgeClientIds forgeNoAuthelia == [ ];
|
|
}
|
|
{
|
|
name = "the forge's OIDC callback is the one forgejo sends";
|
|
ok =
|
|
servicesForgeElsewhere.services.hyperhive.swarm.forge.sso.redirectUri
|
|
== "https://forge.t.local/user/oauth2/authelia/callback";
|
|
}
|
|
{
|
|
# The runner reaches the forge by name and reads its token with its own
|
|
# certificate, so nothing about the host it runs on is asked.
|
|
name = "the CI runner runs on a host with neither the forge nor a hive";
|
|
ok =
|
|
ciAlone.containers ? swarm-ci-runner
|
|
&& !(ciAlone.containers ? hive-forge)
|
|
&& !ciAlone.services.hyperhive.deploy.hive-controller.enable
|
|
&& !(refusedOver ciAlone "ci-runner")
|
|
&&
|
|
ciAlone.systemd.services.swarm-bao-ci-runner.environment.BAO_CLIENT_CERT
|
|
== "/etc/pki/bao-ci-runner.pem";
|
|
}
|
|
{
|
|
# The store's units exist only on its host; `Requires=` on an absent one
|
|
# fails the fetch's start. The arm where the store is here is in
|
|
# ./swarm-services-switch.nix.
|
|
name = "the CI runner's token fetch names none of the store's units on a host without the store";
|
|
ok =
|
|
let
|
|
u = ciAlone.systemd.services.swarm-bao-ci-runner;
|
|
storeUnits = [
|
|
"swarm-bao-pki.service"
|
|
"container@${ciAlone.services.hyperhive.swarm.bao.machine}.service"
|
|
];
|
|
in
|
|
!(lib.any (n: lib.elem n (u.after ++ u.wants ++ u.requires)) storeUnits);
|
|
}
|
|
{
|
|
# Without the identity the token never arrives; the control is the
|
|
# placed pair above.
|
|
name = "the CI runner is refused without its store identity";
|
|
ok = refusedOver ciNoIdentity "deploy.bao.ciRunnerClientCertFile";
|
|
}
|
|
{
|
|
# systemd hands a credential to every process of the unit that loads
|
|
# it, and a host job is one of the daemon's processes and inherits its
|
|
# environment; a token in either lets any workflow register runners.
|
|
name = "the CI runner daemon can read no registration token";
|
|
ok =
|
|
let
|
|
sc = runnerService.serviceConfig;
|
|
in
|
|
!(runnerService.environment ? TOKEN)
|
|
&& lib.toList (sc.EnvironmentFile or [ ]) == [ ]
|
|
&& !(lib.any (lib.hasPrefix "TOKEN=") (lib.toList (sc.Environment or [ ])))
|
|
&& !(sc ? LoadCredential)
|
|
&& !(sc ? LoadCredentialEncrypted)
|
|
&& !(sc ? SetCredential);
|
|
}
|
|
{
|
|
name = "the CI register unit alone loads the registration token, and runs before the daemon";
|
|
ok =
|
|
let
|
|
sc = registerService.serviceConfig;
|
|
in
|
|
sc.LoadCredential == [ "registration-token:/var/lib/swarm-ci-runner/registration-token" ]
|
|
&& sc.Type == "oneshot"
|
|
&& sc.User == runnerService.serviceConfig.User
|
|
&& sc.DynamicUser
|
|
&& sc.StateDirectory == runnerService.serviceConfig.StateDirectory
|
|
&& lib.any (
|
|
s: lib.hasSuffix "/bin/systemctl start swarm-ci-runner-register.service" s
|
|
) runnerService.serviceConfig.ExecStartPre
|
|
&& !(lib.any (
|
|
s: lib.hasInfix "gitea-register-runner" (toString s)
|
|
) runnerService.serviceConfig.ExecStartPre);
|
|
}
|
|
{
|
|
name = "the CI register unit registers from its credential, and only without a .runner";
|
|
ok =
|
|
let
|
|
text = registerService.script;
|
|
beforeRegister = lib.head (lib.splitString "register --no-interactive" text);
|
|
in
|
|
lib.hasInfix "if [ -e .runner ]; then\n exit 0\nfi" beforeRegister
|
|
&& lib.hasInfix ''token="$(< "$CREDENTIALS_DIRECTORY/registration-token")"'' beforeRegister
|
|
&& lib.hasInfix ''--token "$token"'' text
|
|
&& !(lib.hasInfix "export" text)
|
|
&& !(lib.hasInfix "TOKEN" text);
|
|
}
|
|
{
|
|
# The daemon rewrites `.runner`'s labels to this form on start; compared
|
|
# against the configured strings verbatim, a scheme-less label would
|
|
# discard `.runner` and register a new runner on every start.
|
|
name = "the CI runner compares .runner's labels in the form the daemon writes them";
|
|
ok = registerService.environment.SWARM_CI_RUNNER_LABELS == "hive-ci:host,nix:host";
|
|
}
|
|
{
|
|
# The control is the container: it only exists if `enable` arrived.
|
|
name = "a CI option under deploy.forgejo sets the same option under deploy.ci-runner";
|
|
ok =
|
|
let
|
|
r = ciUnderForge.services.hyperhive.deploy.ci-runner;
|
|
in
|
|
r.enable
|
|
&& r.name == "old-runner"
|
|
&& r.concurrency == 3
|
|
&& r.labels == [ "nix:host" ]
|
|
&& r.package == pkgs.hello
|
|
&& r.jobTimeout == "2h"
|
|
&& ciUnderForge.containers ? swarm-ci-runner
|
|
&& lib.any (lib.hasInfix "services.hyperhive.deploy.forgejo.ci.enable") ciUnderForge.warnings
|
|
&& !(lib.any (a: !a.assertion) ciUnderForge.assertions);
|
|
}
|
|
{
|
|
# The control is the container: it only exists if `enable` arrived.
|
|
name = "a CI option under swarm.forge sets the same option under deploy.ci-runner";
|
|
ok =
|
|
let
|
|
r = ciUnderSwarmForge.services.hyperhive.deploy.ci-runner;
|
|
in
|
|
r.enable
|
|
&& r.name == "older-runner"
|
|
&& r.concurrency == 2
|
|
&& r.labels == [ "nix:host" ]
|
|
&& r.package == pkgs.hello
|
|
&& ciUnderSwarmForge.containers ? swarm-ci-runner
|
|
&& lib.any (lib.hasInfix "services.hyperhive.swarm.forge.ci.enable") ciUnderSwarmForge.warnings
|
|
&& !(lib.any (a: !a.assertion) ciUnderSwarmForge.assertions);
|
|
}
|
|
{
|
|
# The rename carries `enable` but not the identity: an old config with
|
|
# only the old name is refused, and the message says what to set. The
|
|
# control is the placed pair in `ciUnderForge` above.
|
|
name = "an old CI enable without a store identity is refused naming the options to set";
|
|
ok =
|
|
refusedOver ciUnderForgeNoIdentity "services.hyperhive.deploy.bao.ciRunnerClientCertFile"
|
|
&& refusedOver ciUnderForgeNoIdentity "services.hyperhive.deploy.bao.ciRunnerClientKeyFile"
|
|
&& refusedOver ciUnderForgeNoIdentity "services.hyperhive.deploy.bao.enable";
|
|
}
|
|
{
|
|
# The forge's host publishes the controller's token under its own leaf,
|
|
# which the store mints beside it here. `value=@` is what keeps the token
|
|
# out of the process's arguments.
|
|
name = "the forge's host publishes the controller's forge token to the store under its own leaf";
|
|
ok =
|
|
let
|
|
u = publisher allLocal;
|
|
in
|
|
u.environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/forge-publisher.pem"
|
|
&& u.environment.BAO_CLIENT_KEY == "/var/lib/swarm-bao-pki/forge-publisher-key.pem"
|
|
&& lib.hasInfix "bao kv put secret/swarm/controller/swarm-controller/forge/token value=@\"$src\"" u.script
|
|
&& lib.hasInfix "/var/lib/nixos-containers/hive-forge/var/lib/forgejo/swarm-controller-token" u.script
|
|
&& lib.elem "container@hive-forge.service" u.wantedBy
|
|
# The deploy that adds the unit starts it only through a target.
|
|
&& lib.elem "multi-user.target" u.wantedBy
|
|
&& !(allLocal.systemd.services ? hive-forge-swarm-controller-token);
|
|
}
|
|
{
|
|
# What makes it re-run. A oneshot left `active (exited)` makes every later
|
|
# start a no-op, so `RemainAfterExit` would silence every trigger: the
|
|
# targets and the container's start (`wantedBy`, above) and a write of
|
|
# the token file. A missing file exits 0, so a switch with the container
|
|
# down does not fail.
|
|
name = "the forge publisher re-runs on each container start and each write of the token file";
|
|
ok =
|
|
let
|
|
p = allLocal.systemd.paths.swarm-bao-forge-publisher;
|
|
in
|
|
!((publisher allLocal).serviceConfig.RemainAfterExit or false)
|
|
&&
|
|
p.pathConfig.PathChanged
|
|
== "/var/lib/nixos-containers/hive-forge/var/lib/forgejo/swarm-controller-token"
|
|
&& p.pathConfig.Unit == "swarm-bao-forge-publisher.service"
|
|
&& lib.hasInfix "the path unit publishes it when it does" (publisher allLocal).script
|
|
&& lib.hasInfix "exit 0" (publisher allLocal).script
|
|
&& lib.elem "multi-user.target" p.wantedBy
|
|
&& !(bare.systemd.paths ? swarm-bao-forge-publisher);
|
|
}
|
|
{
|
|
# The absence arm: a forge host without the store has no leaf unless one
|
|
# is placed, and the unit says which options to set.
|
|
name = "a forge host with no publisher leaf renders a publisher that fails naming the options";
|
|
ok =
|
|
let
|
|
s = (publisher forgeHere).script;
|
|
in
|
|
lib.hasInfix "forgePublisherClientCertFile" s
|
|
&& lib.hasInfix "exit 1" s
|
|
&& !(lib.hasInfix "bao kv put" s);
|
|
}
|
|
{
|
|
# The forge's OIDC secret has one route, the store, under the forge
|
|
# publisher's leaf, landing where `clientSecretFile` points inside the
|
|
# container. No unit copies it out of authelia's tree, even on a host
|
|
# that runs authelia, and a forge host without authelia needs no path
|
|
# set by hand.
|
|
name = "the forge reads its OIDC secret from the store under the forge publisher's leaf";
|
|
ok =
|
|
let
|
|
u = oidcFetch forgeAlone;
|
|
d = forgeAlone.services.hyperhive.deploy;
|
|
in
|
|
!d.authelia.enable
|
|
&& !d.bao.enable
|
|
&& d.forgejo.sso.clientSecretFile == "/var/lib/forgejo-oidc/forgejo.secret"
|
|
&& !(refusedOver forgeAlone "deploy.forgejo.sso.clientSecretFile")
|
|
&& u.environment.BAO_CLIENT_CERT == "/etc/pki/forge-publisher.pem"
|
|
&& u.environment.BAO_CLIENT_KEY == "/etc/pki/forge-publisher-key.pem"
|
|
&& lib.hasInfix "bao kv get -field=value secret/swarm/services/forgejo/oidc/client" u.script
|
|
&& lib.hasInfix "/var/lib/nixos-containers/hive-forge/var/lib/forgejo-oidc/forgejo.secret" u.script
|
|
&& lib.hasInfix "refresh_consumer hive-forge forgejo-sso-source.service" u.script
|
|
# An unpublished or empty secret fails the unit, so `storeRetry` retries it.
|
|
&& !(lib.hasInfix "exit 0" u.script)
|
|
&& u.serviceConfig.Restart == "on-failure"
|
|
&& lib.elem "container@hive-forge.service" u.before
|
|
&& lib.elem "multi-user.target" u.wantedBy
|
|
&& (oidcFetch allLocal).environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/forge-publisher.pem"
|
|
&& !(forgeAlone.systemd.services ? hive-forge-oidc-secret)
|
|
&& !(allLocal.systemd.services ? hive-forge-oidc-secret)
|
|
&& !(bare.systemd.services ? swarm-bao-forge-oidc);
|
|
}
|
|
{
|
|
# An operator-chosen path is where the fetched secret lands.
|
|
name = "the forge's OIDC secret lands where clientSecretFile points";
|
|
ok =
|
|
let
|
|
s = (oidcFetch forgeNoAuthelia).script;
|
|
in
|
|
lib.hasInfix "/var/lib/nixos-containers/hive-forge/var/lib/forgejo-oidc/by-hand.secret" s;
|
|
}
|
|
{
|
|
# Beside the store, the fetch waits for the unit writing the forge
|
|
# publisher's role, the leaf it logs in with.
|
|
name = "the forge's OIDC secret fetch is ordered after the forge publisher's policy unit";
|
|
ok =
|
|
let
|
|
u = oidcFetch allLocal;
|
|
in
|
|
lib.elem "swarm-bao-forge-publisher-policy.service" u.after
|
|
&& lib.elem "swarm-bao-forge-publisher-policy.service" u.wants;
|
|
}
|
|
{
|
|
# The absence arm: no publisher leaf, no secret, and the unit says which
|
|
# options to set.
|
|
name = "a forge host with no publisher leaf renders an OIDC secret fetch that fails naming the options";
|
|
ok =
|
|
let
|
|
u = oidcFetch forgeHere;
|
|
in
|
|
!(u.environment ? BAO_CLIENT_CERT)
|
|
&& lib.hasInfix "forgePublisherClientCertFile" u.script
|
|
&& lib.hasInfix "exit 1" u.script
|
|
&& !(lib.hasInfix "bao login" u.script);
|
|
}
|
|
{
|
|
# The login source fetches the issuer's discovery document by name
|
|
# wherever the forge runs, so it waits for the resolver on a forge host
|
|
# without authelia too.
|
|
name = "the login source waits for the container's resolver on a forge host without authelia";
|
|
ok = lib.elem "forgejo-sso-source.service" (
|
|
forgeAlone.containers.hive-forge.config.systemd.services.swarm-bridge-dns.before
|
|
);
|
|
}
|
|
{
|
|
# The host copy is removed where the forge runs: nothing reads it.
|
|
name = "the forge's host removes its old copy of the controller's forge token";
|
|
ok =
|
|
lib.elem "r /var/lib/hyperhive-forge/swarm-controller.token" forgeHere.systemd.tmpfiles.rules
|
|
&& !(lib.elem "r /var/lib/hyperhive-forge/swarm-controller.token" bare.systemd.tmpfiles.rules);
|
|
}
|
|
{
|
|
# The only thing that makes a human's forge account: nothing else
|
|
# creates one.
|
|
name = "a first authelia login creates the forge account, named by preferred_username";
|
|
ok =
|
|
let
|
|
o = forgeSettings.oauth2_client;
|
|
in
|
|
o.ENABLE_AUTO_REGISTRATION == true && o.USERNAME == "preferred_username";
|
|
}
|
|
{
|
|
# `auto` would give an SSO user whatever local account carries their
|
|
# name — an agent's, or `core`'s.
|
|
name = "an SSO login adopts an existing forge account only with that account's password";
|
|
ok = forgeSettings.oauth2_client.ACCOUNT_LINKING == "login";
|
|
}
|
|
{
|
|
# Both halves: local sign-up stays off, and nothing turns on the one
|
|
# setting forgejo's auto-registration does check.
|
|
name = "local sign-up stays off without blocking the SSO registration";
|
|
ok =
|
|
forgeSettings.service.DISABLE_REGISTRATION == true
|
|
&& !(forgeSettings.service.ALLOW_ONLY_INTERNAL_REGISTRATION or false);
|
|
}
|
|
];
|
|
in
|
|
runGroup "forge-placement" cases
|