Repository navigation
Expand file tree
/
Copy pathstorage.rules
More file actions
116 lines (105 loc) · 5.05 KB
/
Copy pathstorage.rules
File metadata and controls
116 lines (105 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Signed-in user whose email address is verified. Open email/password
// sign-up means request.auth alone proves nothing.
function isVerifiedUser() {
return request.auth != null
&& request.auth.token.email_verified == true;
}
// Canonical staff identity: a verified user carrying the admin
// custom claim set by the session route (Postgres admin_users is the
// authority). No Firestore lookup — the admins/ collection is
// retired and must never be an authorization input again.
function isAdmin() {
return isVerifiedUser()
&& request.auth.token.admin == true;
}
// Uploads are image assets only (site/team photos), max 5 MB.
// request.resource is null on delete, so deletes only need isAdmin().
function isImageUpload() {
return request.resource.size < 5 * 1024 * 1024
&& request.resource.contentType.matches("image/.*");
}
function canWrite() {
return isAdmin()
&& (request.resource == null || isImageUpload());
}
// Clinical-document uploads from the admin medical record: image or
// PDF, max 5 MB (mirrors isVetDocumentFile in src/lib/medical.ts).
function isVetDocUpload() {
return request.resource.size < 5 * 1024 * 1024
&& request.resource.size > 0
&& (request.resource.contentType.matches("image/.*")
|| request.resource.contentType == "application/pdf");
}
// There is deliberately no images/ rule: no active workflow owns
// the namespace (nothing in the app uploads to or reads from it,
// and the production prefix is empty), so it falls through to
// default-deny for everyone — same posture as animals/ below.
// A future media feature must introduce explicit authorization
// for its own prefix rather than reviving a generic catch-all.
// Animal photos are plain URLs stored on the registry row —
// nothing uploads to an animals/ Storage prefix, so there is
// deliberately no animals/ rule: it falls through to default-deny.
// A future upload feature must add lifecycle-aware authorization
// (a photo for a pending/adopted/malformed animal must not become
// publicly readable just because it landed under animals/).
// Team photos - public read, admin-only image uploads
match /team-photos/{allPaths=**} {
allow read: if true;
allow write: if canWrite();
}
// Payment receipts attached to registration submissions - private
// data stored at receipts/<submission uuid>. The submission id is
// generated client-side so the object path is bound to the Postgres
// registration_submissions row.
//
// Client SDK access is fully denied (#219). The public form writes
// through a short-lived signed URL minted by the submission server
// action after validation + rate limiting (GCS-level authorization,
// rules-bypassing), or through the Admin SDK server-save path on
// emulators — arbitrary unauthenticated clients can no longer create
// receipt objects directly. Size/type are verified server-side at
// finalize time before the row stores the path.
//
// Staff view receipts through short-lived signed read URLs minted by
// a server action, and orphan cleanup runs through the Admin SDK in
// the scheduled sweep — both bypass rules entirely. A receipt object
// is therefore unreachable via the client SDK for every principal,
// and retired Firestore collections are never consulted for
// authorization.
match /receipts/{receiptId} {
allow read, write: if false;
}
// Clinical documents attached to the veterinary record (#192) —
// private data stored at vet-docs/<uuid>[.<ext>]; the vet_documents
// row is written by a server action after the upload lands.
//
// Create is staff-only: a verified user carrying the admin custom
// claim. The resource == null check keeps even staff from
// overwriting an existing object (paths carry a fresh uuid anyway).
//
// There is no client-side read/update/delete at all — not even for
// admins. Staff view documents through the /admin/documents/[id]
// proxy route and orphan cleanup runs through the Admin SDK in the
// scheduled sweep — both bypass rules entirely. A vet document is
// therefore unreachable via the client SDK for every principal.
match /vet-docs/{docId} {
allow create: if resource == null && isAdmin() && isVetDocUpload();
allow read, update, delete: if false;
}
// Database backups (#180) — pg_dump output uploaded by the Admin
// SDK from the scheduled GitHub Action / operator machine, which
// bypasses rules entirely. No client principal may ever read,
// write, or delete a dump: they contain the full registry
// including owner PII and payment history.
match /db-backups/{allPaths=**} {
allow read, write: if false;
}
// Default deny
match /{allPaths=**} {
allow read, write: if false;
}
}
}