/
firestore.rules
235 lines (200 loc) · 8.02 KB
/
firestore.rules
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
// Easy Chat Security Rules
//
// TODO: User blocking for each chat room.
// TODO: User blocking for the whole app. Use /easy-setting/user/block/{uid} to store the blocked users.
//
// Admin UID is hard coded. This is to reduce the pricing of Firestore document read.
// It is a good idea to check the admin with custom claims.
//
// Reading a document is required to see if the user is blocked unless it is done with custom claims.
// Reading a chat room (settings) document is required when creating a new chat message.
rules_version = '2';
// Return true if the use is root user.
function isAdmin() {
let adminUIDs = ['root', 'admin'];
return request.auth.uid in adminUIDs || request.auth.token.admin == true;
}
service cloud.firestore {
match /databases/{database}/documents {
function prefix() {
return /databases/$(database)/documents;
}
match /{document=**} {
allow read, write: if false;
}
match /easy-settings/{documentId} {
allow read: if true;
allow write: if isAdmin();
}
// Master can change all the fields.
// Moderator can only change some of the fields.
// Chat members can only add or delete their own uid into `users` field.
match /easychat/{roomId} {
function willBeGroupChat() {
return request.resource.data.group == true;
}
function willBeSingleChat() {
return request.resource.data.group == false;
}
// Return true if master field is not updated.
function notUpdatingMaster() {
return notUpdating(['master']);
}
// Return true
// - if the master is not leaving(removing) from the room.
// - if the user does not remove the master from the room.
function notRemovingMaster() {
return notRemoving('users', resource.data.master);
}
// Return true if the user is removing himself from the room.
function isLeaving() {
return onlyRemoving('users', request.auth.uid);
}
// Return true if the user is adding himself to the room.
function isJoining() {
return
onlyAddingOneElement('users')
&&
request.resource.data.users.toSet().difference(resource.data.users.toSet()) == [request.auth.uid].toSet();
}
//
function isMaster() {
return resource.data.master == request.auth.uid;
}
function isModerator() {
return 'moderators' in resource.data && resource.data.moderators.hasAny([request.auth.uid]);
}
function isRoomUser() {
return resource.data.users.hasAny([request.auth.uid]);
}
function isOpen() {
return 'open' in resource.data && resource.data.open;
}
function isGroupChat() {
return 'group' in resource.data && resource.data.group;
}
allow read: if isOpen() || isRoomUser();
allow create: if
required(['master', 'createdAt', 'group', 'open', 'users'])
&&
(
(willBeSingleChat() && userSize() == 2) // if it's a single chat, there must be 2 members of the room.
||
(willBeGroupChat() && userSize() == 1) // if it's a group, there must have only one member
)
&&
// The master uid must exists in the users field.
request.resource.data.users.hasAny([request.resource.data.master])
&&
// The master uid must be the creator.
request.resource.data.master == request.auth.uid;
allow update: if
( isMaster() && notRemovingMaster() )
||
( isModerator() && notUpdatingMaster() && notRemovingMaster() )
||
isLeaving()
||
( isRoomUser() && onlyUpdating(['lastMessage', 'noOfNewMessages']) )
||
( isOpen() && ((isRoomUser() && onlyAddingOneElement('users')) || isJoining()) )
;
allow delete: if false;
match /messages/{messageId} {
function isMyMessage() {
return 'uid' in request.resource.data && request.resource.data.uid == request.auth.uid;
}
function roomData() {
return get(/$(prefix())/easychat/$(roomId)).data;
}
function isRoomUser() {
return 'users' in roomData() && roomData().users.hasAny([request.auth.uid]);
}
// Check if the user is blocked for the chat room.
// User to user blocking must be implemented in the client side ( by hiding or replacing the message as blocked ).
function isBlocked() {
return 'blockedUsers' in roomData() && roomData().hasAny([request.auth.uid]);
}
allow read: if isRoomUser() && !isBlocked();
allow create: if isRoomUser() && !isBlocked();
allow update: if isMyMessage() && !isBlocked();
allow delete: if isMyMessage() && !isBlocked();
}
}
match /readonly/{documentId} {
allow read: if true;
allow write: if false;
}
match /users/{documentId} {
allow read, write: if true;
}
match /rule-test-onlyUpdating/{documentId} {
allow read: if true;
allow update: if onlyUpdating(['a', 'b']);
}
match /rule-test-notUpdating/{documentId} {
allow read: if true;
allow update: if notUpdating(['a', 'b']);
}
match /rule-test-onlyRemoving/{documentId} {
allow read: if true;
allow update: if onlyRemoving('users', 'b');
}
}
}
// * Warning : It's check the fields after save.
// * Warning : !!! It's not checking the incoming data fields !!!
function required(fields) {
return request.resource.data.keys().hasAll( fields );
}
// Returns the number of users in the room AFTER save. Count without uinique uids.
// ! After save
function userSize() {
return request.resource.data.users.size();
}
// Adding an element to the array field.
//
// This must add an elemnt only. Not replacing any other element. It does unique element check.
function onlyAddingOneElement(arrayField) {
return
resource.data[arrayField].toSet().intersection(request.resource.data[arrayField].toSet()) == resource.data[arrayField].toSet()
&&
request.resource.data[arrayField].toSet().difference(resource.data[arrayField].toSet()).size() == 1
;
}
// Returns true if the fields are updated in the document.
//
// For instance, the input fields are ['A', 'B'] and if the document is updated with ['A', 'C'], then it return true.
// For instance, the input fields are ['A', 'B'] and if the document is updated with ['C', 'D'], then it return false.
function onlyUpdating(fields) {
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields);
}
// 입력된 fields 중 하나라도 업데이트 되면 false. 즉, 업데이트 안되어야 성공.
// 예) 기존: {a: 1, b: 2, c: 3}, 업데이트: {a: 10, b: 20} 인 경우, a 와 b 필드가 업데이트된다.
// 이 때, 입력 필드 fields 가 ['a', 'c'] 인 경우,
// a 와 c 필드가 업데이트 되지 않기를 원하는데, 실제로는 b 가 업데이트 되었다. 그래서, false 리턴
function notUpdating(fields) {
return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields);
}
// Return true if the array field in the document is not removing the element.
//
// Usage: notRemoving('users', resource.data.master) - for blocking moderator to remove master.
function notRemoving(field, element) {
return request.resource.data[field].hasAny([element]);
}
// Return true if the array field in the document is removing only the the element. It must maintain other elements.
//
// arrayField is an array
// [element] is an element to be removed from the arrayField
function onlyRemoving(arryField, element) {
return
resource.data[arryField].toSet().difference(request.resource.data[arryField].toSet()) == [element].toSet()
&&
resource.data[arryField].toSet().intersection(request.resource.data[arryField].toSet()) == request.resource.data[arryField].toSet()
;
}
// If the user is disabled by the admin in Firebase Auth, they cannot login and they don't have request.auth.uid.
// So, no need to check if the user is disabled.
// function isDisabled() {
// return true;
// }