import hashlib import json import time from pathlib import Path import pyotp import pytest from cryptography.fernet import Fernet from gewu.adapters.sqlite_identity import SQLiteIdentityStore def issue_challenge( store: SQLiteIdentityStore, *, principal_id: str, challenge_id: str, domain: str, now: int, ) -> None: store.issue_owner_challenge( challenge_id=challenge_id, owner_principal_id=principal_id, proposal_id="proposal-one", proposal_revision=1, payload_digest="a" * 64, diff_digest="b" * 64, domains=(domain,), expires_at=now + 5 * 60, now=now, ) def test_credentials_are_hashed_revocable_and_project_scoped(tmp_path: Path) -> None: store = SQLiteIdentityStore(tmp_path / "private" / "identity.sqlite3") token = store.provision( credential_id="agent-key-1", principal_id="agent-pilot", roles=("agent",), project_ids=("dome-performance",), ) principal = store.authenticate(token) assert principal is not None assert principal.principal_id == "agent-pilot" assert principal.credential_id == "agent-key-1" assert principal.can_read_project("dome-performance") assert not principal.can_read_project("interactive-video") assert token.encode() not in (tmp_path / "private" / "identity.sqlite3").read_bytes() assert store.revoke("agent-key-1") assert store.authenticate(token) is None assert not store.revoke("agent-key-1") def test_admin_role_can_read_without_project_grant(tmp_path: Path) -> None: store = SQLiteIdentityStore(tmp_path / "identity.sqlite3") token = store.provision( credential_id="admin-key", principal_id="operator-1", roles=("admin",), ) principal = store.authenticate(token) assert principal is not None assert principal.can_read_project("dome-performance") def test_publication_authority_snapshot_detects_grant_or_role_change(tmp_path: Path) -> None: store = SQLiteIdentityStore(tmp_path / "identity.sqlite3") token = store.provision( credential_id="agent-key", principal_id="agent-one", roles=("agent",), project_ids=("dome-performance", "shared"), ) principal = store.authenticate(token) assert principal is not None assert store.is_current( principal.principal_id, principal.credential_id, roles=principal.roles, project_ids=principal.project_ids, ) assert not store.is_current( principal.principal_id, principal.credential_id, roles=principal.roles, project_ids=frozenset({"dome-performance"}), ) def test_rotation_revokes_old_key_and_preserves_attribution_and_grants(tmp_path: Path) -> None: store = SQLiteIdentityStore(tmp_path / "identity.sqlite3") old_token = store.provision( credential_id="agent-key-old", principal_id="agent-pilot", roles=("agent",), project_ids=("dome-performance",), ) new_token = store.rotate("agent-key-old", "agent-key-new") assert store.authenticate(old_token) is None principal = store.authenticate(new_token) assert principal is not None assert principal.principal_id == "agent-pilot" assert principal.credential_id == "agent-key-new" assert principal.can_read_project("dome-performance") def test_browser_sessions_are_human_only_csrf_protected_and_revocable(tmp_path: Path) -> None: store = SQLiteIdentityStore(tmp_path / "identity.sqlite3") viewer_key = store.provision( credential_id="viewer-key", principal_id="viewer-one", roles=("viewer",), project_ids=("dome-performance",), ) agent_key = store.provision( credential_id="agent-key", principal_id="agent-one", roles=("agent",), project_ids=("dome-performance",), ) with pytest.raises(PermissionError, match="human browser identity"): store.create_browser_session(agent_key) session = store.create_browser_session(viewer_key) principal = store.authenticate_browser_session(session.session_token) assert principal is not None assert principal.auth_method == "browser_session" assert principal.can_read_project("dome-performance") assert not principal.can_submit_project("dome-performance") assert store.browser_csrf_valid(session.session_token, session.csrf_token) assert not store.browser_csrf_valid(session.session_token, "gwc_invalid") assert store.revoke_browser_session(session.session_token) assert store.authenticate_browser_session(session.session_token) is None assert not store.browser_csrf_valid(session.session_token, session.csrf_token) admin_key = store.provision( credential_id="human-admin-key", principal_id="human-admin", roles=("admin",), project_ids=("dome-performance",), ) admin_session = store.create_browser_session(admin_key) browser_admin = store.authenticate_browser_session(admin_session.session_token) assert browser_admin is not None and not browser_admin.can_submit_project("dome-performance") def test_revoking_browser_credential_invalidates_its_sessions(tmp_path: Path) -> None: store = SQLiteIdentityStore(tmp_path / "identity.sqlite3") token = store.provision( credential_id="human-key", principal_id="human-one", roles=("editor",), project_ids=("dome-performance",), ) session = store.create_browser_session(token) assert store.authenticate_browser_session(session.session_token) is not None assert store.revoke("human-key") assert store.authenticate_browser_session(session.session_token) is None def test_identity_store_rejects_symlinked_database_path(tmp_path: Path) -> None: target = tmp_path / "real" target.mkdir() link = tmp_path / "linked" link.symlink_to(target, target_is_directory=True) with pytest.raises(ValueError, match="symbolic links"): SQLiteIdentityStore(link / "identity.sqlite3") def test_owner_session_is_challenge_bound_short_lived_and_separate_from_agent_auth( tmp_path: Path, ) -> None: store = SQLiteIdentityStore( tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key() ) secret = pyotp.random_base32() store.provision_owner( principal_id="project-owner", password="correct horse battery staple", totp_secret=secret, project_ids=("dome-performance",), ) database_bytes = (tmp_path / "identity.sqlite3").read_bytes() assert secret.encode() not in database_bytes assert b"correct horse battery staple" not in database_bytes now = int(time.time()) issue_challenge( store, principal_id="project-owner", challenge_id="challenge-1", domain="dome-performance", now=now, ) session = store.authenticate_owner( principal_id="project-owner", password="correct horse battery staple", totp_code=pyotp.TOTP(secret).at(now), challenge_id="challenge-1", audience="https://gewu.example", now=now, ) assert session is not None assert session.expires_at.timestamp() == now + 5 * 60 principal = store.authenticate_owner_session( session.session_token, challenge_id="challenge-1", audience="https://gewu.example", ) assert principal is not None assert principal.roles == frozenset({"owner"}) assert principal.auth_method == "owner_session" assert principal.can_read_project("dome-performance") assert not principal.can_submit_project("dome-performance") assert store.authenticate(session.session_token) is None assert ( store.authenticate_owner_session( session.session_token, challenge_id="challenge-2", audience="https://gewu.example", ) is None ) assert ( store.authenticate_owner_session( session.session_token, challenge_id="challenge-1", audience="https://other.example", ) is None ) def test_owner_totp_cannot_be_replayed_and_login_failures_are_rate_limited( tmp_path: Path, ) -> None: store = SQLiteIdentityStore( tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key() ) secret = pyotp.random_base32() store.provision_owner( principal_id="project-owner", password="correct horse battery staple", totp_secret=secret, project_ids=("dome-performance",), ) now = int(time.time()) issue_challenge( store, principal_id="project-owner", challenge_id="challenge-1", domain="dome-performance", now=now, ) code = pyotp.TOTP(secret).at(now) args = { "principal_id": "project-owner", "password": "correct horse battery staple", "totp_code": code, "challenge_id": "challenge-1", "audience": "https://gewu.example", "now": now, } assert store.authenticate_owner(**args) is not None assert store.authenticate_owner(**args) is None for attempt in range(5): challenge_id = f"challenge-bad-{attempt}" issue_challenge( store, principal_id="project-owner", challenge_id=challenge_id, domain="dome-performance", now=now, ) assert ( store.authenticate_owner( principal_id="project-owner", password="incorrect password", totp_code="000000", challenge_id=challenge_id, audience="https://gewu.example", now=now, ) is None ) issue_challenge( store, principal_id="project-owner", challenge_id="challenge-final", domain="dome-performance", now=now, ) assert ( store.authenticate_owner( principal_id="project-owner", password="incorrect password", totp_code="000000", challenge_id="challenge-final", audience="https://gewu.example", now=now, ) is None ) def test_owner_operations_stay_disabled_without_encryption_key(tmp_path: Path) -> None: store = SQLiteIdentityStore(tmp_path / "identity.sqlite3") with pytest.raises(RuntimeError, match="encryption key is not configured"): store.provision_owner( principal_id="project-owner", password="correct horse battery staple", totp_secret=pyotp.random_base32(), project_ids=(), ) assert ( store.authenticate_owner( principal_id="project-owner", password="correct horse battery staple", totp_code="000000", challenge_id="challenge-1", audience="https://gewu.example", ) is None ) def test_owner_challenge_retry_is_bound_to_payload_diff_and_granted_domains( tmp_path: Path, ) -> None: store = SQLiteIdentityStore( tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key() ) store.provision_owner( principal_id="project-owner", password="correct horse battery staple", totp_secret=pyotp.random_base32(), project_ids=("dome-performance",), ) now = int(time.time()) issue_challenge( store, principal_id="project-owner", challenge_id="challenge-1", domain="dome-performance", now=now, ) issue_challenge( store, principal_id="project-owner", challenge_id="challenge-1", domain="dome-performance", now=now, ) with pytest.raises(ValueError, match="different content"): store.issue_owner_challenge( challenge_id="challenge-1", owner_principal_id="project-owner", proposal_id="proposal-one", proposal_revision=1, payload_digest="c" * 64, diff_digest="b" * 64, domains=("dome-performance",), expires_at=now + 300, now=now, ) def test_owner_decision_is_exact_diff_bound_and_idempotent(tmp_path: Path) -> None: store = SQLiteIdentityStore( tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key() ) secret = pyotp.random_base32() store.provision_owner( principal_id="project-owner", password="correct horse battery staple", totp_secret=secret, project_ids=("dome-performance",), ) now = int(time.time()) diff_json = json.dumps({"files": [{"path": "projects/dome/nodes/a.yaml"}]}) diff_digest = hashlib.sha256(diff_json.encode()).hexdigest() store.issue_owner_challenge( challenge_id="challenge-bound", owner_principal_id="project-owner", proposal_id="proposal-one", proposal_revision=1, payload_digest="a" * 64, diff_digest=diff_digest, domains=("dome-performance",), expires_at=now + 300, diff_json=diff_json, now=now, ) token = store.authenticate_owner( principal_id="project-owner", password="correct horse battery staple", totp_code=pyotp.TOTP(secret).at(now), challenge_id="challenge-bound", audience="gewu-test", now=now, ) assert token is not None assert ( store.authenticate_owner_session( token.session_token, challenge_id="challenge-bound", audience="gewu-test" ) is not None ) challenge = store.get_owner_challenge("challenge-bound", principal_id="project-owner") assert challenge is not None assert challenge["diff_json"] == diff_json arguments = { "challenge_id": "challenge-bound", "principal_id": "project-owner", "proposal_revision": 1, "payload_digest": "a" * 64, "diff_digest": diff_digest, "decision": "authorize", "rationale": "Reviewed exact proposed files and approve the policy exception.", "owner_session_id": hashlib.sha256(token.session_token.encode()).hexdigest(), "audience": "gewu-test", "now": now, } with pytest.raises(ValueError, match="does not match"): store.decide_owner_challenge(**{**arguments, "diff_digest": "b" * 64}) with pytest.raises(PermissionError, match="authority is unavailable"): store.decide_owner_challenge(**{**arguments, "owner_session_id": "0" * 64}) first = store.decide_owner_challenge(**arguments) assert first["status"] == "authorized" assert store.decide_owner_challenge(**arguments) == first with pytest.raises(ValueError, match="different decision"): store.decide_owner_challenge(**{**arguments, "decision": "reject"}) with pytest.raises(PermissionError, match="every affected domain"): store.issue_owner_challenge( challenge_id="challenge-out-of-scope", owner_principal_id="project-owner", proposal_id="proposal-one", proposal_revision=1, payload_digest="a" * 64, diff_digest="b" * 64, domains=("interactive-video",), expires_at=now + 300, now=now, ) def test_owner_grant_change_and_revocation_invalidate_sessions(tmp_path: Path) -> None: store = SQLiteIdentityStore( tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key() ) secret = pyotp.random_base32() store.provision_owner( principal_id="project-owner", password="correct horse battery staple", totp_secret=secret, project_ids=("dome-performance",), ) now = int(time.time()) issue_challenge( store, principal_id="project-owner", challenge_id="challenge-1", domain="dome-performance", now=now, ) first = store.authenticate_owner( principal_id="project-owner", password="correct horse battery staple", totp_code=pyotp.TOTP(secret).at(now), challenge_id="challenge-1", audience="https://gewu.example", now=now, ) assert first is not None assert store.update_owner_grants("project-owner", ("interactive-video",)) assert ( store.authenticate_owner_session( first.session_token, challenge_id="challenge-1", audience="https://gewu.example", ) is None ) second_counter = now // 30 + 1 second_now = second_counter * 30 issue_challenge( store, principal_id="project-owner", challenge_id="challenge-2", domain="interactive-video", now=second_now, ) second = store.authenticate_owner( principal_id="project-owner", password="correct horse battery staple", totp_code=pyotp.TOTP(secret).generate_otp(second_counter), challenge_id="challenge-2", audience="https://gewu.example", now=second_now, ) assert second is not None assert store.revoke_owner("project-owner") assert ( store.authenticate_owner_session( second.session_token, challenge_id="challenge-2", audience="https://gewu.example", ) is None )