-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
405 lines (346 loc) · 14.5 KB
/
Copy pathfirestore.rules
File metadata and controls
405 lines (346 loc) · 14.5 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
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
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ============================================
// HELPER FUNCTIONS
// ============================================
function isAuthenticated() {
return request.auth != null;
}
function getUserData() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
}
function getUserRole() {
return getUserData().role;
}
function getUserCompanyId() {
return getUserData().companyId;
}
// Role checking functions (NEW ROLE SYSTEM)
function isStudent() {
return isAuthenticated() && getUserRole() == 'STUDENT';
}
function isCompany() {
return isAuthenticated() && getUserRole() == 'COMPANY';
}
function isReviewer() {
return isAuthenticated() && getUserRole() == 'REVIEWER';
}
// Legacy role support (for backward compatibility during migration)
function isLegacyRecruiter() {
return isAuthenticated() && getUserRole() == 'RECRUITER';
}
// Combined role checks
function isCompanyOrReviewer() {
return isCompany() || isReviewer();
}
function isVerifiedCompany() {
return isCompany() &&
exists(/databases/$(database)/documents/companies/$(getUserCompanyId())) &&
get(/databases/$(database)/documents/companies/$(getUserCompanyId())).data.verified == true;
}
// Organization membership checks
function isOrgMember(orgId) {
return exists(/databases/$(database)/documents/organizationMembers/$(request.auth.uid + '_' + orgId));
}
function isOrgAdmin(orgId) {
let membership = get(/databases/$(database)/documents/organizationMembers/$(request.auth.uid + '_' + orgId)).data;
return membership.status == 'ACTIVE' && (membership.role == 'OWNER' || membership.role == 'ADMIN');
}
// Role immutability check
function roleIsImmutable() {
return !resource.data.keys().hasAny(['role']) ||
resource.data.role == request.resource.data.role;
}
// ============================================
// USERS COLLECTION
// ============================================
// Users can read/write their own document
// Role is IMMUTABLE after first set
match /users/{userId} {
allow read: if isAuthenticated() && request.auth.uid == userId;
// Allow creation only by the user themselves
allow create: if isAuthenticated() &&
request.auth.uid == userId &&
request.resource.data.uid == userId &&
// Role must be null, STUDENT, COMPANY, or REVIEWER
(request.resource.data.role == null ||
request.resource.data.role == 'STUDENT' ||
request.resource.data.role == 'COMPANY' ||
request.resource.data.role == 'REVIEWER');
// Allow updates only by the user themselves
// CRITICAL: Role cannot be changed once set (immutable)
allow update: if isAuthenticated() &&
request.auth.uid == userId &&
// If role exists and is NOT null in current document, it cannot be changed
(
// Case 1: Role doesn't exist in current doc (new field being added)
!('role' in resource.data) ||
// Case 2: Role is null in current doc (first time setting)
resource.data.role == null ||
// Case 3: Role is being set to the same value (no change)
resource.data.role == request.resource.data.role
) &&
// Ensure role is valid if being set
(!('role' in request.resource.data) ||
request.resource.data.role == null ||
request.resource.data.role == 'STUDENT' ||
request.resource.data.role == 'COMPANY' ||
request.resource.data.role == 'REVIEWER');
allow delete: if false; // Never allow user deletion via client
}
// ============================================
// TASKS COLLECTION
// ============================================
// All authenticated users can read tasks
// Only COMPANY role can create/update tasks
match /tasks/{taskId} {
// Allow read for all authenticated users (no role check needed for reading)
allow read: if isAuthenticated();
// Companies can create tasks
// Check that user document exists and has COMPANY role
allow create: if isAuthenticated() &&
exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'COMPANY' &&
request.resource.data.keys().hasAll(['companyId', 'isActive']) &&
request.resource.data.isActive == true;
// Companies can update their own tasks
// Check if user has COMPANY role and companyId matches
allow update: if isAuthenticated() &&
exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'COMPANY' &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.companyId == resource.data.companyId;
allow delete: if false; // Never allow deletion (set isActive = false instead)
}
// ============================================
// SUBMISSIONS COLLECTION
// ============================================
// Students can create and update their own (if not locked)
// Reviewers and Companies can read
match /submissions/{submissionId} {
// Simplified read rules - allow authenticated users with proper roles
allow read: if isAuthenticated();
allow create: if isAuthenticated() &&
exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'STUDENT' &&
request.resource.data.userId == request.auth.uid &&
request.resource.data.status == 'PENDING' &&
request.resource.data.isLocked == false;
allow update: if isAuthenticated() && (
// Students can update their own submissions if not locked
(resource.data.userId == request.auth.uid &&
resource.data.isLocked == false &&
request.resource.data.userId == resource.data.userId &&
request.resource.data.taskId == resource.data.taskId &&
request.resource.data.startTime == resource.data.startTime) ||
// Companies can update submissions for their tasks (for review)
(isCompany() &&
resource.data.companyId == getUserCompanyId() &&
// Can update status, score, and feedback fields
request.resource.data.userId == resource.data.userId &&
request.resource.data.taskId == resource.data.taskId)
);
allow delete: if false; // Never allow deletion
}
// ============================================
// REVIEWS COLLECTION
// ============================================
// Only reviewers can create
// No updates or deletes allowed (immutable)
match /reviews/{reviewId} {
// Students, reviewers, and companies can read reviews
allow read: if isAuthenticated();
allow create: if isAuthenticated() &&
isReviewer() &&
request.resource.data.reviewerId == request.auth.uid;
allow update: if false; // Immutable
allow delete: if false; // Immutable
}
// RECEIPTS COLLECTION
// Public read (no auth required)
// No client writes allowed (Admin SDK only)
// Immutable forever
match /receipts/{receiptId} {
allow read: if true; // Public read
allow write: if false; // Admin SDK only, immutable
// AI Profile subcollection
match /aiProfile/{profileId} {
allow read: if true; // Public read for AI insights
allow write: if false; // Admin SDK only
}
}
// CANDIDATE PROFILES COLLECTION (AI aggregated profiles)
// Public read for discovery
match /candidateProfiles/{studentId} {
allow read: if true; // Public read
allow write: if false; // Admin SDK only
}
// GEMINI ANALYTICS LOGS
// No client access (Admin SDK only)
match /geminiAnalyticsLogs/{logId} {
allow read: if false;
allow write: if false; // Admin SDK only
}
// ============================================
// SHORTLISTS COLLECTION
// ============================================
// Companies can save candidates to shortlists
match /shortlists/{shortlistId} {
allow read: if isCompany() &&
resource.data.companyId == getUserCompanyId();
allow create: if isCompany() &&
request.resource.data.companyId == getUserCompanyId();
allow update: if isCompany() &&
resource.data.companyId == getUserCompanyId();
allow delete: if isCompany() &&
resource.data.companyId == getUserCompanyId();
}
// RECRUITER ANALYTICS COLLECTION
// No client access (Admin SDK only)
match /recruiterAnalytics/{analyticsId} {
allow read: if false;
allow write: if false; // Admin SDK only
}
// ============================================
// COMPANIES COLLECTION
// ============================================
// All authenticated users can read company profiles
// Companies can update their own profile
match /companies/{companyId} {
allow read: if isAuthenticated();
// Allow creation during role selection (before role is fully set)
// User must be authenticated and must be the owner
allow create: if isAuthenticated() &&
request.resource.data.ownerUid == request.auth.uid;
// Companies can update their own profile
allow update: if isCompany() &&
getUserCompanyId() == companyId &&
// Cannot change ownership
resource.data.ownerUid == request.resource.data.ownerUid;
allow delete: if false; // Never allow deletion
}
// COMPANY TASKS COLLECTION
// Public read for discovery
// Only verified companies can write
match /companyTasks/{taskId} {
allow read: if isAuthenticated();
allow create: if false; // Created via API only
allow update: if isVerifiedCompany() &&
resource.data.companyId == get(/databases/$(database)/documents/users/$(request.auth.uid)).data.companyId;
allow delete: if false; // Never allow deletion
}
// ============================================
// PAYMENTS COLLECTION
// ============================================
// No client access (Admin SDK only)
match /payments/{paymentId} {
allow read: if isAuthenticated() && (
// Company can read their payments
(isCompany() && resource.data.companyId == getUserCompanyId()) ||
// Student can read their payments
resource.data.studentId == request.auth.uid
);
allow write: if false; // Admin SDK only
}
// COMPANY ACTIVITY LOGS
// No client access (Admin SDK only)
match /companyActivityLogs/{logId} {
allow read: if false;
allow write: if false; // Admin SDK only
}
// ORGANIZATIONS COLLECTION
// Members can read their organizations
match /organizations/{orgId} {
allow read: if isAuthenticated();
allow create: if false; // API only
allow update: if isAuthenticated(); // Members can update via API
allow delete: if false;
}
// ORGANIZATION MEMBERS COLLECTION
// Members can read their own membership
match /organizationMembers/{memberId} {
allow read: if isAuthenticated();
allow create: if false; // API only
allow update: if false; // API only
allow delete: if false; // API only (soft delete)
}
// CANDIDATE NOTES COLLECTION
// Org members can read/write notes for their org
match /candidateNotes/{noteId} {
allow read: if isAuthenticated();
allow create: if false; // API only
allow update: if false; // API only
allow delete: if false; // API only
}
// API KEYS COLLECTION
// No client access (Admin SDK only)
match /apiKeys/{keyId} {
allow read: if false;
allow write: if false; // Admin SDK only
}
// AUDIT LOGS COLLECTION
// No client access (Admin SDK only)
match /auditLogs/{logId} {
allow read: if false;
allow write: if false; // Admin SDK only
}
// ORG ANALYTICS COLLECTION
// No client access (computed via API)
match /orgAnalytics/{orgId} {
allow read: if false;
allow write: if false; // Admin SDK only
}
// RECEIPT SIGNATURES COLLECTION (Cryptographic trust)
// Public read for verification
match /receiptSignatures/{signatureId} {
allow read: if true; // Public read for verification
allow write: if false; // Admin SDK only
}
// TRUST REGISTRY (Public keys)
// Public read for signature verification
match /trustRegistry/{keyId} {
allow read: if true; // Public read
allow write: if false; // Admin SDK only
}
// REVOKED RECEIPTS
// Public read to check revocation status
match /revokedReceipts/{receiptId} {
allow read: if true; // Public read
allow write: if false; // Admin SDK only
}
// CREDENTIAL PROFILES (Public skill passports)
// Public read if profile is public
match /credentialProfiles/{studentId} {
allow read: if true; // Public read (filtered by isPublic in API)
allow create: if isAuthenticated() && request.auth.uid == studentId;
allow update: if isAuthenticated() && request.auth.uid == studentId;
allow delete: if false;
}
// REPUTATION SCORES
// Public read for leaderboards
match /reputationScores/{studentId} {
allow read: if true; // Public read
allow write: if false; // Admin SDK only
}
// SKILL GRAPH (Global analytics)
// Public read
match /skillGraph/{skill} {
allow read: if true; // Public read
allow write: if false; // Admin SDK only
}
// INSTITUTION PROGRAMS
// Members can read their org's programs
match /institutionPrograms/{programId} {
allow read: if isAuthenticated();
allow create: if false; // API only
allow update: if false; // API only
allow delete: if false;
}
// RECEIPT AUDIT TRAIL
// No client access (compliance/legal)
match /receiptAuditTrail/{eventId} {
allow read: if false;
allow write: if false; // Admin SDK only
}
}
}