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
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:
+72
-21
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user