feat(backend): optional per-user multitenancy (tenant-scoped storage + data control) - #36
Merged
Merged
Conversation
…+ data control) A signed-in caller's uploads/reels are isolated to their own tenants/<uid>/ storage prefix, enforced by construction via a prefix-filtering storage wrapper rather than a per-lookup check. Guest behavior (no Authorization header, or FIREBASE_PROJECT_ID unset) is unchanged. - get_tenant: FastAPI dependency resolving a verified Firebase ID token to a tenant id, or None for guest; a present-but-invalid token is a hard 401, never a silent guest downgrade - _TenantScopedStorage: composes over any StorageBackend, prefixing every key and exposing an index filtered+stripped to the caller's own prefix, so a cross-tenant lookup 404s because the row is physically absent - every reel route, the async job store, and two new self-service routes (GET /me/library, DELETE /me/data) are wired through it - adds StorageBackend.delete to FakeStorage and B2Storage (additive), and fixes a delete/merge-on-write interaction in B2Storage's index persistence
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds optional per-user multitenancy to the backend. A signed-in caller's uploads and reels are isolated to their own tenant, and they can list and delete their own data. Guests (no login) behave exactly as before — this is the security-sensitive part, so it is reviewed before merge.
get_tenant: a FastAPI dependency that resolves a verified Firebase ID token (Authorization: Bearer <token>) to a tenant id, orNonefor guest. A present-but-invalid/expired/malformed token is a hard401— never a silent downgrade to guest.FIREBASE_PROJECT_IDunset means multitenancy is off entirely: every request is guest, even one carrying a header._TenantScopedStorage: a prefixing wrapper over the existingStorageBackend— every key is prefixed withtenants/<uid>/on write/read, andindexreturns only rows under that prefix (stripped). A cross-tenant or guest lookup 404s because the row is physically absent from the scanned index, not because a check rejected it.GET /me/library,DELETE /me/data) are wired through it.StorageBackend.deleteadded additively toFakeStorageandB2Storage; a delete/merge-on-write interaction bug inB2Storage's index persistence (an instance's own delete could resurrect its row via the existing union-merge) is fixed with aremoved=parameter on_persist_index.Guest behavior is provably unchanged: every route computes the tenant-scoped object as
_tenant_storage(tenant)/_tenant_pipeline(tenant), both of which return the exact pre-existing module-level_storage/_pipelineobjects whentenant is None— not new instances, the same objects, so every existing guest code path (including tests that monkeypatchapi._storage/api._pipelinedirectly) is byte-identical.Test plan
git diff --stat -- tests/shows zero existing test files touched; all 4 new test files are additive.ruff check src tests scripts— 0 issues (verified against ruff 0.16.0, matching CI's resolved version).pytest tests/unit— 157 passed (was ~148 pre-PR; +9 new:test_fake_storage_delete.py,test_b2_storage_delete.py).pytest tests/integration— 98 passed (+25 new:test_api_multitenancy.py).pytest tests/e2e— 59 passed, unaffected.pytest tests/security— 62 passed (+18 new:test_multitenancy_isolation.py).--cov-fail-under=90, matching CI's 3-stage accumulation) — 97.08% (up from ~97%, comfortably above the 90% floor;api.pyitself at 98%).python scripts/readiness.py --min 95— 100.0% automatable, unaffected.DELETE /me/datacannot reach outside its own prefix (tenant B + guest reels survive), forged/garbage/expired/wrong-key tokens → 401 on all 10 wired routes (never guest), tenant cannot be spoofed via query param /X-Tenantheader / body field,FIREBASE_PROJECT_IDunset treats a valid bearer header as guest (the additive-default proof).