"""Admin gate — per-member TOTP enrollment + revocation isolation.""" from __future__ import annotations import json import pyotp import pytest from psyc.cockpit import adminauth @pytest.fixture def fresh_state(tmp_path, monkeypatch): monkeypatch.setattr(adminauth, "_STATE_PATH", tmp_path / "admin_auth.json") yield tmp_path / "admin_auth.json" def _code_for_secret(secret: str) -> str: return pyotp.TOTP(secret).now() def _secret_of(state_path, member_id: str) -> str: data = json.loads(state_path.read_text()) return next(m["secret"] for m in data["members"] if m["id"] == member_id) def test_starts_unbootstrapped(fresh_state): assert adminauth.is_bootstrapped() is False assert adminauth.members() == [] def test_bootstrap_promotes_pending_to_owner(fresh_state): code = adminauth.current_code() # pending secret assert adminauth.verify(code) == "owner" assert adminauth.is_bootstrapped() is True assert [m["label"] for m in adminauth.members()] == ["owner"] def test_add_member_then_each_secret_authenticates(fresh_state): adminauth.verify(adminauth.current_code()) # bootstrap owner aid, _ = adminauth.add_member("alice") bid, _ = adminauth.add_member("bob") assert adminauth.verify(_code_for_secret(_secret_of(fresh_state, aid))) == "alice" assert adminauth.verify(_code_for_secret(_secret_of(fresh_state, bid))) == "bob" def test_revoke_isolates_one_member(fresh_state): adminauth.verify(adminauth.current_code()) aid, _ = adminauth.add_member("alice") bid, _ = adminauth.add_member("bob") a_secret = _secret_of(fresh_state, aid) b_secret = _secret_of(fresh_state, bid) assert adminauth.revoke_member(aid) is True # Alice's code no longer works… assert adminauth.verify(_code_for_secret(a_secret)) is None # …but Bob's still does. assert adminauth.verify(_code_for_secret(b_secret)) == "bob" # Alice is gone from the active roster, Bob remains. labels = [m["label"] for m in adminauth.members()] assert "alice" not in labels and "bob" in labels def test_members_never_expose_secrets(fresh_state): adminauth.verify(adminauth.current_code()) adminauth.add_member("alice") for m in adminauth.members(): assert "secret" not in m def test_revoke_unknown_id_is_false(fresh_state): adminauth.verify(adminauth.current_code()) assert adminauth.revoke_member("deadbeef") is False def test_migrates_old_single_secret_format(fresh_state): # Simulate the pre-stage-27 state file. old_secret = pyotp.random_base32() fresh_state.write_text(json.dumps({ "totp_secret": old_secret, "session_secret": "s", "provisioned": True, })) # Old enrolled secret should authenticate as the migrated 'owner'. assert adminauth.verify(_code_for_secret(old_secret)) == "owner" assert [m["label"] for m in adminauth.members()] == ["owner"]