-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
188 lines (169 loc) · 10.4 KB
/
Copy pathfirestore.rules
File metadata and controls
188 lines (169 loc) · 10.4 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ── postbox collection ──────────────────────────────────────────────────
// Server-only (Cloud Functions admin SDK) in BOTH directions. Postbox docs
// carry exact geopoints; the game's fuzzy-compass privacy model promises
// clients only vague directions, and every read path goes through the
// callables (nearbyPostboxes/startScoring/routePostboxes strip coordinates
// via applyUserClaims). No Dart or Kotlin code reads this collection
// directly, so the old `read: if request.auth != null` only offered a
// scrape-the-whole-map hole. Closed in v1.5 as an offline-play
// prerequisite (ROADMAP v1.5).
match /postbox/{postboxId} {
allow read, write: if false;
}
// ── claims collection ───────────────────────────────────────────────────
// Claims are written exclusively by Cloud Functions (admin SDK).
// Authenticated users may read their own claims only.
match /claims/{claimId} {
allow read: if request.auth != null && resource.data.userid == request.auth.uid;
allow write: if false;
}
// ── users collection ────────────────────────────────────────────────────
// Profile fields (displayName, streak, lastClaimDate, createdAt) are
// written exclusively by Cloud Functions (Admin SDK) — this prevents
// clients from bypassing the profanity filter by writing displayName
// directly. The friends array is the only field clients may modify.
match /users/{uid} {
allow read: if request.auth != null;
// The friends array, notificationPrefs map, and mapColor string are the
// only fields clients may modify on their own document. All other fields
// (displayName, streak, lastClaimDate, createdAt, points) are written by
// Cloud Functions using the Admin SDK, which bypasses these rules. FCM
// tokens live in the separate fcmTokens/{uid} collection to avoid
// exposure here.
// friends is capped at 200 entries to prevent O(n) leaderboard read abuse.
// notificationPrefs must be a map (type-validated to prevent garbage writes).
// mapColor must be a short string (palette key) when present; deletion
// (FieldValue.delete()) is also allowed since the diff only fires on
// present fields.
allow update: if request.auth != null && request.auth.uid == uid
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['friends', 'notificationPrefs', 'mapColor'])
&& (!request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['friends'])
|| (request.resource.data.friends is list
&& request.resource.data.friends.size() <= 200))
&& (!request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['notificationPrefs'])
|| request.resource.data.notificationPrefs is map)
&& (!request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['mapColor'])
|| !('mapColor' in request.resource.data)
|| (request.resource.data.mapColor is string
&& request.resource.data.mapColor.size() <= 32));
// Create and all other writes are handled by Cloud Functions.
allow create: if false;
}
// Per-county lifetime stats kept under users/{uid}/countyStats/{slug}.
// Readable by any signed-in user (matches the parent users/{uid} rule
// used by friend-profile views); written only by Cloud Functions.
match /users/{uid}/countyStats/{countySlug} {
allow read: if request.auth != null;
allow write: if false;
}
// ── leaderboards collection ─────────────────────────────────────────────
// Leaderboard documents are written by Cloud Functions only.
// Authenticated users may read them.
match /leaderboards/{period} {
allow read: if request.auth != null;
allow write: if false;
}
// Per-county lifetime leaderboards live under
// leaderboards/lifetime_by_county/counties/{countySlug}. Same access
// pattern as the period docs above.
match /leaderboards/lifetime_by_county/counties/{countySlug} {
allow read: if request.auth != null;
allow write: if false;
}
// ── meta collection ──────────────────────────────────────────────────────
// Read-only aggregate stats (e.g. meta/stats.totalPostboxes) used to
// compute lifetime leaderboard percentages. Written by import scripts only.
match /meta/{docId} {
allow read: if request.auth != null;
allow write: if false;
}
// ── reports collection ───────────────────────────────────────────────────
// User-submitted reports about postbox data problems. Created and reviewed
// exclusively by Cloud Functions (submitReport / reviewReport, Admin SDK).
// A reporter may read their own reports; users with the `admin` custom
// claim may read all of them (the in-app admin review interface).
match /reports/{reportId} {
allow read: if request.auth != null
&& (resource.data.reporterUid == request.auth.uid
|| request.auth.token.admin == true);
allow write: if false;
}
// ── reportQuotas collection ──────────────────────────────────────────────
// Per-user daily counter enforcing the submitReport rate limit. Written
// (and only ever read) by the submitReport Cloud Function (Admin SDK);
// clients have no business reading or writing it.
match /reportQuotas/{uid} {
allow read, write: if false;
}
// ── offlineFlushQuotas collection ────────────────────────────────────────
// Per-user daily counter enforcing the flushOfflineClaims cap (v1.5), the
// exact analogue of reportQuotas above. Read and written only by that
// callable (Admin SDK). A client that could read it would learn its
// remaining budget; one that could write it would have none.
match /offlineFlushQuotas/{uid} {
allow read, write: if false;
}
// ── retention collection ─────────────────────────────────────────────────
// Resume state for the scheduled data-retention sweep (its paging cursor /
// watermark, so a crashed run continues rather than restarting). Purely
// server-side bookkeeping — Cloud Functions (Admin SDK) only.
match /retention/{docId} {
allow read, write: if false;
}
// ── fcmTokens collection ─────────────────────────────────────────────────
// Stores FCM device tokens separate from the world-readable users collection
// so that tokens are not exposed to other authenticated users. Written
// exclusively by the registerFcmToken Cloud Function (Admin SDK).
// Users may only read their own token document.
match /fcmTokens/{uid} {
allow read: if request.auth != null && request.auth.uid == uid;
allow write: if false;
}
// ── moderationFlags collection ───────────────────────────────────────────
// Shadow-mode abuse-detection flags, written exclusively by the
// onClaimCreated Cloud Function (Admin SDK) and updated by reviewFlag.
// Readable only by users with the `admin` custom claim (the in-app abuse
// review interface). No client writes.
match /moderationFlags/{flagId} {
allow read: if request.auth != null && request.auth.token.admin == true;
allow write: if false;
}
// ── trustScores collection ───────────────────────────────────────────────
// Per-user server-only trust score decayed by the abuse detector. Never
// client-readable (kept out of the world-readable users/{uid} doc so it
// can't leak) and never client-writable — Cloud Functions (Admin SDK) only.
match /trustScores/{uid} {
allow read, write: if false;
}
// ── deletions collection ─────────────────────────────────────────────────
// Per-London-day account-deletion audit counter, written by the
// onUserDeleted Cloud Function (Admin SDK). Server-only; clients have no
// business reading or writing it.
match /deletions/{docId} {
allow read, write: if false;
}
// ── attempts collection ──────────────────────────────────────────────────
// Idempotent claim-attempt markers (v1.5 offline play): the server stores
// each startScoring/flushOfflineClaims response under a client-generated
// attempt id so a dropped response can be replayed on retry. Server-only —
// replay happens inside the callables; results echo claim data clients
// must never read directly. Reaped by Firestore TTL on `expiresAt`.
match /attempts/{attemptId} {
allow read, write: if false;
}
// ── notificationState collection ─────────────────────────────────────────
// Server-only dispatch bookkeeping for scheduled notifications (e.g. the
// once-per-day streak reminder's random target slot / sent marker). Written
// exclusively by Cloud Functions (Admin SDK); clients never touch it.
match /notificationState/{docId} {
allow read, write: if false;
}
}
}