diff --git a/scripts/guildhall-join-check.py b/scripts/guildhall-join-check.py index 03fe4aa..91e72a3 100644 --- a/scripts/guildhall-join-check.py +++ b/scripts/guildhall-join-check.py @@ -1,5 +1,6 @@ -"""Self-check for guildhall-join.py: invites, the append-only users file, and -that Authelia accepts the password hashes it writes. +"""Self-check for guildhall-join.py: invites, the append-only users file (also +after Authelia rewrote it for a password reset), and that Authelia accepts the +password hashes it writes. Run it against the same python the service uses, with authelia on PATH: @@ -78,6 +79,23 @@ check("display name survives quoting", users["alex"]["displayname"] == 'Alex "th check("invite is spent", j.open_invite(token) is None) check("spent invite refused", refused(j.sign_up, token, "sam", "", "longpassword", "longpassword")) +# What Authelia's yaml writer leaves behind after a password reset. +REWRITTEN = """users: + fred: + password: $argon2id$v=19$m=65536,t=3,p=4$c2FsdA$aGFzaA + displayname: Fred + email: fred@example.org + groups: + - household + address: null + extra: {} +""" +j.USERS_FILE.write_text(REWRITTEN) +second = j.mint("after a reset") +j.sign_up(second, "sam", "", "correct horse battery", "correct horse battery") +rewritten = yaml.safe_load(j.USERS_FILE.read_text())["users"] +check("signs up after authelia rewrote the file", set(rewritten) == {"fred", "sam"} and rewritten["sam"]["groups"] == ["guildhall"]) + expired = j.mint("old", now=1.0) check("expired invite is closed", j.open_invite(expired) is None) diff --git a/scripts/guildhall-join.py b/scripts/guildhall-join.py index e707bfe..4446a1e 100644 --- a/scripts/guildhall-join.py +++ b/scripts/guildhall-join.py @@ -113,18 +113,26 @@ def write_atomic(path: Path, text: str, mode: int, gid: int = -1) -> None: raise +def user_indent(text: str) -> str: + """The indent of the existing user keys. Authelia rewrites the whole file + with four spaces when someone resets their password; by hand it's two.""" + match = re.search(r"^users:[ \t]*\n(?:[ \t]*(?:#.*)?\n)*( +)\S", text, re.M) + return match.group(1) if match else " " + + def add_user(path: Path, username: str, display: str, password_hash: str) -> None: """Append one user; refuse unless every existing user parses back unchanged.""" text = path.read_text() before = yaml.safe_load(text)["users"] if username.lower() in (name.lower() for name in before): raise Refused("That username is taken.") + i = user_indent(text) block = ( - f" {username}:\n" - f" displayname: {json.dumps(display)}\n" - f" password: {json.dumps(password_hash)}\n" - f" groups:\n" - f" - {GROUP}\n" + f"{i}{username}:\n" + f"{i * 2}displayname: {json.dumps(display)}\n" + f"{i * 2}password: {json.dumps(password_hash)}\n" + f"{i * 2}groups:\n" + f"{i * 3}- {GROUP}\n" ) new_text = text.rstrip("\n") + "\n\n" + block after = yaml.safe_load(new_text)["users"] diff --git a/services/authelia.nix b/services/authelia.nix index 83bd6f1..524efca 100644 --- a/services/authelia.nix +++ b/services/authelia.nix @@ -2,8 +2,9 @@ # Secrets live in /var/secrets/authelia (root:authelia-main, 640) — see readme. # # Users live in /var/lib/authelia-users so guildhall-join (services/ -# guildhall-join.nix) can add invited players; Authelia only reads it and -# reloads on change. Groups decide reach: `household` gets the shared home +# guildhall-join.nix) can add invited players. Authelia reloads it on change +# and writes it back in place when someone resets their password, which only +# works for users with an `email` (the link lands in notification.txt). Groups decide reach: `household` gets the shared home # sites, `guildhall` gets the game and nothing else. { config, lib, pkgs, ... }: let @@ -13,7 +14,8 @@ let usersFile = "${usersDir}/users_database.yml"; # One-time move out of Authelia's private state dir, then the same - # ownership on every start: guildhall-join writes, authelia-users reads. + # ownership on every start: guildhall-join appends, authelia-users reads and + # saves password resets. prepareUsers = pkgs.writeShellScript "authelia-users-prepare" '' set -eu ${pkgs.coreutils}/bin/install -d -m 0750 -o guildhall-join -g authelia-users ${usersDir} @@ -21,7 +23,7 @@ let ${pkgs.coreutils}/bin/cp /var/lib/authelia-main/users_database.yml ${usersFile} fi ${pkgs.coreutils}/bin/chown guildhall-join:authelia-users ${usersFile} - ${pkgs.coreutils}/bin/chmod 0640 ${usersFile} + ${pkgs.coreutils}/bin/chmod 0660 ${usersFile} ''; in { @@ -94,7 +96,12 @@ in }; }; - # First, before the module's validate-config pre-start. - systemd.services.authelia-main.serviceConfig.ExecStartPre = lib.mkBefore [ "+${prepareUsers}" ]; + systemd.services.authelia-main.serviceConfig = { + # First, before the module's validate-config pre-start. + ExecStartPre = lib.mkBefore [ "+${prepareUsers}" ]; + # The directory, not the file: guildhall-join replaces the file, which + # would leave a file bind mount pointing at the old one. + ReadWritePaths = [ usersDir ]; + }; }; }