731857231f
- SUMMARY.md: documents all four endpoints, security invariants, and verification results - STATE.md: advances to 4/9 plans; adds decisions for IDOR 404, route ordering, quota isolation
119 lines
4.8 KiB
Markdown
119 lines
4.8 KiB
Markdown
---
|
|
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)
|