-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
142 lines (126 loc) · 6.4 KB
/
firestore.rules
File metadata and controls
142 lines (126 loc) · 6.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
// Firestore Security Rules for TextAgent Share
// Deploy with: firebase deploy --only firestore:rules
// (requires firebase-tools: npm install -g firebase-tools)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper: validate the optional 'view' field (must be 'ppt' or 'preview')
function validView() {
return !('view' in request.resource.data)
|| request.resource.data.view == 'ppt'
|| request.resource.data.view == 'preview';
}
match /shares/{docId} {
// Anyone can read shared documents (needed for shared link access)
allow read: if true;
// Allow create with strict validation:
// - Quick share: { d, t, wt [, view] }
// - Compact share: { d, k, t, wt [, view] }
// - Secure share: { d, t, salt, secure, wt [, view] }
// - 'wt' is the write-token for ownership verification
// - 'view' is optional view-lock ('ppt' | 'preview')
// - 'ekHash' is optional SHA-256 hash of the edit key
// - 'eWt' is optional write-token encrypted with the edit key
allow create: if (
// Quick share / compact share / cloud auto-save
(request.resource.data.keys().hasOnly(['d', 't', 'wt', 'view', 'k', 'rkHash', 'ekHash', 'eWt'])
&& request.resource.data.d is string
&& request.resource.data.d.size() < 1048576
&& request.resource.data.t is int
&& request.resource.data.wt is string
&& request.resource.data.wt.size() >= 16
&& request.resource.data.wt.size() <= 64
&& validView())
||
// Secure share
(request.resource.data.keys().hasOnly(['d', 't', 'salt', 'secure', 'wt', 'view', 'rkHash', 'ekHash', 'eWt'])
&& request.resource.data.d is string
&& request.resource.data.d.size() < 1048576
&& request.resource.data.t is int
&& request.resource.data.salt is string
&& request.resource.data.secure == true
&& request.resource.data.wt is string
&& request.resource.data.wt.size() >= 16
&& request.resource.data.wt.size() <= 64
&& validView())
);
// Allow update only if write-token matches (ownership proof)
// Backward compat: old docs without 'wt' can still be updated
allow update: if request.resource.data.keys().hasOnly(['d', 't', 'wt', 'view', 'k', 'rkHash', 'ekHash', 'eWt', 'salt', 'secure'])
&& request.resource.data.d is string
&& request.resource.data.d.size() < 1048576
&& request.resource.data.t is int
&& request.resource.data.wt is string
&& validView()
&& (
// Doc has no write-token (legacy) — allow update
!('wt' in resource.data)
||
// Write-token matches — owner can update
request.resource.data.wt == resource.data.wt
);
// Never allow deletes via client
allow delete: if false;
// Form responses subcollection
match /responses/{respId} {
// Anyone with the form link can read responses
allow read: if true;
// Anyone can submit a response (write-once, no updates)
allow create: if request.resource.data.keys().hasOnly(['d', 't'])
&& request.resource.data.d is string
&& request.resource.data.d.size() < 65536
&& request.resource.data.t is int;
// No updates to responses (immutable)
allow update: if false;
// Only form owner can delete responses (write-token check)
allow delete: if get(/databases/$(database)/documents/shares/$(docId)).data.wt
== request.auth.token.wt;
}
}
// ─── Spaces (personal document hubs) ───────────────
match /spaces/{spaceId} {
allow read: if true;
allow create: if request.resource.data.keys().hasOnly(['name', 'items', 'wt', 'eh', 't', 'description', 'owner', 'theme'])
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.name.size() <= 100
&& request.resource.data.items is list
&& request.resource.data.items.size() <= 50
&& request.resource.data.wt is string
&& request.resource.data.wt.size() >= 16
&& request.resource.data.wt.size() <= 64
&& request.resource.data.t is int;
allow update: if request.resource.data.keys().hasOnly(['name', 'items', 'wt', 'eh', 't', 'description', 'owner', 'theme'])
&& request.resource.data.name is string
&& request.resource.data.items is list
&& request.resource.data.items.size() <= 50
&& request.resource.data.wt is string
&& request.resource.data.t is int
&& (
!('wt' in resource.data)
|| request.resource.data.wt == resource.data.wt
);
allow delete: if false;
}
// ─── Link Analytics (global click counters & live presence) ──────
match /link_clicks/{docId} {
// Anyone can read click counts (public analytics)
allow read: if true;
// Allow write: click increment or presence parent doc creation
// Fields: { url, clicks, lastClicked } — clicks must be a number
allow write: if true;
// Presence subcollection: each session writes its own heartbeat doc
match /readers/{sessionId} {
allow read: if true;
// Each session can write only { lastSeen: number }
allow write: if request.resource.data.keys().hasOnly(['lastSeen'])
&& request.resource.data.lastSeen is int;
allow delete: if true;
}
}
// Deny access to all other collections by default
match /{document=**} {
allow read, write: if false;
}
}
}