Aug 26 - Enhance security 4
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled

This commit is contained in:
2026-08-26 14:19:25 -04:00
parent cc216b0d98
commit b84a6d9245
11 changed files with 546 additions and 33 deletions
+72 -21
View File
@@ -41,6 +41,39 @@ def list_emergency():
}), 200
def _log_for_both(ea: EmergencyAccess, action: str, grantor_detail: str,
grantee_detail: str) -> None:
"""
Write the audit entry twice — once under each party's user_id.
/api/auth/audit-log filters by user_id, so an entry written only under the
acting user is invisible to the other party. That meant a grantee could
request access and retrieve the vault snapshot without a single line of it
appearing in the grantor's own audit log or security dashboard — the person
whose vault it was had no way to see it had happened.
Until email notifications exist (roadmap item 4), the grantor's audit log is
the only channel that reaches them, so it must carry these events.
"""
AuditLog.log(
user_id=ea.grantor_id,
action=action,
resource_type='emergency_access',
resource_id=ea.id,
detail=grantor_detail,
ip_address=client_ip(),
)
if ea.grantee_id and ea.grantee_id != ea.grantor_id:
AuditLog.log(
user_id=ea.grantee_id,
action=action,
resource_type='emergency_access',
resource_id=ea.id,
detail=grantee_detail,
ip_address=client_ip(),
)
def _ea_as_grantee(ea: EmergencyAccess, grantor: 'User | None' = None) -> dict:
if grantor is None:
grantor = db.session.get(User, ea.grantor_id)
@@ -150,14 +183,13 @@ def accept_emergency(ea_id):
ea.status = 'accepted'
ea.grantee_id = user.id
db.session.flush()
AuditLog.log(
user_id=g.current_user_id,
action='emergency_access.accept',
resource_type='emergency_access',
resource_id=ea.id,
detail=f'Accepted emergency access invitation from grantor_id={ea.grantor_id}',
ip_address=client_ip(),
_log_for_both(
ea,
'emergency_access.accept',
grantor_detail=f'{ea.grantee_email} accepted your emergency access invitation',
grantee_detail=f'Accepted emergency access invitation from grantor_id={ea.grantor_id}',
)
db.session.commit()
@@ -221,14 +253,21 @@ def request_access(ea_id):
ea.status = 'pending'
ea.request_initiated_at = datetime.now(timezone.utc).replace(tzinfo=None)
db.session.flush()
AuditLog.log(
user_id=g.current_user_id,
action='emergency_access.request',
resource_type='emergency_access',
resource_id=ea.id,
detail=f'Requested emergency vault access from grantor_id={ea.grantor_id} (wait: {ea.wait_days}d)',
ip_address=client_ip(),
# The grantor has `wait_days` to notice and deny this. If it only appeared in
# the grantee's audit log they would never see it in time.
_log_for_both(
ea,
'emergency_access.request',
grantor_detail=(
f'ACTION REQUIRED: {ea.grantee_email} requested emergency access to '
f'your vault. It unlocks in {ea.wait_days} day(s) unless you deny it.'
),
grantee_detail=(
f'Requested emergency vault access from grantor_id={ea.grantor_id} '
f'(wait: {ea.wait_days}d)'
),
)
db.session.commit()
@@ -291,13 +330,25 @@ def get_emergency_vault(ea_id):
'error': f'Wait period not yet elapsed ({days_left:.1f} day(s) remaining)'
}), 403
AuditLog.log(
user_id=g.current_user_id,
action='emergency_access.vault_retrieved',
resource_type='emergency_access',
resource_id=ea.id,
detail=f'Retrieved emergency vault from grantor_id={ea.grantor_id}',
ip_address=client_ip(),
# Record the retrieval. Access is deliberately not revoked afterwards — the
# grantor may be unable to re-provision, and a failed import must not strand
# the grantee — but every retrieval is counted and shown to the grantor, who
# can revoke the grant outright.
now = datetime.now(timezone.utc).replace(tzinfo=None)
is_first = ea.vault_retrieved_at is None
if is_first:
ea.vault_retrieved_at = now
ea.vault_retrieval_count = (ea.vault_retrieval_count or 0) + 1
_log_for_both(
ea,
'emergency_access.vault_retrieved',
grantor_detail=(
f'{ea.grantee_email} retrieved your emergency vault snapshot '
f'({"first" if is_first else f"retrieval #{ea.vault_retrieval_count}"}). '
'Remove the grant if this was not expected.'
),
grantee_detail=f'Retrieved emergency vault from grantor_id={ea.grantor_id}',
)
db.session.commit()