mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-06 17:13:24 -05:00
In #356221 nixpkgs removed `services.plausible.adminUser.*` because Plausible upstream removed the ability to do that with an easy CLI command, see: https://github.com/plausible/analytics/pull/2357#issuecomment-4595200593 However, Plausible's default setup without automatic admin user creation is unsafe for Plausible instances exposed to the Internet because it offers a setup wizard, allowing anybody who can reach its webserver to create the first admin users. Plausible is usually exposed to the Internet, given that it is a web analytics tool which must be web-reachable to function. NixOS defaults configure it to listen only on `localhost`, which is already good, but on NixOS it generally makes sense to be able to configure services declaratively so they work out of the box on first deploy. Workarounds such as "deploy first binding only to localhost, manually do the setup wizard, then deploy another config to expose it" are annoying. Thus, this commit adds back the original declarative `adminUser` implementation, doing the needed DB changes directly, race-free, and with better security (allowing to configure a password has instead of a plain-text password). Assisted-By: Zoo Code with Claude Opus 4.8, human review
142 lines
6 KiB
Nix
142 lines
6 KiB
Nix
{ runTest }:
|
|
|
|
let
|
|
secretKeybase = "nannannannannannannannannannannannannannannannannannannan_batman!";
|
|
|
|
# A fixed bcrypt password hash is fine for a test; the plaintext is never
|
|
# needed because the test only checks that an admin user exists (and thus the
|
|
# "first launch" setup wizard is unreachable), not that login with the
|
|
# password works.
|
|
adminEmail = "admin@localhost";
|
|
|
|
mkPlausibleTest =
|
|
{
|
|
seedAdmin ? false,
|
|
}:
|
|
runTest (
|
|
{ lib, pkgs, ... }:
|
|
let
|
|
adminPassword = "correct-horse-battery-staple";
|
|
adminPasswordHashFile = pkgs.runCommand "plausible-admin-password-hash" { } ''
|
|
${lib.getExe pkgs.mkpasswd} -m bcrypt ${lib.escapeShellArg adminPassword} > "$out"
|
|
'';
|
|
in
|
|
{
|
|
name = "plausible" + lib.optionalString seedAdmin "-declarative-admin-user";
|
|
meta = {
|
|
maintainers = with lib.maintainers; [
|
|
nh2
|
|
stepbrobd
|
|
];
|
|
};
|
|
|
|
nodes.machine = {
|
|
# On first boot, the ClickHouse migrations run by Plausible's
|
|
# `migrate.sh` intermittently fail with `(Mint.TransportError) socket
|
|
# closed`, which aborts startup before the web server opens its port.
|
|
# The failure is transient and succeeds on a subsequent attempt, so
|
|
# retry startup without a rate limit until the port opens. (Without
|
|
# this, the test is flaky.)
|
|
systemd.services.plausible.serviceConfig.Restart = lib.mkForce "always";
|
|
systemd.services.plausible.serviceConfig.RestartSec = 1;
|
|
systemd.services.plausible.unitConfig.StartLimitIntervalSec = 0;
|
|
|
|
services.plausible = {
|
|
enable = true;
|
|
adminUser = lib.mkIf seedAdmin {
|
|
email = adminEmail;
|
|
name = "Test Admin";
|
|
passwordHashFile = "${adminPasswordHashFile}";
|
|
};
|
|
server = {
|
|
baseUrl = "http://localhost:8000";
|
|
secretKeybaseFile = builtins.toFile "plausible-test-secret-keybase-file" secretKeybase;
|
|
};
|
|
};
|
|
};
|
|
|
|
testScript = ''
|
|
machine.wait_for_unit("plausible.service")
|
|
machine.wait_for_open_port(8000)
|
|
|
|
# Ensure that the software does not make the machine
|
|
# listen on any public interfaces by default.
|
|
machine.fail("ss -tlpn 'src = 0.0.0.0 or src = [::]' | grep LISTEN")
|
|
|
|
machine.succeed("curl -f localhost:8000 >&2")
|
|
machine.succeed("curl -f localhost:8000/js/script.js >&2")
|
|
|
|
def user_count():
|
|
# Plausible's "first launch" state is defined as "no user exists"
|
|
# (`Plausible.Release.should_be_first_launch?` is
|
|
# `not Repo.exists?(Plausible.Auth.User)`), so we inspect the `users`
|
|
# table directly. Local Postgres connections use `trust` auth in this
|
|
# VM, so the `postgres` superuser can query without a password.
|
|
return machine.succeed(
|
|
"sudo -u postgres psql --dbname plausible --tuples-only --no-align "
|
|
"--command 'SELECT count(*) FROM users'"
|
|
).strip()
|
|
|
|
def login_redirect_url():
|
|
return machine.succeed(
|
|
"curl -s -o /dev/null -w '%{redirect_url}' localhost:8000/login"
|
|
).strip()
|
|
''
|
|
+ (
|
|
if seedAdmin then
|
|
''
|
|
with subtest("the admin user is seeded"):
|
|
assert user_count() == "1", "expected exactly one seeded admin user"
|
|
email = machine.succeed(
|
|
"sudo -u postgres psql --dbname plausible --tuples-only --no-align "
|
|
"--command 'SELECT email FROM users'"
|
|
).strip()
|
|
assert email == "${adminEmail}", f"unexpected seeded admin email: {email!r}"
|
|
|
|
with subtest("the setup wizard is NOT reachable"):
|
|
# `/login` must render normally (HTTP 200) and must not redirect
|
|
# to the first-launch `/register` wizard.
|
|
status = machine.succeed(
|
|
"curl -s -o /dev/null -w '%{http_code}' localhost:8000/login"
|
|
).strip()
|
|
assert status == "200", f"expected /login to render (200), got: {status!r}"
|
|
location = login_redirect_url()
|
|
assert "/register" not in location, (
|
|
f"/login unexpectedly redirected to the setup wizard: {location!r}"
|
|
)
|
|
|
|
with subtest("seeding is idempotent across restarts"):
|
|
machine.succeed("systemctl restart plausible.service")
|
|
machine.wait_for_open_port(8000)
|
|
assert user_count() == "1", "expected still exactly one user after restart"
|
|
''
|
|
else
|
|
''
|
|
with subtest("without an admin user, the setup wizard is reachable"):
|
|
assert user_count() == "0", "expected no users in first-launch state"
|
|
# With no user seeded, `should_be_first_launch?` is true and the
|
|
# browser pipeline's `FirstLaunchPlug` 302-redirects every page to
|
|
# `/register`.
|
|
location = login_redirect_url()
|
|
assert "/register" in location, (
|
|
f"expected /login to redirect to the setup wizard /register, but redirect was: {location!r}"
|
|
)
|
|
''
|
|
);
|
|
}
|
|
);
|
|
in
|
|
{
|
|
# Basic test: Plausible without a declaratively configured admin user is in
|
|
# the "first launch" state, so the unauthenticated setup wizard is reachable.
|
|
# This also asserts that the way the `declarative-admin-user` test detects
|
|
# "wizard reachable" actually fires for this Plausible version, so a future
|
|
# Plausible change that breaks the detection would make that test fail rather
|
|
# than silently pass.
|
|
basic = mkPlausibleTest { seedAdmin = false; };
|
|
|
|
# Tests that a declaratively configured admin user is seeded before the web
|
|
# server accepts connections, so the unauthenticated "first launch" setup
|
|
# wizard is never reachable.
|
|
declarative-admin-user = mkPlausibleTest { seedAdmin = true; };
|
|
}
|