-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
112 lines (104 loc) · 5.44 KB
/
Copy pathfirestore.rules
File metadata and controls
112 lines (104 loc) · 5.44 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
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() { return request.auth != null; }
function isOwner(userId) { return signedIn() && request.auth.uid == userId; }
function hasRole(role) { return signedIn() && request.auth.token.role == role; }
function isInstitutionMember(institutionId) {
return signedIn() && request.auth.token.institutionId == institutionId;
}
function reportHasOnlyRedactedFields() {
return request.resource.data.keys().hasOnly([
'reportId', 'createdAt', 'createdAtIso', 'redactedMessage', 'redactionCounts',
'highRiskMarkers', 'language', 'countryRegion', 'destinationCountry', 'platform',
'context', 'claimedAuthority', 'senderDomainOrLinkHost', 'score', 'level',
'scamTypeGuess', 'consentVersion', 'deletionInstructions', 'institutionId'
]);
}
function reportRejectsRawMessage() {
return !request.resource.data.keys().hasAny(['rawMessage', 'message', 'fullMessage', 'unredactedMessage', 'attachments']);
}
function feedbackHasOnlyStructuredFields() {
return request.resource.data.keys().hasOnly([
'id', 'createdAt', 'createdAtIso', 'helpful', 'verifiedOfficialChannel',
'calibration', 'category', 'score', 'level', 'context', 'platform',
'tacticIds', 'riskAreaLevels', 'storageMode', 'schemaVersion'
]);
}
function feedbackRejectsMessageFields() {
return !request.resource.data.keys().hasAny([
'rawMessage', 'message', 'fullMessage', 'redactedMessage',
'unredactedMessage', 'attachments'
]);
}
function feedbackLooksStructured() {
return request.resource.data.id is string
&& request.resource.data.createdAtIso is string
&& request.resource.data.helpful in ['yes', 'no']
&& request.resource.data.verifiedOfficialChannel in ['yes', 'no', 'not yet']
&& request.resource.data.calibration in ['too low', 'accurate', 'too high']
&& request.resource.data.category in ['', 'missed risk', 'false alarm', 'unclear wording', 'useful']
&& request.resource.data.score is int
&& request.resource.data.score >= 0
&& request.resource.data.score <= 100
&& request.resource.data.level in ['low', 'medium', 'high', 'critical']
&& request.resource.data.context is string
&& request.resource.data.platform is string
&& request.resource.data.tacticIds is list
&& request.resource.data.riskAreaLevels is map
&& request.resource.data.storageMode in ['local', 'firebase']
&& request.resource.data.schemaVersion == 'feedback-v1';
}
function reportLooksRedacted() {
return request.resource.data.reportId is string
&& request.resource.data.redactedMessage is string
&& request.resource.data.redactedMessage.size() <= 5000
&& request.resource.data.score is int
&& request.resource.data.score >= 0
&& request.resource.data.score <= 100
&& request.resource.data.level in ['low', 'medium', 'high', 'critical'];
}
// Reports: anonymous or authenticated users may create redacted reports only.
// The public cannot read, update, or delete individual reports from the client.
match /reports/{reportId} {
allow create: if reportHasOnlyRedactedFields() && reportRejectsRawMessage() && reportLooksRedacted();
allow read, update, delete: if false;
}
// Anonymous feedback: structured calibration only. Raw, redacted, or original
// message fields are explicitly rejected; public clients can only create.
match /anonymousFeedback/{feedbackId} {
allow create: if feedbackHasOnlyStructuredFields() && feedbackRejectsMessageFields() && feedbackLooksStructured();
allow read: if hasRole('admin');
allow update, delete: if false;
}
// Users: each authenticated user can manage only their own profile.
match /users/{userId} {
allow read, delete: if isOwner(userId);
allow create, update: if isOwner(userId)
&& !request.resource.data.keys().hasAny(['rawMessage', 'passportNumber', 'cardNumber']);
}
// Institutions: institution members can read their institution record; only admins can write.
match /institutions/{institutionId} {
allow read: if hasRole('admin') || isInstitutionMember(institutionId);
allow create, update, delete: if hasRole('admin');
}
// Institution summaries: aggregated trend intelligence only, scoped to authorized members/admins.
match /institutionSummaries/{summaryId} {
allow read: if hasRole('admin') || (resource.data.institutionId is string && isInstitutionMember(resource.data.institutionId));
allow create, update, delete: if hasRole('admin');
}
// Pilot requests: public visitors may request contact, but raw scam messages are rejected.
// Admins can read and manage requests.
match /pilotRequests/{requestId} {
allow create: if !request.resource.data.keys().hasAny(['rawMessage', 'passportNumber', 'cardNumber', 'attachments'])
&& request.resource.data.keys().hasOnly(['organizationName', 'contactEmail', 'role', 'country', 'notes', 'createdAt'])
&& request.resource.data.contactEmail is string
&& request.resource.data.contactEmail.size() <= 320;
allow read, update, delete: if hasRole('admin');
}
// Deny by default for every other collection/document.
match /{document=**} {
allow read, write: if false;
}
}
}