Stage 209: handlers.list_audit_logs entity_id filter
Browse files- infra/api/handlers.py +57 -3
infra/api/handlers.py
CHANGED
|
@@ -1148,12 +1148,66 @@ def export_tenant(svc: OrgStateService, tenant_id: str) -> dict:
|
|
| 1148 |
def list_audit_logs(svc: OrgStateService, *,
|
| 1149 |
actor: Optional[str] = None,
|
| 1150 |
action: Optional[str] = None,
|
|
|
|
| 1151 |
tenant_id: Optional[str] = None,
|
| 1152 |
limit: int = 100,
|
| 1153 |
offset: int = 0) -> dict:
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1157 |
|
| 1158 |
|
| 1159 |
def run_from_stored(svc: OrgStateService, tenant_id: str,
|
|
|
|
| 1148 |
def list_audit_logs(svc: OrgStateService, *,
|
| 1149 |
actor: Optional[str] = None,
|
| 1150 |
action: Optional[str] = None,
|
| 1151 |
+
entity_id: Optional[str] = None,
|
| 1152 |
tenant_id: Optional[str] = None,
|
| 1153 |
limit: int = 100,
|
| 1154 |
offset: int = 0) -> dict:
|
| 1155 |
+
"""Stage 209 — ``entity_id`` filter matches audit rows whose
|
| 1156 |
+
payload references the entity. Currently used by
|
| 1157 |
+
bulk_update_decisions (which lists ``entity_ids`` per Stage 206)
|
| 1158 |
+
and decision-status events (whose payload carries a single
|
| 1159 |
+
``entity_id``). Other action types are unaffected.
|
| 1160 |
+
|
| 1161 |
+
Implementation: post-filter on the JSON payload. We over-fetch
|
| 1162 |
+
(10× the requested limit) from the DB and trim after filtering
|
| 1163 |
+
so the typical "find a few touches on widget_42" query
|
| 1164 |
+
returns the right rows without a schema change. For very
|
| 1165 |
+
large tenants with mostly-irrelevant rows we may return
|
| 1166 |
+
fewer than ``limit`` even when more matches exist past the
|
| 1167 |
+
over-fetch window; a future stage can add a denormalized
|
| 1168 |
+
entity_id index column.
|
| 1169 |
+
"""
|
| 1170 |
+
if entity_id is None:
|
| 1171 |
+
rows = svc.audit.list(
|
| 1172 |
+
actor=actor, action=action, tenant_id=tenant_id,
|
| 1173 |
+
limit=limit, offset=offset,
|
| 1174 |
+
)
|
| 1175 |
+
return {"audit_logs": rows, "limit": limit, "offset": offset}
|
| 1176 |
+
|
| 1177 |
+
# Entity filter path — over-fetch + post-filter.
|
| 1178 |
+
raw = svc.audit.list(
|
| 1179 |
+
actor=actor, action=action, tenant_id=tenant_id,
|
| 1180 |
+
limit=limit * 10, offset=offset,
|
| 1181 |
+
)
|
| 1182 |
+
matched = []
|
| 1183 |
+
for r in raw:
|
| 1184 |
+
if _audit_row_mentions_entity(r, entity_id):
|
| 1185 |
+
matched.append(r)
|
| 1186 |
+
if len(matched) >= limit:
|
| 1187 |
+
break
|
| 1188 |
+
return {"audit_logs": matched, "limit": limit, "offset": offset}
|
| 1189 |
+
|
| 1190 |
+
|
| 1191 |
+
def _audit_row_mentions_entity(row: dict, entity_id: str) -> bool:
|
| 1192 |
+
"""Return True iff the row's payload references the entity.
|
| 1193 |
+
|
| 1194 |
+
Two shapes we currently know:
|
| 1195 |
+
- bulk_update_decisions (Stage 206): payload.entity_ids is a list.
|
| 1196 |
+
- decision-side actions: payload.entity_id is a single string.
|
| 1197 |
+
Falls through to False on unknown shapes — adding new action
|
| 1198 |
+
types is opt-in via this matcher, NOT a leaky default that
|
| 1199 |
+
would silently include unrelated rows.
|
| 1200 |
+
"""
|
| 1201 |
+
payload = row.get("payload") or {}
|
| 1202 |
+
if not isinstance(payload, dict):
|
| 1203 |
+
return False
|
| 1204 |
+
ids = payload.get("entity_ids")
|
| 1205 |
+
if isinstance(ids, list) and entity_id in ids:
|
| 1206 |
+
return True
|
| 1207 |
+
single = payload.get("entity_id")
|
| 1208 |
+
if isinstance(single, str) and single == entity_id:
|
| 1209 |
+
return True
|
| 1210 |
+
return False
|
| 1211 |
|
| 1212 |
|
| 1213 |
def run_from_stored(svc: OrgStateService, tenant_id: str,
|