-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathfirestore.rules
More file actions
121 lines (110 loc) · 5.74 KB
/
Copy pathfirestore.rules
File metadata and controls
121 lines (110 loc) · 5.74 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
117
118
119
120
121
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// The signal's reporter (owner), compared by DocumentReference path.
// Used by both the prod and test signal update/delete rules so the
// security-critical ownership check lives in exactly one place.
function isSignalReporter() {
return resource.data.reporter
== /databases/$(database)/documents/users/$(request.auth.uid);
}
// Non-reporters may ONLY advance a signal's status (the volunteer flow),
// and must self-stamp lastUpdatedBy so it can't be spoofed to another user.
function isStatusOnlyUpdate() {
return request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['status', 'lastUpdatedBy'])
&& request.resource.data.lastUpdatedBy
== /databases/$(database)/documents/users/$(request.auth.uid)
&& request.resource.data.status is int
&& request.resource.data.status >= 0
&& request.resource.data.status <= 2;
}
// Users collection - private profile (tokens, location, prefs, phone).
// Owner-only: never expose to other users.
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// User live location - kept separate from the user doc so high-frequency
// location writes don't trigger the token-dedupe Cloud Function. Owner-only;
// the notification fan-out reads it via the Admin SDK (bypasses rules).
match /userLocations/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Public profiles - just the display name, readable by any signed-in user
// (incl. anonymous) so reporter/comment-author names resolve for everyone.
// Writable only by the owner. On account deletion this is overwritten with
// "Deleted user" so erasure propagates to all signals/comments dynamically.
match /publicProfiles/{userId} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == userId;
}
// Signals collection - public read, authenticated write
match /signals/{signalId} {
allow read: if true; // Public read - signals are public data
allow create: if request.auth != null;
// Reporter may edit any field; anyone else signed in may only advance the
// status (self-stamping lastUpdatedBy). Prevents non-reporters from
// rewriting reporter/title/phone/photos or taking over a signal.
allow update: if request.auth != null
&& (isSignalReporter() || isStatusOnlyUpdate());
// Only the signal's reporter may delete it
allow delete: if request.auth != null && isSignalReporter();
// Comments subcollection
match /comments/{commentId} {
allow read: if true; // Public read
allow create: if request.auth != null;
// The signal's reporter may delete comments (enables delete-signal cascade)
allow delete: if request.auth != null
&& get(/databases/$(database)/documents/signals/$(signalId)).data.reporter
== /databases/$(database)/documents/users/$(request.auth.uid);
}
}
// Test signals collection - same rules as signals
match /signals_test/{signalId} {
allow read: if true;
allow create: if request.auth != null;
// Same rules as prod signals (see helpers above).
allow update: if request.auth != null
&& (isSignalReporter() || isStatusOnlyUpdate());
allow delete: if request.auth != null && isSignalReporter();
match /comments/{commentId} {
allow read: if true;
allow create: if request.auth != null;
allow delete: if request.auth != null
&& get(/databases/$(database)/documents/signals_test/$(signalId)).data.reporter
== /databases/$(database)/documents/users/$(request.auth.uid);
}
}
// Allow collection group queries for comments
match /{path=**}/comments/{commentId} {
allow read: if request.auth != null;
}
// Feedback collection - only admins can read (via Admin SDK/Console).
// Any signed-in caller (incl. the automatic anonymous app sessions) may
// submit, but (M-2):
// - auth is required — no unauthenticated writes; combined with App Check
// enforcement this closes the open email/cost-abuse vector.
// - userId is pinned to the caller so it can't be spoofed to frame another
// user (the email/rate-limit both key off it).
// - email, when present, must be a syntactically valid address (also
// re-validated in the function before it's used as replyTo).
// Each accepted write triggers onFeedbackCreated, which sends an email and
// enforces a per-user rate limit.
match /feedback/{feedbackId} {
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.message is string
&& request.resource.data.message.size() > 0
&& request.resource.data.message.size() <= 1000
&& request.resource.data.type in ['general', 'bug', 'feature', 'other']
&& (
!('email' in request.resource.data)
|| request.resource.data.email == null
|| (request.resource.data.email is string
&& request.resource.data.email.size() <= 254
&& request.resource.data.email.matches('^[^@ ]+@[^@ ]+[.][^@ ]+$'))
);
allow read, update, delete: if false; // Only accessible via Admin SDK/Console
}
}
}