-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathfirestore.rules
More file actions
301 lines (250 loc) · 13 KB
/
Copy pathfirestore.rules
File metadata and controls
301 lines (250 loc) · 13 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
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper function: verifica se l'utente è autenticato
function isAuthenticated() {
return request.auth != null;
}
// Helper function: verifica se l'utente è il proprietario
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
// Helper: l'utente è un membro delegato dell'account di ownerUid.
// Legge account-access/{ownerUid}.memberUids. Guardia exists() perché get()
// su un documento inesistente ritorna null e l'accesso a .data fallirebbe.
function isMemberOf(ownerUid) {
return isAuthenticated()
&& exists(/databases/$(database)/documents/account-access/$(ownerUid))
&& request.auth.uid in get(/databases/$(database)/documents/account-access/$(ownerUid)).data.memberUids;
}
// Helper: l'utente può agire sull'account di ownerUid — è il proprietario
// oppure un membro delegato (account condiviso). Usato al posto di isOwner
// su tutte le collezioni di dati (non su profilo/preferenze, che restano
// legate al viewer).
function canAccess(ownerUid) {
return isOwner(ownerUid) || isMemberOf(ownerUid);
}
// Collezione users - ogni utente può leggere e scrivere solo i propri dati
match /users/{userId} {
allow read: if isOwner(userId);
allow write: if isOwner(userId);
}
// Collezione assets - proprietario o membro delegato dell'account
match /assets/{assetId} {
// Lettura: proprietario o membro delegato
allow read: if canAccess(resource.data.userId);
// Creazione: un membro può creare doc timbrati con lo userId dell'account
// a cui ha accesso (non necessariamente il proprio uid)
allow create: if canAccess(request.resource.data.userId);
// Aggiornamento: proprietario/membro e userId immutabile (se presente)
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
// Cancellazione: proprietario o membro delegato
allow delete: if canAccess(resource.data.userId);
}
// Collezione monthly-snapshots - snapshot mensili del patrimonio
match /monthly-snapshots/{snapshotId} {
allow read: if canAccess(resource.data.userId);
allow create: if canAccess(request.resource.data.userId);
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
allow delete: if canAccess(resource.data.userId);
}
// Collezione price-history - storico prezzi (può essere condiviso tra utenti)
match /price-history/{priceId} {
// Lettura pubblica per tutti gli utenti autenticati
allow read: if isAuthenticated();
// Solo il sistema (admin SDK) può scrivere
allow write: if false;
}
// Collezione portfolios - se hai portafogli separati
match /portfolios/{portfolioId} {
allow read: if canAccess(resource.data.userId);
allow create: if canAccess(request.resource.data.userId);
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
allow delete: if canAccess(resource.data.userId);
}
// Collezione assetAllocationTargets - target di allocazione asset
match /assetAllocationTargets/{targetId} {
// Read: usa targetId (doc id) invece di resource.data.userId per consentire
// la lettura di documenti inesistenti (durante la registrazione, getDoc()
// viene chiamato prima che il documento esista)
allow read: if canAccess(targetId);
allow create: if canAccess(request.resource.data.userId);
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
allow delete: if canAccess(resource.data.userId);
}
// Collezione expenses - tracciamento spese ed entrate
match /expenses/{expenseId} {
// Lettura: proprietario o membro delegato
allow read: if canAccess(resource.data.userId);
// Creazione: un membro può creare spese timbrate con lo userId dell'account condiviso
allow create: if canAccess(request.resource.data.userId);
// Aggiornamento: proprietario/membro e userId immutabile (se presente)
// Permette update parziali che non includono userId
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
// Cancellazione: proprietario o membro delegato
allow delete: if canAccess(resource.data.userId);
}
// Collezione expenseCategories - categorie per le spese
match /expenseCategories/{categoryId} {
// Lettura: proprietario o membro delegato
allow read: if canAccess(resource.data.userId);
// Creazione: un membro può creare categorie sull'account condiviso
allow create: if canAccess(request.resource.data.userId);
// Aggiornamento: proprietario/membro e userId immutabile (se presente)
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
// Cancellazione: proprietario o membro delegato
allow delete: if canAccess(resource.data.userId);
}
// Collezione dividends - tracciamento dividendi
match /dividends/{dividendId} {
// Lettura: proprietario o membro delegato
allow read: if canAccess(resource.data.userId);
// Creazione: un membro può creare dividendi sull'account condiviso
allow create: if canAccess(request.resource.data.userId);
// Aggiornamento: proprietario/membro e userId immutabile (se presente)
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
// Cancellazione: proprietario o membro delegato
allow delete: if canAccess(resource.data.userId);
}
// Collezione pensionContributions - contributi al fondo pensione
// Scritta dal client SDK (come dividends): non c'è replay atomico multi-doc,
// e l'atomicità conto<->fondo del versamento volontario è già garantita da
// updateCashAssetBalancesAtomic dentro reconcileTransferCreate.
match /pensionContributions/{contributionId} {
// Lettura: proprietario o membro delegato
allow read: if canAccess(resource.data.userId);
// Creazione: un membro può creare contributi sull'account condiviso
allow create: if canAccess(request.resource.data.userId);
// Aggiornamento: proprietario/membro e userId immutabile (se presente)
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
// Cancellazione: proprietario o membro delegato
allow delete: if canAccess(resource.data.userId);
}
// Collezione hall-of-fame - ranking dell'account (doc id = ownerUid)
match /hall-of-fame/{userId} {
// Lettura: proprietario o membro delegato
allow read: if canAccess(userId);
// Scrittura: proprietario o membro; il campo userId deve coincidere col doc id
allow create, update: if canAccess(userId) &&
request.resource.data.userId == userId;
// Cancellazione: proprietario o membro delegato
allow delete: if canAccess(userId);
}
// Collezione goalBasedInvesting - obiettivi di investimento (doc id = ownerUid)
match /goalBasedInvesting/{userId} {
// Read: usa userId (doc id) per consentire la lettura di doc inesistenti (null)
allow read: if canAccess(userId);
// Write: proprietario o membro; il campo userId deve coincidere col doc id
allow create, update: if canAccess(userId) &&
request.resource.data.userId == userId;
// Delete: proprietario o membro delegato
allow delete: if canAccess(userId);
}
// Collezione budgets - configurazione budget (doc id = ownerUid)
match /budgets/{userId} {
// Read: usa userId (doc id) per consentire la lettura di doc inesistenti (null)
allow read: if canAccess(userId);
// Write: proprietario o membro; il campo userId deve coincidere col doc id
allow create, update: if canAccess(userId) &&
request.resource.data.userId == userId;
// Delete: proprietario o membro delegato
allow delete: if canAccess(userId);
}
// Collezione userPreferences - preferenze UI per utente (tema colori, ecc.)
// Document ID == userId, nessun campo userId nel documento
match /userPreferences/{userId} {
allow read: if isOwner(userId);
allow create, update: if isOwner(userId);
allow delete: if isOwner(userId);
}
// Collezione performance-cache - cache pre-calcolata dei dati Rendimenti
// Document ID == ownerUid. Un membro delegato legge/ricalcola la cache dell'account condiviso.
match /performance-cache/{userId} {
allow read: if canAccess(userId);
allow create, update: if canAccess(userId) &&
request.resource.data.userId == userId;
allow delete: if canAccess(userId);
}
// Collezione exposure-cache - cache esposizione portafoglio (ETF holdings, settori, emittenti)
// Document ID == ownerUid; Admin SDK write only (client cannot write directly).
match /exposure-cache/{userId} {
allow read: if canAccess(userId);
allow write: if false;
}
// Collezione benchmark-cache - rendimenti storici dei portafogli modello (dato globale)
// Document ID == benchmarkId (es. "60-40", "all-weather"); nessun campo userId.
// Leggibile da qualsiasi utente autenticato; scrivibile solo via Admin SDK (write: false).
match /benchmark-cache/{benchmarkId} {
allow read: if isAuthenticated();
allow write: if false;
}
// Collezione fx-rate-cache - tassi di cambio mensili USD/EUR (dato globale)
// Document ID == "usd-eur"; nessun campo userId.
// Leggibile da qualsiasi utente autenticato; scrivibile solo via Admin SDK (write: false).
match /fx-rate-cache/{docId} {
allow read: if isAuthenticated();
allow write: if false;
}
// Collezione ecb-rate-cache - tassi BCE deposit facility mensili (dato globale)
// Document ID == "deposit-rate"; nessun campo userId.
// Leggibile da qualsiasi utente autenticato; scrivibile solo via Admin SDK (write: false).
match /ecb-rate-cache/{docId} {
allow read: if isAuthenticated();
allow write: if false;
}
// Collezione costCenters - centri di costo per raggruppare spese per oggetto/progetto
match /costCenters/{costCenterId} {
allow read: if canAccess(resource.data.userId);
allow create: if canAccess(request.resource.data.userId);
allow update: if canAccess(resource.data.userId) &&
(!('userId' in request.resource.data) ||
request.resource.data.userId == resource.data.userId);
allow delete: if canAccess(resource.data.userId);
}
// Collezione assetTransactions - registro operazioni asset (trade ledger)
// Leggibile dal proprietario o da un membro delegato; scrittura SOLO via Admin API
// (ogni mutazione riscrive atomicamente i campi derivati dell'asset — vedi
// docs/specs/1-asset-transactions). Stessa postura "write:false" di account-access.
match /assetTransactions/{transactionId} {
allow read: if canAccess(resource.data.userId);
allow create, update, delete: if false;
}
// Collezione assetTransactionsMeta - metadati del registro (doc id = ownerUid)
// Read con userId (doc id) per consentire la lettura del doc inesistente prima
// della migrazione; scrittura solo via Admin API.
match /assetTransactionsMeta/{userId} {
allow read: if canAccess(userId);
allow write: if false;
}
// Collezione account-access - concessioni di accesso delegato (doc id = ownerUid)
// Lettura: il proprietario, o un membro elencato (abilita sia get() delle rules
// sopra sia la query array-contains con cui il viewer scopre gli account condivisi).
// Scrittura: solo via Admin SDK (/api/account/members), perché aggiungere un
// membro richiede la risoluzione email->uid lato server.
match /account-access/{ownerUid} {
allow read: if isOwner(ownerUid) ||
(isAuthenticated() && request.auth.uid in resource.data.memberUids);
allow write: if false;
}
// Blocca tutto il resto per sicurezza
match /{document=**} {
allow read, write: if false;
}
}
}