94 lines
3.5 KiB
Python
94 lines
3.5 KiB
Python
"""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)
|