""" Regression tests for emergency-access visibility (finding #9). Two problems, both leaving the grantor blind to activity on their own vault: 1. Audit entries were written only under the acting user's id, and /api/auth/audit-log filters by user_id — so a grantee could request access and retrieve the snapshot without a single line appearing in the grantor's log or security dashboard. 2. Retrieval left no trace on the record at all: status stayed 'pending' and nothing counted, so repeated fetches were indistinguishable from none. Retrieval is deliberately still permitted after the first time — the grantor may be unable to re-provision, which is the whole premise — so the fix is visibility and revocability, not blocking. """ from datetime import datetime, timedelta, timezone from app import db from app.models.audit_log import AuditLog from app.models.emergency_access import EmergencyAccess from tests.conftest import auth_headers, make_user GRANTOR = 'owner@example.com' GRANTEE = 'trusted@example.com' def _pair(client): """Create grantor + grantee, returns (grantor_token, grantee_token).""" g_token, _ = make_user(client, GRANTOR, 'HASH-O', 'SALT-O') t_token, _ = make_user(client, GRANTEE, 'HASH-T', 'SALT-T') return g_token, t_token def _grant(client, grantor_token, grantee_token, wait_days=7): res = client.post('/api/emergency', headers=auth_headers(grantor_token), json={'grantee_email': GRANTEE, 'wait_days': wait_days}) assert res.status_code == 201, res.get_json() ea_id = res.get_json()['id'] assert client.post(f'/api/emergency/{ea_id}/accept', headers=auth_headers(grantee_token)).status_code == 200 assert client.post(f'/api/emergency/{ea_id}/provide', headers=auth_headers(grantor_token), json={'enc_vault': '[{"id":1,"enc_data":"X","iv":"Y","enc_name":"N","iv_name":"I"}]'} ).status_code == 200 return ea_id def _grantor_log(client, token): res = client.get('/api/auth/audit-log?limit=200', headers=auth_headers(token)) assert res.status_code == 200 return res.get_json()['entries'] def _elapse_wait(ea_id): """Backdate the request so the wait period has passed.""" ea = db.session.get(EmergencyAccess, ea_id) ea.request_initiated_at = ( datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=ea.wait_days + 1) ) db.session.commit() def test_access_request_appears_in_the_grantors_audit_log(client, app): """The grantor has wait_days to notice and deny — they must be able to see it.""" g_token, t_token = _pair(client) ea_id = _grant(client, g_token, t_token) assert client.post(f'/api/emergency/{ea_id}/request', headers=auth_headers(t_token)).status_code == 200 entries = _grantor_log(client, g_token) requests = [e for e in entries if e['action'] == 'emergency_access.request'] assert requests, 'the access request is invisible in the grantor audit log' assert 'ACTION REQUIRED' in requests[0]['detail'] assert GRANTEE in requests[0]['detail'] def test_vault_retrieval_appears_in_the_grantors_audit_log(client, app): g_token, t_token = _pair(client) ea_id = _grant(client, g_token, t_token) client.post(f'/api/emergency/{ea_id}/request', headers=auth_headers(t_token)) _elapse_wait(ea_id) assert client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)).status_code == 200 entries = _grantor_log(client, g_token) retrievals = [e for e in entries if e['action'] == 'emergency_access.vault_retrieved'] assert retrievals, 'vault retrieval is invisible in the grantor audit log' assert GRANTEE in retrievals[0]['detail'] def test_retrieval_is_counted_and_exposed_to_the_grantor(client, app): g_token, t_token = _pair(client) ea_id = _grant(client, g_token, t_token) client.post(f'/api/emergency/{ea_id}/request', headers=auth_headers(t_token)) _elapse_wait(ea_id) grants = client.get('/api/emergency', headers=auth_headers(g_token)).get_json()['grants'] assert grants[0]['vault_retrieval_count'] == 0 assert grants[0]['vault_retrieved_at'] is None for _ in range(3): assert client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)).status_code == 200 grants = client.get('/api/emergency', headers=auth_headers(g_token)).get_json()['grants'] assert grants[0]['vault_retrieval_count'] == 3, 'repeated retrieval not counted' assert grants[0]['vault_retrieved_at'] is not None, 'first retrieval not timestamped' def test_first_retrieval_timestamp_does_not_move(client, app): """vault_retrieved_at records FIRST access, so it cannot be reset by re-fetching.""" g_token, t_token = _pair(client) ea_id = _grant(client, g_token, t_token) client.post(f'/api/emergency/{ea_id}/request', headers=auth_headers(t_token)) _elapse_wait(ea_id) client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)) first = db.session.get(EmergencyAccess, ea_id).vault_retrieved_at client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)) assert db.session.get(EmergencyAccess, ea_id).vault_retrieved_at == first def test_grantor_can_still_revoke_after_retrieval(client, app): """Visibility is only useful if the grantor can act on it.""" g_token, t_token = _pair(client) ea_id = _grant(client, g_token, t_token) client.post(f'/api/emergency/{ea_id}/request', headers=auth_headers(t_token)) _elapse_wait(ea_id) client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)) assert client.delete(f'/api/emergency/{ea_id}', headers=auth_headers(g_token)).status_code == 200 assert client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)).status_code == 404 def test_acceptance_is_visible_to_the_grantor(client, app): g_token, t_token = _pair(client) _grant(client, g_token, t_token) accepts = [e for e in _grantor_log(client, g_token) if e['action'] == 'emergency_access.accept'] assert accepts, 'grantee acceptance is invisible to the grantor' def test_retrieval_before_the_wait_elapses_is_still_refused(client, app): """The wait period remains the gate — none of this loosens it.""" g_token, t_token = _pair(client) ea_id = _grant(client, g_token, t_token) client.post(f'/api/emergency/{ea_id}/request', headers=auth_headers(t_token)) res = client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)) assert res.status_code == 403 assert db.session.get(EmergencyAccess, ea_id).vault_retrieval_count == 0 def test_denying_a_request_stops_retrieval(client, app): g_token, t_token = _pair(client) ea_id = _grant(client, g_token, t_token) client.post(f'/api/emergency/{ea_id}/request', headers=auth_headers(t_token)) assert client.post(f'/api/emergency/{ea_id}/deny', headers=auth_headers(g_token)).status_code == 200 _elapse_wait(ea_id) # even with time passed, the request was cancelled res = client.get(f'/api/emergency/{ea_id}/vault', headers=auth_headers(t_token)) assert res.status_code == 403