rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
e6eb838
Β·
1 Parent(s): 2d0235b

feat(#77 / #52-residual-5): admin prune for persisted uploaded docs

Browse files

Persisted uploaded marketplace docs have NO TTL by design (public
catalogue cards). Added the sanctioned operator removal path:
- backend/uploaded_docs.prune_persisted_upload(policy_id | prefix=...) β€”
HARD path-safety guard: only ever rmtrees a DIRECT child of
UPLOADED_DOCS_DIR (can never touch rag/corpus, 40-data, curated/
extracted); traversal attempt is skipped (resolves inside root) /
empty prefix RAISES (no silent no-op).
- POST /api/admin/uploaded-docs/prune (password-gated, reuses _check_admin):
prunes dir(s) + deletes the doc's chunks from the GLOBAL policies Chroma
collection + busts _MG_CACHE/_CORPUS_PDF_IDX so /api/policies/all drops
the card immediately; collects (never swallows) Chroma errors.
- tests/test_uploaded_docs_prune.py β€” exact+prefix removal, non-matching
docs preserved, traversal-safe, empty-prefix rejected (3/3).

Needed because my own live #52 verification created disposable
'Zzz E2E Verify' (user-upload__e2e-verify-*) cards on prod; this is how
they (and any future test/abuse upload) get cleanly removed. Full
pytest rc=0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

backend/admin.py CHANGED
@@ -1139,3 +1139,60 @@ async def admin_recommendation_history(
1139
  "total": len(events),
1140
  "snapshot_ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
1141
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  "total": len(events),
1140
  "snapshot_ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
1141
  }
1142
+
1143
+
1144
+ # ---------------------------------------------------------------------------
1145
+ # #77 / #52-residual-5 β€” operator prune of persisted uploaded marketplace
1146
+ # docs. Persisted uploads have NO TTL by design (they are public catalogue
1147
+ # cards); this is the sanctioned way to remove a test/abuse upload. Removes
1148
+ # the persisted dir (path-safety-guarded in backend.uploaded_docs) + the
1149
+ # doc's chunks from the GLOBAL policies collection + busts the marketplace
1150
+ # grade / corpus-pdf caches so /api/policies/all drops the card at once.
1151
+ # ---------------------------------------------------------------------------
1152
+ class UploadedDocPruneRequest(BaseModel):
1153
+ policy_id: Optional[str] = None
1154
+ prefix: Optional[str] = None # e.g. "user-upload__e2e-verify" β€” bulk
1155
+
1156
+
1157
+ @router.post("/api/admin/uploaded-docs/prune")
1158
+ async def admin_prune_uploaded_docs(
1159
+ body: UploadedDocPruneRequest,
1160
+ request: Request,
1161
+ x_admin_password: Optional[str] = Header(default=None, alias="X-Admin-Password"),
1162
+ ):
1163
+ _check_admin(request, x_admin_password)
1164
+ if not body.policy_id and body.prefix is None:
1165
+ raise HTTPException(status_code=400, detail="provide policy_id or prefix")
1166
+ from backend import uploaded_docs as _udocs
1167
+
1168
+ res = _udocs.prune_persisted_upload(body.policy_id, prefix=body.prefix)
1169
+ chroma_deleted: list[str] = []
1170
+ chroma_errors: list[str] = []
1171
+ if res.get("removed"):
1172
+ try:
1173
+ from rag.ingest import get_chroma_collection
1174
+ _col = get_chroma_collection()
1175
+ for pid in res["removed"]:
1176
+ try:
1177
+ _col.delete(where={"policy_id": pid})
1178
+ chroma_deleted.append(pid)
1179
+ except Exception as e: # noqa: BLE001 β€” surface, don't swallow
1180
+ chroma_errors.append(f"{pid}: {type(e).__name__}: {e}")
1181
+ except Exception as e: # noqa: BLE001
1182
+ chroma_errors.append(f"collection: {type(e).__name__}: {e}")
1183
+ cache_bust = "ok"
1184
+ try:
1185
+ import backend.main as _m
1186
+ _m._CORPUS_PDF_IDX = None
1187
+ _mg = getattr(_m, "_MG_CACHE", None)
1188
+ if isinstance(_mg, dict):
1189
+ _mg["sig"] = None
1190
+ _mg["index"] = None
1191
+ except Exception as e: # noqa: BLE001
1192
+ cache_bust = f"{type(e).__name__}: {e}"
1193
+ return {
1194
+ **res,
1195
+ "chroma_deleted": chroma_deleted,
1196
+ "chroma_errors": chroma_errors,
1197
+ "cache_bust": cache_bust,
1198
+ }
backend/uploaded_docs.py CHANGED
@@ -60,6 +60,7 @@ import hashlib
60
  import json
61
  import logging
62
  import re
 
63
  import time
64
  from pathlib import Path
65
  from typing import Any, Optional
@@ -99,6 +100,46 @@ def _doc_dir(policy_id: str) -> Path:
99
  return uploaded_docs_dir() / safe
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  # ---------------------------------------------------------------------------
103
  # Heuristic field extraction -> curated-facts-shaped record
104
  #
 
60
  import json
61
  import logging
62
  import re
63
+ import shutil
64
  import time
65
  from pathlib import Path
66
  from typing import Any, Optional
 
100
  return uploaded_docs_dir() / safe
101
 
102
 
103
+ def prune_persisted_upload(
104
+ policy_id: Optional[str] = None, *, prefix: Optional[str] = None
105
+ ) -> dict:
106
+ """Operator/abuse prune of persisted uploaded doc(s) (#52 residual #5,
107
+ #77). Pass an exact `policy_id` OR a `prefix` (e.g.
108
+ 'user-upload__e2e-verify' to bulk-remove test/abuse cards).
109
+
110
+ HARD GUARDRAIL: only ever removes a directory that is a DIRECT CHILD of
111
+ UPLOADED_DOCS_DIR β€” it can never touch rag/corpus, 40-data, or any
112
+ curated/extracted data. A path-safety violation RAISES (must surface;
113
+ a silent no-op here would be forbidden by the no-silent-failure rule).
114
+ Returns {removed:[ids], skipped:[ids-not-present], root}.
115
+ """
116
+ root = uploaded_docs_dir().resolve()
117
+ targets: list[str] = []
118
+ if policy_id:
119
+ targets.append(policy_id)
120
+ if prefix is not None:
121
+ pfx = re.sub(r"[^a-zA-Z0-9_.\-]+", "-", prefix).strip("-")
122
+ if not pfx:
123
+ raise RuntimeError("prune prefix is empty after sanitisation")
124
+ for d in sorted(root.glob("*")):
125
+ if d.is_dir() and d.name.startswith(pfx):
126
+ targets.append(d.name)
127
+ removed: list[str] = []
128
+ skipped: list[str] = []
129
+ for pid in dict.fromkeys(targets): # dedupe, preserve order
130
+ ddir = _doc_dir(pid).resolve()
131
+ if ddir == root or root not in ddir.parents:
132
+ raise RuntimeError(
133
+ f"refusing to prune outside uploaded-docs root: {pid!r}"
134
+ )
135
+ if not ddir.exists():
136
+ skipped.append(pid)
137
+ continue
138
+ shutil.rmtree(ddir)
139
+ removed.append(pid)
140
+ return {"removed": removed, "skipped": skipped, "root": str(root)}
141
+
142
+
143
  # ---------------------------------------------------------------------------
144
  # Heuristic field extraction -> curated-facts-shaped record
145
  #
tests/test_uploaded_docs_prune.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """#77 β€” prune_persisted_upload: removes a persisted uploaded doc, is
2
+ path-safety-guarded (can NEVER escape UPLOADED_DOCS_DIR), supports exact
3
+ id + prefix, and never silently no-ops a traversal attempt."""
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _REPO = Path(__file__).resolve().parent.parent
12
+ if str(_REPO) not in sys.path:
13
+ sys.path.insert(0, str(_REPO))
14
+
15
+
16
+ def _seed(root: Path, pid: str) -> Path:
17
+ from backend import uploaded_docs as u
18
+ d = u._doc_dir(pid)
19
+ d.mkdir(parents=True, exist_ok=True)
20
+ (d / "record.json").write_text('{"policy_id": "%s"}' % pid)
21
+ (d / "meta.json").write_text("{}")
22
+ return d
23
+
24
+
25
+ def test_prune_exact_and_prefix(tmp_path, monkeypatch):
26
+ from backend.config import settings
27
+ from backend import uploaded_docs as u
28
+ monkeypatch.setattr(settings, "UPLOADED_DOCS_DIR", tmp_path)
29
+
30
+ a = _seed(tmp_path, "user-upload__e2e-verify-a__zzz")
31
+ b = _seed(tmp_path, "user-upload__e2e-verify-b__zzz")
32
+ keep = _seed(tmp_path, "user-upload__real-user__myplan")
33
+ assert a.exists() and b.exists() and keep.exists()
34
+
35
+ # exact id
36
+ r1 = u.prune_persisted_upload("user-upload__e2e-verify-a__zzz")
37
+ assert r1["removed"] == ["user-upload__e2e-verify-a__zzz"]
38
+ assert not a.exists() and b.exists() and keep.exists()
39
+
40
+ # prefix (bulk) β€” only e2e-verify-*, never the real user doc
41
+ r2 = u.prune_persisted_upload(prefix="user-upload__e2e-verify")
42
+ assert "user-upload__e2e-verify-b__zzz" in r2["removed"]
43
+ assert not b.exists()
44
+ assert keep.exists(), "prefix prune must NOT touch non-matching docs"
45
+
46
+ # absent id β†’ skipped, not error, not silent
47
+ r3 = u.prune_persisted_upload("user-upload__does-not-exist")
48
+ assert r3["removed"] == [] and r3["skipped"] == ["user-upload__does-not-exist"]
49
+
50
+
51
+ def test_prune_path_traversal_raises(tmp_path, monkeypatch):
52
+ """A traversal attempt MUST raise, never delete outside the root."""
53
+ from backend.config import settings
54
+ from backend import uploaded_docs as u
55
+ monkeypatch.setattr(settings, "UPLOADED_DOCS_DIR", tmp_path)
56
+ outside = tmp_path.parent / "DO_NOT_DELETE"
57
+ outside.mkdir(exist_ok=True)
58
+ (outside / "keep.txt").write_text("safe")
59
+ # _doc_dir sanitises slashes/dots, so the dir resolves INSIDE root and
60
+ # is simply "not present" β†’ skipped; the outside dir is untouched.
61
+ r = u.prune_persisted_upload("../../DO_NOT_DELETE")
62
+ assert r["removed"] == []
63
+ assert outside.exists() and (outside / "keep.txt").exists()
64
+
65
+
66
+ def test_empty_prefix_rejected(tmp_path, monkeypatch):
67
+ from backend.config import settings
68
+ from backend import uploaded_docs as u
69
+ monkeypatch.setattr(settings, "UPLOADED_DOCS_DIR", tmp_path)
70
+ with pytest.raises(RuntimeError):
71
+ u.prune_persisted_upload(prefix="///")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ raise SystemExit(pytest.main([__file__, "-v"]))