-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathstorage.rules
More file actions
99 lines (85 loc) · 4.33 KB
/
Copy pathstorage.rules
File metadata and controls
99 lines (85 loc) · 4.33 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
rules_version = '2';
// Firebase Storage Security Rules
//
// Covered by emulator tests in `firestore-tests/storage.rules.test.js`. Run
// `npm test` there before every `firebase deploy --only storage`:
// help-a-paw-dev is production, so a rules mistake is a live outage.
service firebase.storage {
match /b/{bucket}/o {
// ---------------------------------------------------------------- helpers
/// Whether [userId] is the reporter of [signalId].
///
/// The Storage path carries no collection name, so photos for a signal
/// created in *test mode* land here too while its document lives in
/// `signals_test`. Checking only `signals` made every test-mode upload
/// fail — and fail hard: dereferencing `.data` on a missing document
/// raises a Null value error rather than evaluating to false.
function isSignalReporter(signalId, userId) {
return isReporterOf('signals', signalId, userId)
|| isReporterOf('signals_test', signalId, userId);
}
/// One cross-service read, null-safe. `||` above short-circuits, so a live
/// signal costs a single document read and only a test-mode signal pays
/// for the second.
function isReporterOf(collection, signalId, userId) {
let signal = firestore.get(/databases/(default)/documents/$(collection)/$(signalId));
return signal != null
&& signal.data.reporter == /databases/(default)/documents/users/$(userId);
}
/// Bounds an incoming upload. Deliberately not applied to deletes, where
/// `request.resource` is null.
///
/// The client sets `contentType` explicitly on every upload, so this is an
/// assertion about our own writes rather than a guess about the platform's
/// MIME inference.
function isImageWithin(maxBytes) {
return request.resource != null
&& request.resource.size <= maxBytes
&& request.resource.contentType is string
&& request.resource.contentType.matches('image/.*');
}
// --------------------------------------------------------- signal photos
match /signals/{signalId}/photos/{fileName} {
// Signals are public data, and so are their photos.
allow read: if true;
// 5 MB: the app re-encodes to JPEG at 1920px/q85, which lands well under
// 2 MB in practice, so this bounds abuse without rejecting real photos.
allow create, update: if request.auth != null
&& isSignalReporter(signalId, request.auth.uid)
&& isImageWithin(5 * 1024 * 1024);
allow delete: if request.auth != null
&& isSignalReporter(signalId, request.auth.uid);
}
// Future: comment photos. No write rule on purpose — the feature does not
// exist yet, and default-deny is the right placeholder.
match /signals/{signalId}/comments/{commentId}/photos/{fileName} {
allow read: if true;
}
// -------------------------------------------------------- profile photos
/// Avatars, written as `profile_photos/{uid}.jpg`.
///
/// This block was previously absent altogether, so every avatar upload was
/// denied by default-deny and the user got an opaque error.
///
/// Public read: an avatar is meant to be seen by other people, the same as
/// a display name. Nothing renders another user's avatar *yet* — reporter
/// and comment names resolve through `publicProfiles`, which carries no
/// photo — but the rule should not be what blocks that when it lands.
/// The exposure already matches `publicProfiles`: uids are visible to
/// anyone reading a signal (`reporter` is a document reference, and signals
/// are world-readable), so a name is discoverable per-uid today and an
/// avatar now is too.
///
/// Account deletion removes these through the Admin SDK, which also
/// bypasses rules, so no cross-user delete permission is needed here.
match /profile_photos/{fileName} {
allow read: if true;
// 2 MB: the picker caps avatars at 512px/q80, i.e. tens of KB.
allow create, update: if request.auth != null
&& fileName == request.auth.uid + '.jpg'
&& isImageWithin(2 * 1024 * 1024);
allow delete: if request.auth != null
&& fileName == request.auth.uid + '.jpg';
}
}
}