Legal-i commited on
Commit
fa608ca
·
verified ·
1 Parent(s): c98858f

Stage 209: handlers.list_audit_logs entity_id filter

Browse files
Files changed (1) hide show
  1. 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
- rows = svc.audit.list(actor=actor, action=action, tenant_id=tenant_id,
1155
- limit=limit, offset=offset)
1156
- return {"audit_logs": rows, "limit": limit, "offset": offset}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,