-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
135 lines (125 loc) · 5.98 KB
/
Copy pathfirestore.rules
File metadata and controls
135 lines (125 loc) · 5.98 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
rules_version = '2';
// Taledge Firestore security rules.
// Default DENY. Every collection grants the minimum required access, scoped to
// the authenticated user. The Admin SDK (server routes + seed script) bypasses
// these rules entirely, so privileged writes happen server-side, never client.
// Deploy with: firebase deploy --only firestore:rules
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isOwner(uid) {
return isSignedIn() && request.auth.uid == uid;
}
// Custom claims (set via Admin SDK / seed) drive role-based reads.
function hasRole(role) {
return isSignedIn() && request.auth.token[role] == true;
}
// Recruiter-visibility / scoring keys on a candidate doc. These are written
// ONLY by the server Admin SDK (which bypasses these rules); a candidate must
// never be able to client-set them and forge a recruiter-visible, verified,
// or scored record.
function candidatePrivilegedKeys() {
return ['fit', 'publishedToRecruiters', 'publishedAt', 'verified',
'status', 'sourcedFrom', 'recruiterId', 'instituteId', 'jobId'];
}
// Account profile: self-signup may create one role-bearing record, but the
// identity and stakeholder role become immutable after creation. This keeps
// the open multi-persona registration flow while preventing role escalation.
match /users/{uid} {
allow read: if isOwner(uid);
allow create: if isOwner(uid)
&& request.resource.data.uid == uid
&& request.resource.data.email == request.auth.token.email
&& (!request.resource.data.keys().hasAny(['role'])
|| request.resource.data.role in ['candidate', 'recruiter', 'coach', 'institute']);
allow update: if isOwner(uid)
&& !request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['uid', 'email', 'linkId'])
&& (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['role'])
|| (!resource.data.keys().hasAny(['role'])
&& request.resource.data.role in ['candidate', 'recruiter', 'coach', 'institute']));
allow delete: if false;
}
// Candidate profiles: a user reads their own document; recruiters/institutes
// read profiles the candidate consented to publish.
match /candidates/{uid} {
allow read: if isOwner(uid);
allow read: if (hasRole('recruiter') || hasRole('institute'))
&& resource.data.publishedToRecruiters == true;
// The owner may create/update their own profile but must NOT set the
// privileged recruiter-visibility/scoring keys — those flow through the
// Admin SDK only (which bypasses these rules), so a client cannot forge a
// recruiter-visible or verified record. hasOnly(non-privileged) is
// expressed here as "the affected keys include none of the privileged set".
allow create: if isOwner(uid)
&& !request.resource.data.keys().hasAny(candidatePrivilegedKeys());
allow update: if isOwner(uid)
&& !request.resource.data.diff(resource.data)
.affectedKeys().hasAny(candidatePrivilegedKeys());
}
// ── Catalog / aggregate collections ──────────────────────────────────────
// Seeded reference data (cohorts, institutes, org drives, recruiter pool).
// Any signed-in user may READ; all writes go through the Admin SDK only.
match /examAspirants/{id} {
allow read: if isSignedIn();
allow write: if false;
}
match /institutes/{id} {
allow read: if isSignedIn();
allow write: if false;
}
match /recruiters/{id} {
allow read: if isSignedIn();
allow write: if false;
}
match /coaches/{id} {
allow read: if isSignedIn();
allow write: if false;
}
match /organisations/{id} {
allow read: if isSignedIn();
allow write: if false;
}
match /recruiterPool/{id} {
// Recruiter-facing shortlist data.
allow read: if hasRole('recruiter') || hasRole('institute');
allow write: if false;
}
// Interview sessions: owner-only. Server (Admin SDK) bypasses these rules.
match /interviewSessions/{sessionId} {
allow read, write: if isSignedIn()
&& resource.data.ownerUid == request.auth.uid;
allow create: if isSignedIn()
&& request.resource.data.ownerUid == request.auth.uid;
}
// Scored reports: owner read/write; recruiters read only published reports.
match /reports/{reportId} {
allow read, write: if isSignedIn()
&& resource.data.ownerUid == request.auth.uid;
allow create: if isSignedIn()
&& request.resource.data.ownerUid == request.auth.uid;
allow read: if hasRole('recruiter') && resource.data.publishedToRecruiters == true;
}
// Coaching sessions: owner-only; coaches read sessions assigned to them.
match /coachingSessions/{sessionId} {
allow read, write: if isSignedIn()
&& resource.data.ownerUid == request.auth.uid;
allow create: if isSignedIn()
&& request.resource.data.ownerUid == request.auth.uid;
// A coach reads ONLY the sessions assigned to them, never every tenant's.
allow read: if hasRole('coach') && resource.data.coachId == request.auth.uid;
}
// Hidden coding-assessment tests are generated and consumed only by trusted
// server routes. Candidates must never be able to read expected outputs or
// replace the grading cases through the client SDK.
match /codeTestCases/{testId} {
allow read, write: if false;
}
// Everything else: explicit deny.
match /{document=**} {
allow read, write: if false;
}
}
}