authelia: household group; guildhall invite links
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
791a3bdf34
commit
782a1c96a6
6 changed files with 502 additions and 8 deletions
94
scripts/guildhall-join-check.py
Normal file
94
scripts/guildhall-join-check.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""Self-check for guildhall-join.py: invites, the append-only users file, and
|
||||
that Authelia accepts the password hashes it writes.
|
||||
|
||||
Run it against the same python the service uses, with authelia on PATH:
|
||||
|
||||
py=$(nix build --no-link --print-out-paths --impure --expr \
|
||||
'with import <nixpkgs> {}; python3.withPackages (ps: [ ps.pyyaml ps.argon2-cffi ])')
|
||||
nix shell nixpkgs#authelia -c $py/bin/python3 scripts/guildhall-join-check.py
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
TMP = Path(tempfile.mkdtemp())
|
||||
os.environ["GUILDHALL_JOIN_STATE"] = str(TMP / "state")
|
||||
os.environ["GUILDHALL_JOIN_USERS"] = str(TMP / "users_database.yml")
|
||||
|
||||
spec = importlib.util.spec_from_file_location("join", HERE / "guildhall-join.py")
|
||||
j = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(j)
|
||||
|
||||
ORIGINAL = """users:
|
||||
fred:
|
||||
password: "$argon2id$v=19$m=65536,t=3,p=4$c2FsdA$aGFzaA"
|
||||
displayname: Fred
|
||||
email: fred@example.org
|
||||
groups: [household]
|
||||
|
||||
kayla:
|
||||
password: "$argon2id$v=19$m=65536,t=3,p=4$c2FsdA$aGFzaA"
|
||||
displayname: Kayla
|
||||
groups: [household]
|
||||
"""
|
||||
j.USERS_FILE.write_text(ORIGINAL)
|
||||
failures = 0
|
||||
|
||||
|
||||
def check(label, ok):
|
||||
global failures
|
||||
print(("ok " if ok else "FAIL ") + label)
|
||||
failures += not ok
|
||||
|
||||
|
||||
def refused(fn, *args):
|
||||
try:
|
||||
fn(*args)
|
||||
except j.Refused as reason:
|
||||
return str(reason)
|
||||
return None
|
||||
|
||||
|
||||
token = j.mint("for alex")
|
||||
check("fresh invite is open", j.open_invite(token) is not None)
|
||||
check("state holds digests, not tokens", token not in (TMP / "state" / "invites.json").read_text())
|
||||
check("unknown token is closed", j.open_invite("x" * 24) is None)
|
||||
|
||||
check("bad username refused", refused(j.sign_up, token, "Al", "", "longpassword", "longpassword"))
|
||||
check("existing username refused", refused(j.sign_up, token, "Kayla", "", "longpassword", "longpassword"))
|
||||
check("short password refused", refused(j.sign_up, token, "alex", "", "short", "short"))
|
||||
check("mismatched passwords refused", refused(j.sign_up, token, "alex", "", "longpassword", "longpassword2"))
|
||||
check("refusals leave the file alone", j.USERS_FILE.read_text() == ORIGINAL)
|
||||
check("refusals leave the invite open", j.open_invite(token) is not None)
|
||||
|
||||
name = j.sign_up(token, " Alex ", 'Alex "the Bold" Æ', "correct horse battery", "correct horse battery")
|
||||
users = yaml.safe_load(j.USERS_FILE.read_text())["users"]
|
||||
check("username normalised", name == "alex")
|
||||
check("old users unchanged", {k: users[k] for k in ("fred", "kayla")} == yaml.safe_load(ORIGINAL)["users"])
|
||||
check("original text kept byte for byte", j.USERS_FILE.read_text().startswith(ORIGINAL.rstrip("\n")))
|
||||
check("new user only in guildhall group", users["alex"]["groups"] == ["guildhall"])
|
||||
check("display name survives quoting", users["alex"]["displayname"] == 'Alex "the Bold" Æ')
|
||||
check("invite is spent", j.open_invite(token) is None)
|
||||
check("spent invite refused", refused(j.sign_up, token, "sam", "", "longpassword", "longpassword"))
|
||||
|
||||
expired = j.mint("old", now=1.0)
|
||||
check("expired invite is closed", j.open_invite(expired) is None)
|
||||
|
||||
if shutil.which("authelia"):
|
||||
result = subprocess.run(
|
||||
["authelia", "crypto", "hash", "validate", "--password", "correct horse battery", "--", users["alex"]["password"]],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
check("authelia accepts the hash", result.returncode == 0 and "matches" in result.stdout.lower())
|
||||
else:
|
||||
print("skip authelia hash check (authelia not on PATH)")
|
||||
|
||||
shutil.rmtree(TMP)
|
||||
sys.exit(1 if failures else 0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue