diff --git a/.planning/STATE.md b/.planning/STATE.md index 8d48926..a72c132 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -27,13 +27,13 @@ progress: | 1 | Infrastructure Foundation | ✓ Complete | | 2 | Users & Authentication | ✓ Complete (5/5 plans) | | 3 | Document Migration & Multi-User Isolation | ✓ Complete (5/5 plans, 10/10 UAT, security gate passed) | -| 4 | Folders, Sharing, Quotas & Document UX | In Progress (3/9 plans complete) | +| 4 | Folders, Sharing, Quotas & Document UX | In Progress (4/9 plans complete) | | 5 | Cloud Storage Backends | Not Started | ## Current Position **Phase:** 04-folders-sharing-quotas-document-ux — In progress -**Plan:** 3/9 — Wave 0 scaffolds (04-01), migration 0004 + put_object_raw (04-02), Folders API + audit helper (04-03) +**Plan:** 4/9 — Wave 0 scaffolds (04-01), migration 0004 + put_object_raw (04-02), Folders API + audit helper (04-03), Sharing API (04-04) **Progress:** ██████░░░░ 60% (3/5 phases complete) ## Performance Metrics @@ -112,6 +112,9 @@ progress: | Breadcrumb uses iterative Python parent-walk | Not WITH RECURSIVE — ensures SQLite unit tests pass; cycle guard (visited set) prevents infinite loop on malformed data | | document_move_router is a separate APIRouter | PATCH /api/documents/{id}/folder placed in folders.py not documents.py; separate router with /api/documents prefix avoids circular import | | FTS plainto_tsquery wrapped in try/except | SQLite silently degrades to unfiltered results when plainto_tsquery unavailable; PostgreSQL works fully — no unit test breakage | +| Share IDOR: DELETE returns 404 not 403 | Prevents share ID enumeration; attacker cannot learn which share IDs exist for other users (T-04-04-02) | +| /received before /{share_id} in router | Path parameter conflict: FastAPI routes /received as /{share_id}="received" if DELETE is defined first — ordering enforced by comment | +| No quota touch in shares.py | Recipient's quota is never modified by share operations (T-04-04-04); sharing is metadata-only from quota's perspective | ### Open Questions @@ -151,6 +154,7 @@ _Updated at each phase transition._ | Last session | 2026-05-25 — Plan 04-01 executed: 30 Wave 0 xfail stubs across 5 test files; 39 xfailed total, zero new failures | | Last session | 2026-05-25 — Plan 04-02 executed: migration 0004 (pdf_open_mode, GIN FTS index, audit-logs bucket) + MinIOBackend.put_object_raw(); 122 tests pass | | Last session | 2026-05-25 — Plan 04-03 executed: write_audit_log() helper (flush-not-commit, never-raises) + FOLD-01..05 folder API + document sort/FTS/move; 122 pass, 0 new failures | -| Next action | Continue Wave 2 execution: run plan 04-04 (shares API) | +| Last session | 2026-05-25 — Plan 04-04 executed: Sharing API (SHARE-01..05) — grant/list/received/revoke with IDOR protection; 7 xfailed, zero new failures | +| Next action | Continue Wave 3 execution: run plan 04-05 (quota enforcement) | | Pending decisions | None | -| Resume file | `.planning/phases/04-folders-sharing-quotas-document-ux/04-03-SUMMARY.md` | +| Resume file | `.planning/phases/04-folders-sharing-quotas-document-ux/04-04-SUMMARY.md` | diff --git a/.planning/phases/04-folders-sharing-quotas-document-ux/04-04-SUMMARY.md b/.planning/phases/04-folders-sharing-quotas-document-ux/04-04-SUMMARY.md new file mode 100644 index 0000000..6ab11b6 --- /dev/null +++ b/.planning/phases/04-folders-sharing-quotas-document-ux/04-04-SUMMARY.md @@ -0,0 +1,118 @@ +--- +phase: 04-folders-sharing-quotas-document-ux +plan: "04" +subsystem: sharing-api +tags: [sharing, idor, audit, fastapi, sqlalchemy] +dependency_graph: + requires: + - "04-01" # Folder API (models present) + - "04-02" # Document-move API + - "04-03" # Audit service (write_audit_log) + provides: + - sharing-api-endpoints + affects: + - backend/main.py +tech_stack: + added: [] + patterns: + - FastAPI router with prefix /api/shares + - SQLAlchemy async join (Share + User for handle lookup) + - IntegrityError → HTTP 409 (duplicate share) + - IDOR protection via owner assertion → 404 (not 403) +key_files: + created: + - backend/api/shares.py + modified: + - backend/main.py +decisions: + - "GET /received defined before DELETE /{share_id} to prevent FastAPI path parameter conflict" + - "DELETE wrong-owner returns 404 not 403 (T-04-04-02 — prevents share ID enumeration)" + - "write_audit_log uses session.flush within caller transaction (D-14 pattern)" + - "No quota table touched in shares.py — recipient quota isolation (T-04-04-04)" +metrics: + duration_seconds: 145 + completed_date: "2026-05-25" + tasks_total: 1 + tasks_completed: 1 + files_created: 1 + files_modified: 1 +--- + +# Phase 04 Plan 04: Sharing API Summary + +## One-liner + +JWT-authenticated document sharing API with IDOR-safe revoke (404 on wrong-owner), handle-based recipient lookup, and metadata-only "received" virtual folder. + +## What was built + +Created `backend/api/shares.py` implementing four endpoints: + +1. **POST /api/shares** — grant share by recipient handle; 400 self-share, 404 bad UUID/unknown doc/unknown user, 409 duplicate (UniqueConstraint → IntegrityError) +2. **GET /api/shares?document_id=** — list shares owned by current user for a document, with recipient handle via JOIN +3. **GET /api/shares/received** — virtual "Shared with me" folder; returns metadata only (id, filename, content_type, size_bytes, created_at, owner_handle) — never extracted_text +4. **DELETE /api/shares/{share_id}** — revoke with IDOR protection: `share.owner_id != current_user.id → 404 "Share not found"` + +Registered in `backend/main.py` as `shares_router` under the Phase 4 routers section. + +## Commits + +| Hash | Description | +|------|-------------| +| 964128e | feat(phase-4): Sharing API (SHARE-01..05) — grant by handle, received folder, IDOR-safe revoke | + +## Task Results + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Create backend/api/shares.py — full sharing API | 964128e | backend/api/shares.py (created), backend/main.py (modified) | + +## Verification Results + +``` +tests/test_shares.py — 7 xfailed (all stubs — implementations in 04-05) +Full suite: 1 failed (pre-existing test_extract_docx ModuleNotFoundError), 122 passed, 7 skipped, 39 xfailed +``` + +The pre-existing failure (`test_extract_docx — ModuleNotFoundError: No module named 'docx'`) is completely unrelated to this plan (missing python-docx package in the local environment). It was already failing before this plan. + +## Security Invariants Verified + +| Threat | Mitigation | Verified | +|--------|-----------|---------| +| T-04-04-02: Share IDOR on DELETE | `share.owner_id != current_user.id → 404` | grep line 246 | +| T-04-04-03: extracted_text leak | Not included in received response | grep: absent from return dict | +| T-04-04-04: Quota modification | No quotas table touched in shares.py | grep: no `quotas` reference | +| T-04-04-05: Duplicate share DoS | IntegrityError → 409 on UniqueConstraint | line 103-106 | +| T-04-04-06: Doc existence leak | Ownership assertion → 404 (not 403) | line 99-100 | + +## Acceptance Criteria + +- [x] backend/api/shares.py exists with all four endpoint functions +- [x] GET /api/shares/received defined before DELETE /{share_id} (line 187 vs 228) +- [x] DELETE checks `share.owner_id != current_user.id` → 404 +- [x] IntegrityError → 409 for duplicate share +- [x] write_audit_log called for share.granted and share.revoked (lines 110, 255) +- [x] GET /received does NOT include extracted_text +- [x] `python3 -c "from api.shares import router"` exits 0 +- [x] test_share_revoke_wrong_owner_404 is xfail (not FAILED) +- [x] Full suite has zero FAILED from this plan's changes + +## Deviations from Plan + +None — plan executed exactly as written. Route ordering, IDOR pattern, audit log calls, and quota isolation all implemented per specification. + +## Known Stubs + +None — all four endpoints are fully implemented. Test stubs in test_shares.py are intentional xfail stubs that will be implemented in plan 04-05 (per the test file docstring). + +## Threat Flags + +None — all trust boundaries and STRIDE threats from the plan's threat model are mitigated in the implementation. + +## Self-Check: PASSED + +- [x] backend/api/shares.py exists +- [x] backend/main.py includes shares_router +- [x] Commit 964128e exists in git log +- [x] All 7 share tests are xfail (zero FAILED)