feat(audit): split actor from subject on auth events

auth_events.user_id was overloaded and meant something different per call
site: admin mutations filed the row under the target user and hid the acting
admin in metadata->>'by', team events filed it under the actor, and quota
events switched between the two depending on scope. The practical consequence
was that "show me everything admin X did" had no answer.

Adds actor_id (never NULL) and target_id (NULL when the event is not about a
user) alongside the existing column, which keeps its meaning as the per-user
feed key so nothing historical is rewritten. The backfill recovers the actor
from metadata for rows that predate the columns.

Also adds the indexes the cross-user admin feed needs. The only index was
(user_id, created_at DESC), so the global feed -- which orders by created_at
with no user predicate -- fell back to a sequential scan plus a sort on every
page.

The feed gains actor, multi-event, until and free-text search filters, plus a
distinct-event catalogue so the UI can offer what the instance recorded
instead of asking operators to type a name from memory.
This commit is contained in:
arc53-machine committed 2026-09-22 10:18:16 +01:00
1 parent f5b02ed374
commit cb5343397f
11 files changed
+483 -15

No files matched your search

+10
View File
@@ -171,6 +171,8 @@ class AdminUserResource(Resource):
ip=request.remote_addr,
user_agent=request.headers.get("User-Agent"),
metadata={"by": actor, "via": "admin_api"},
actor_id=actor,
target_id=user_id,
)
if not active:
# Best-effort live-session revocation (mirrors SCIM deactivation).
@@ -203,6 +205,8 @@ class AdminUserRoleResource(Resource):
"granted_by": actor,
"via": "admin_api",
},
actor_id=actor,
target_id=user_id,
)
return make_response(
jsonify({"success": True, "granted": inserted, "role": ROLE_ADMIN}), 200
@@ -238,6 +242,8 @@ class AdminUserRoleResource(Resource):
"revoked_by": actor,
"via": "admin_api",
},
actor_id=actor,
target_id=user_id,
)
return make_response(jsonify({"success": True, "revoked": removed}), 200)
@@ -261,6 +267,8 @@ class AdminUserSessionsResource(Resource):
ip=request.remote_addr,
user_agent=request.headers.get("User-Agent"),
metadata={"token_id": token_id, "by": _actor(), "via": "admin_sessions_revoked"},
actor_id=_actor(),
target_id=user_id,
)
AuthEventsRepository(conn).insert(
user_id,
@@ -273,6 +281,8 @@ class AdminUserSessionsResource(Resource):
"persisted": ok,
"personal_access_tokens_revoked": len(revoked_token_ids),
},
actor_id=_actor(),
target_id=user_id,
)
return make_response(jsonify({"success": True, "revoked": ok}), 200)