-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
172 lines (153 loc) · 6.19 KB
/
Copy pathfirestore.rules
File metadata and controls
172 lines (153 loc) · 6.19 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
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ─────────────────────────────────────────────
// HELPER FUNCTIONS
// ─────────────────────────────────────────────
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
function isAdmin() {
return isAuthenticated() &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
// ─────────────────────────────────────────────
// COLLECTION RULES
// ─────────────────────────────────────────────
// Products: Publicly readable (active only), Admin writeable
match /products/{productId} {
allow read: if isAdmin() || (resource == null || resource.data.status == 'active');
allow write: if isAdmin();
}
// Categories & Types (Taxonomy)
match /knowledgebase_categories/{catId} {
allow read: if true;
allow write: if isAdmin();
}
match /knowledgebase_articles/{artId} {
allow read: if true;
allow write: if isAdmin();
}
match /product_categories/{catId} {
allow read: if true;
allow write: if isAdmin();
}
match /product_types/{typeId} {
allow read: if true;
allow write: if isAdmin();
}
// Users: Owner or Admin
match /users/{userId} {
allow read: if isOwner(userId) || isAdmin();
// Prevent users from changing their own role (privilege escalation)
allow create: if isOwner(userId) && (!request.resource.data.keys().contains('role') || request.resource.data.role == 'customer');
allow update: if isAdmin() || (isOwner(userId) && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']));
}
// Carts: Owner only
match /carts/{cartId} {
allow read, write: if isAuthenticated() && (
resource == null || resource.data.userId == request.auth.uid
) && request.resource.data.userId == request.auth.uid &&
// Production Hardening: Prevent negative quantities in cart at the substrate layer
request.resource.data.items.all(item, item.quantity > 0 && item.quantity < 1000);
}
// Orders: Owner or Admin
match /orders/{orderId} {
allow read: if isAuthenticated() && (
resource.data.userId == request.auth.uid || isAdmin()
);
// Orders are created via server-side API routes (Admin SDK)
allow create: if false;
allow update: if isAdmin();
allow delete: if false;
}
// Wishlists: Owner only
match /wishlists/{wishlistId} {
allow read, write: if isAuthenticated() && (
resource == null || resource.data.userId == request.auth.uid
) && request.resource.data.userId == request.auth.uid;
}
// Support Tickets
match /support_tickets/{ticketId} {
allow read: if isAuthenticated() && (
resource.data.userId == request.auth.uid || isAdmin()
);
// Tickets are created via server-side API routes
allow create: if false;
allow update: if isAdmin() || (
isAuthenticated() && resource.data.userId == request.auth.uid &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(['updatedAt'])
);
}
match /ticket_messages/{messageId} {
allow read: if isAuthenticated() && (
resource.data.senderId == request.auth.uid || isAdmin() ||
get(/databases/$(database)/documents/support_tickets/$(resource.data.ticketId)).data.userId == request.auth.uid
);
// Ensure users can only create messages for tickets they own
allow create: if isAuthenticated() && (
isAdmin() ||
get(/databases/$(database)/documents/support_tickets/$(request.resource.data.ticketId)).data.userId == request.auth.uid
);
}
// Inventory & Procurement: Admin only
match /inventory_locations/{locId} {
allow read, write: if isAdmin();
}
match /inventory_levels/{levelId} {
allow read, write: if isAdmin();
}
match /suppliers/{supId} {
allow read, write: if isAdmin();
}
match /purchase_orders/{poId} {
allow read, write: if isAdmin();
}
match /transfers/{transferId} {
allow read, write: if isAdmin();
}
// Marketing & Settings
match /discounts/{discountId} {
allow get: if true; // Public can check individual discount codes
allow list: if isAdmin(); // Only admins can scrape/list all codes
allow write: if isAdmin();
}
match /settings/{settingId} {
allow get: if true;
allow list: if isAdmin();
allow write: if isAdmin();
}
match /collections/{collId} {
allow read: if true;
allow write: if isAdmin();
}
// System & Forensics
match /hive_audit/{auditId} {
allow read: if isAdmin();
allow write: if false; // Only written via service layer (Admin SDK)
}
match /stripe_events/{eventId} {
allow read: if isAdmin();
allow write: if false; // Only written via service layer
}
match /digital_access_logs/{logId} {
allow read: if isAdmin() || (isAuthenticated() && resource.data.userId == request.auth.uid);
allow write: if false; // Only written via service layer
}
// Email Idempotency Ledger
// TTL: 30 days (field: expiresAt). Firestore TTL policy must be set on this collection.
// Privacy: stores only recipient email, subject, sentAt, expiresAt. No message content.
// Replay safety: after TTL expiry, a new send is permitted (30d window exceeds all realistic retry windows).
match /sent_emails/{emailId} {
allow read: if isAdmin();
allow write: if false; // Only written via Admin SDK (BrevoEmailService.markEmailSent)
}
// Default deny
match /{path=**} {
allow read, write: if false;
}
}
}