-
Notifications
You must be signed in to change notification settings - Fork 4
/
firestore.rules
448 lines (379 loc) · 19.4 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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
// NOTE: to deploy only these rules run
// `npm run deploy:firestore:rules`
// Debugging note: debug(value) will output to firestore-debug.log
// cf. https://firebase.google.com/docs/reference/rules/rules.debug
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// all documents are inaccessible unless allowed by rules below
match /{document=**} {
allow read, write: if false;
}
function stringExists(s) {
return (s != null) && (s != "");
}
function isAuthed() {
return request.auth != null;
}
// function hasClass() {
// return isAuthed() && request.auth.token.class_hash != null;
// }
// function hasClassHash(classHash) {
// return request.auth.token.class_hash == classHash;
// }
function hasRole(role) {
return isAuthed() && request.auth.token.user_type == role;
}
function hasUserId() {
return isAuthed() && request.auth.token.platform_user_id != null;
}
// function matchUserId(userId) {
// return hasUserId() && string(request.auth.token.platform_user_id) == userId;
// }
function matchFirebaseUserId(userId) {
return isAuthed() && request.auth.uid == userId;
}
function isAuthedTeacher() {
return hasUserId() && hasRole("teacher");
}
// uid of submitted document must match user's platform_user_id
function userIsRequestUser() {
return isAuthed() &&
string(request.auth.token.platform_user_id) == request.resource.data.uid;
}
// user's platform_user_id must be in submitted document's list of teachers
function userInRequestTeachers() {
return isAuthed() &&
'teachers' in request.resource.data &&
string(request.auth.token.platform_user_id) in request.resource.data.teachers;
}
// uid of requested document must match user's platform_user_id
function userIsResourceUser() {
return isAuthed() &&
string(request.auth.token.platform_user_id) == resource.data.uid;
}
// user's class_hash must be in submitted document's list of classes
function classInRequestClasses() {
return isAuthed() && request.auth.token.class_hash in request.resource.data.classes;
}
// user's class_hash must be submitted document's context_id
function classIsRequestContextId() {
return isAuthed() && request.auth.token.class_hash == request.resource.data.context_id;
}
// user's class_hash must be in requested document's list of classes
function classInResourceClasses() {
return isAuthed() && request.auth.token.class_hash in resource.data.classes;
}
function isValidCurriculumCreateRequest() {
return userIsRequestUser() &&
request.resource.data.keys().hasAll(["uid", "unit", "problem", "section", "path", "network"]);
}
function preservesReadOnlyDocumentFields() {
let readOnlyFieldsSet = ["uid", "type", "key", "createdAt", "context_id"].toSet();
let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys();
return !affectedFieldsSet.hasAny(readOnlyFieldsSet);
}
function isValidSupportCreateRequest() {
return userIsRequestUser() &&
classInRequestClasses() &&
classIsRequestContextId() &&
(request.resource.data.content != null) &&
(request.resource.data.type == "supportPublication");
}
function preservesReadOnlySupportFields() {
let readOnlyFieldsSet = ["context_id", "createdAt", "network", "originDoc", "platform_id",
"problem", "resource_link_id", "resource_url", "type", "uid"].toSet();
let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys();
return !affectedFieldsSet.hasAny(readOnlyFieldsSet);
}
function isValidSupportUpdateRequest() {
return userIsResourceUser() && preservesReadOnlySupportFields();
}
//
// portal-authenticated secure access rules
//
match /authed/{portal} {
allow read, write: if isAuthedTeacher();
// Check that the class given by the context_id exists and includes the logged-in teacher
function teacherIsInClass(contextid) {
let class_doc = get(/databases/$(database)/documents/authed/$(portal)/classes/$(contextid));
return class_doc != null && string(request.auth.token.platform_user_id) in class_doc.data.teachers;
}
// user's platform_user_id must be listed in the class of the requested document
// also allows access to legacy documents which contain their own list of teachers
function userInResourceTeachers() {
return isAuthed() &&
( teacherIsInClass(resource.data.context_id) ||
string(request.auth.token.platform_user_id) in resource.data.teachers);
}
function isValidDocumentCreateRequest() {
return
request.resource.data.keys().hasAll(["uid", "type", "key", "createdAt"]) &&
(classIsRequestContextId() || teacherIsInClass(request.resource.data.context_id));
}
function isValidDocumentUpdateRequest() {
return preservesReadOnlyDocumentFields() &&
( resourceInUserClass() || userInResourceTeachers() );
}
// return list of networks available to the current teacher
function getTeacherNetworks() {
let platformUserId = string(request.auth.token.platform_user_id);
return get(/databases/$(database)/documents/authed/$(portal)/users/$(platformUserId)).data.networks;
}
// check whether the user being requested is in any of the current teacher's networks
function teacherInRequestedTeacherNetworks() {
return resource.data.networks.hasAny(getTeacherNetworks());
}
// check whether the document being created/updated is associated with one of this teacher's networks
function requestInTeacherNetworks() {
return request.resource.data.network in getTeacherNetworks();
}
// check whether the document being read is associated with one of this teacher's networks
function resourceInTeacherNetworks() {
return resource.data.network in getTeacherNetworks();
}
// user's class_hash must be the requested document's context_id
function resourceInUserClass() {
return request.auth.token.class_hash == resource.data.context_id;
}
match /users/{userId} {
// teachers can read their own user documents or other teachers in the same network
allow read: if isAuthedTeacher() &&
((string(request.auth.token.platform_user_id) == userId) ||
teacherInRequestedTeacherNetworks());
// currently, only admins can write user information
allow write: if false;
}
function isValidClassCreateRequest() {
let requiredFields = ["id", "name", "uri", "context_id", "teacher", "teachers"];
return userInRequestTeachers() &&
(!("network" in request.resource) || requestInTeacherNetworks()) &&
request.resource.data.keys().hasAll(requiredFields);
}
function preservesReadOnlyClassFields() {
let readOnlyFieldsSet = ["id", "uri", "context_id"].toSet();
let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys();
return !affectedFieldsSet.hasAny(readOnlyFieldsSet);
}
function isValidClassUpdateRequest() {
return userInRequestTeachers() && userInResourceTeachers() &&
requestInTeacherNetworks() && preservesReadOnlyClassFields();
}
match /classes/{classId} {
// portal-authenticated teachers can create valid classes
allow create: if isAuthedTeacher() && isValidClassCreateRequest();
// teachers can only update their own classes and only if they're valid
allow update: if isAuthedTeacher() && isValidClassUpdateRequest();
// we don't support deleting classes at this time
allow delete: if false;
// teachers can read, non existing classes, their own classes, or any class in their network
allow read: if isAuthedTeacher() && (
resource == null ||
userInResourceTeachers() ||
resourceInTeacherNetworks());
}
function isValidOfferingCreateRequest() {
let requiredFields = ["id", "name", "uri", "context_id", "teachers",
"unit", "problem", "problemPath", "network"];
return userInRequestTeachers() && requestInTeacherNetworks() &&
request.resource.data.keys().hasAll(requiredFields);
}
function preservesReadOnlyOfferingFields() {
let readOnlyFieldsSet = ["id", "uri", "context_id", "unit",
"problem", "problemPath", "network"].toSet();
let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys();
return !affectedFieldsSet.hasAny(readOnlyFieldsSet);
}
function isValidOfferingUpdateRequest() {
// teachers can update the list of teachers for the offering,
// but only if they were already a teacher and are still a teacher
return userInRequestTeachers() && userInResourceTeachers() &&
requestInTeacherNetworks() && preservesReadOnlyOfferingFields();
}
match /offerings/{offeringId} {
// portal-authenticated teachers can create valid offerings
allow create: if isAuthedTeacher() && isValidOfferingCreateRequest();
// teachers can only update their own documents and only if they're valid
allow update: if isAuthedTeacher() && isValidOfferingUpdateRequest();
// we don't support deleting offerings at this time
allow delete: if false;
// teachers can read offerings in their network
allow read: if isAuthedTeacher() && resourceInTeacherNetworks();
}
match /curriculum/{pathId} {
// portal-authenticated teachers can create valid curriculum documents
allow create: if isAuthedTeacher() && isValidCurriculumCreateRequest();
// curriculum documents can't be updated or deleted
allow update, delete: if false;
// teachers can read their own curriculum documents or others in their network
allow read: if isAuthedTeacher() && (userIsResourceUser() || resourceInTeacherNetworks());
// return the author/owner of the specified (curriculum) document
function getCurriculumOwner() {
return get(/databases/$(database)/documents/authed/$(portal)/curriculum/$(pathId)).data.uid;
}
// return the network with which the specified (curriculum) document is associated
function getCurriculumNetwork() {
return get(/databases/$(database)/documents/authed/$(portal)/curriculum/$(pathId)).data.network;
}
// check whether the (curriculum) document is associated with one of the teacher's networks
function curriculumInTeacherNetworks() {
let curriculumNetwork = getCurriculumNetwork();
return stringExists(curriculumNetwork) && (curriculumNetwork in getTeacherNetworks());
}
// check whether the teacher owns/created the curriculum document
function teacherIsCurriculumOwner() {
let curriculumOwner = getCurriculumOwner();
return string(request.auth.token.platform_user_id) == curriculumOwner;
}
// teachers can access (curriculum) documents that they own or are associated with one of their networks
function teacherCanAccessCurriculum() {
return isAuthedTeacher() && (teacherIsCurriculumOwner() || curriculumInTeacherNetworks());
}
function isValidCommentCreateRequest() {
return userIsRequestUser() &&
// comments network must match network of parent document
(request.resource.data.network == getCurriculumNetwork()) &&
request.resource.data.keys().hasAll(["name", "createdAt", "content", "network"]);
}
function preservesReadOnlyCommentFields() {
let readOnlyFieldsSet = ["uid", "network", "createdAt", "tileId"].toSet();
let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys();
return !affectedFieldsSet.hasAny(readOnlyFieldsSet);
}
function isValidCommentUpdateRequest() {
return userIsRequestUser() && preservesReadOnlyCommentFields();
}
match /comments/{commentId} {
// portal-authenticated teachers with access to the document can create valid comments
allow create: if teacherCanAccessCurriculum() && isValidCommentCreateRequest();
// teachers can only update their own comments and only if they're valid
allow update: if teacherCanAccessCurriculum() && isValidCommentUpdateRequest();
// teachers can only delete their own comments
allow delete: if teacherCanAccessCurriculum() && userIsResourceUser();
// teachers with access to the curriculum document can read the comments
allow read: if teacherCanAccessCurriculum();
}
}
match /documents/{docId} {
// portal-authenticated teachers can create valid documents
allow create: if isValidDocumentCreateRequest();
// teachers can only update their own documents and only if they're valid
allow update: if isValidDocumentUpdateRequest();
// teachers can only delete their own documents
allow delete: if isAuthedTeacher() && userIsResourceUser();
// teachers can read their own documents or other documents in their network
allow read: if (isAuthed() && (resource == null || userOwnsDocument() || resourceInUserClass())) ||
(isAuthedTeacher() && (userInResourceTeachers() || resourceInTeacherNetworks()));
function getDocumentPath() {
return /databases/$(database)/documents/authed/$(portal)/documents/$(docId)
}
function getDocumentData() {
return get(getDocumentPath()).data;
}
// return owner of the parent document
function getDocumentOwner() {
return getDocumentData().uid;
}
// return the network with which the specified document is associated
function getDocumentNetwork() {
return getDocumentData().network;
}
// Note: some of this logic seems redundant with the functions used above:
// userInResourceTeachers, resourceInTeacherNetworks, resourceInUserClass.
// However there is an important difference.
// This function works with the doc that is the parent of the comments or history. While those other
// functions work with the actual resource/document being requested.
// So `userInResourceTeachers` would fail if used on a comment request because it would be
// looking for teachers in the comment document itself. And the logic in this function would
// probably be inefficient for a top level document request becuase it would do an additional lookup
// to retrieve the data from the document, when the system already had access the document in
// `resource.data`.
function userCanAccessDocument() {
let docData = getDocumentData();
let docNetwork = docData.network;
return (
// check if document is in user's class
request.auth.token.class_hash == docData.context_id ||
// check whether the document's network corresponds to one of the users's networks
stringExists(docNetwork) && (docNetwork in getTeacherNetworks()) ||
// check whether the document is in a different class for the teacher
teacherIsInClass(docData.context_id) ||
// check whether the current user is one of the teachers associated with the (legacy) document
// (listing teachers in the document is no longer current practice)
('teachers' in docData &&
(string(request.auth.token.platform_user_id) in docData.teachers))
);
}
// check whether the teacher can access the document
function teacherCanAccessDocument() {
return isAuthedTeacher() && (!exists(getDocumentPath()) || userCanAccessDocument());
}
function isValidCommentCreateRequest() {
return userIsRequestUser() &&
// comment's network must match network of parent document
(request.resource.data.network == getDocumentNetwork()) &&
request.resource.data.keys().hasAll(["name", "createdAt", "content"]);
}
function preservesReadOnlyCommentFields() {
let readOnlyFieldsSet = ["uid", "network", "createdAt", "tileId"].toSet();
let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys();
return !affectedFieldsSet.hasAny(readOnlyFieldsSet);
}
function isValidCommentUpdateRequest() {
return userIsRequestUser() && preservesReadOnlyCommentFields();
}
function userOwnsDocument() {
return getDocumentOwner() == string(request.auth.token.platform_user_id);
}
match /comments/{commentId} {
// portal-authenticated teachers with access to the document can create valid comments
allow create: if teacherCanAccessDocument() && isValidCommentCreateRequest();
// teachers can only update their own comments and only if they're valid
allow update: if teacherCanAccessDocument() && isValidCommentUpdateRequest();
// teachers can only delete their own comments
allow delete: if teacherCanAccessDocument() && userIsResourceUser();
// only teachers that "own" the document can read the comments (for now)
allow read: if teacherCanAccessDocument();
}
// For writing individual history entries
match /history/{entryId} {
allow create: if isAuthed() && userOwnsDocument();
allow read: if (isAuthed() && userOwnsDocument()) || teacherCanAccessDocument();
allow delete: if false;
allow update: if false;
}
}
match /mcsupports/{docId} {
// portal-authenticated teachers can create valid supports
allow create: if isAuthedTeacher() && isValidSupportCreateRequest();
// teachers can only update their own supports and only if they're valid
allow update: if isAuthedTeacher() && isValidSupportUpdateRequest();
// teachers can only delete their own supports
allow delete: if isAuthedTeacher() && userIsResourceUser();
// teachers and students in appropriate classes can read supports
allow read: if userIsResourceUser() || classInResourceClasses();
}
}
//
// non-portal-authenticated/dev/demo/qa/test rules
//
match /demo/{demoName}/{restOfPath=**} {
allow read, write: if isAuthed();
}
// In the future, developers might use a random unique key instead of
// the firebase user id. So this rule is relaxed in order to allow that.
match /dev/{devId}/{restOfPath=**} {
allow read, write: if isAuthed();
}
match /qa/{userId}/{restOfPath=**} {
allow read: if isAuthed();
// users can only write to their own folders
allow write: if matchFirebaseUserId(userId);
}
match /test/{userId}/{restOfPath=**} {
allow read: if isAuthed();
// users can only write to their own folders
allow write: if matchFirebaseUserId(userId);
}
}
}