Security Rules — az utolsó vonal
- security-rulesBevezető
01 — Az architektúrában
A Rules helye — egyedüli vonal a kliens előtt
updateDoc(orderRef, { status: 'paid' }), akkor pontosan ez a kérés érkezik a szerverhez. Ha a Rules nem mondja meg, hogy ezt kinek szabad, akkor bárki, aki ki tudja olvasni a kliens-konfigurációdat, bárki helyett bármit írhat.Mit lát a Firestore minden kérésnél?
- A kérés tartalmát — milyen kollekció, milyen dokumentum, milyen művelet (read/create/update/delete), és írásnál milyen mezők.
- Egy auth tokent — egy JWT-t, amit a Firebase Auth bocsátott ki, és ami tartalmazza a felhasználó UID-jét, custom claimjeit, és a Firebase szerver kulcsa aláírja.
allow vagy deny. Nincs lehetőség arra, hogy a Rules visszaszóljon a kliensnek hibakóddal, hogy pl. „nem volt elég jogosultságod" vs. „a mező típusa rossz" — minden hiba ugyanaz: permission-denied. Ezt érdemes észben tartani: a Rules nem üzleti logika, csak hozzáférés-szabályozás.admin.firestore() hívások mindig minden joggal mennek át. Ez egyszerre jó (kell, hogy a backend funkciók működjenek) és kockázatos (egy rosszul írt Cloud Function megkerülheti a Rules-t). Ezért a fontos validációkat a Cloud Function-ben is meg kell csinálni.- security-rulesBevezető
02 — A nyelv
A szabálynyelv alapjai
rules_version = '2'; // always 2 on new projects
service cloud.firestore { // which service
match /databases/{database}/documents { // root
match /posts/{postId} { // collection + doc
allow read: if true; // anyone can read
allow create: if request.auth != null;
allow update: if request.auth.uid == resource.data.authorId;
allow delete: if request.auth.token.role == 'admin';
}
}
}
A négy alap-művelet és a két csoport
| Művelet | Mit takar |
|---|---|
get | Egyetlen dokumentum lekérése (single doc fetch) |
list | Lekérdezés egy kollekcióra (több doc) |
create | Új dokumentum létrehozása |
update | Meglévő dokumentum módosítása |
delete | Dokumentum törlése |
read | get + list együtt |
write | create + update + delete együtt |
get és list különválasztása fontos: lehet, hogy egy dokumentumot csak akkor szabad olvasni, ha valaki konkrétan kéri az ID-jén keresztül, de listázni nem szabad. Pl. egy felhasználói profil: ha tudod a userId-t, akkor láthatod, de listázni az összeset nem szabad.list szabály a teljes listára vonatkozik egyben. Ha a kliens egy where('public', '==', true) lekérdezést indít, és a Rules csak akkor enged listázást, ha minden dokumentum publikus, akkor a Firestore először megnézi, hogy az egész kollekció ezt teljesíti-e. Általában nem teljesíti. A megoldás: a Rules-ban is le kell írni ugyanazt a szűrést — a Rules ellenőrzi a query-t, és csak akkor engedi át, ha a query maga is csak publikus dokumentumokra szűkít.- security-rulesKözéphaladó
03 — Path-ek
Match path patterns
Egy szegmensű wildcard
match /users/{uid} {
allow read, write: if request.auth.uid == uid;
}
{uid} egy változó: ide bármilyen érték illeszkedik, és ezt belülről változóként használhatod. Az uid név önkényes — bármi lehetne —, de érdemes beszédes nevet adni, mert hat hónap múlva, amikor a saját kódodat olvasod, ez számít.Többszintű wildcard
match /tenants/{tid}/orders/{oid} {
allow read: if request.auth.token.tenantId == tid;
}
Recursive wildcard
match /tenants/{tid}/{document=**} {
allow read: if request.auth.token.tenantId == tid;
}
{document=**} minden mélységű subdocumentumra illik — a tenant alatti összes adatra. Nagyon kényelmes, de óvatosan kell vele bánni: ha valamelyik subcollection kivételes szabályt igényel, akkor azt külön match-csel felül kell írni.Több match egymásba ágyazva
match /tenants/{tid} {
// Only members can read the tenant doc
allow read: if isMember(tid);
match /orders/{oid} {
allow read: if isMember(tid);
allow write: if isAdmin(tid);
}
match /products/{pid} {
allow read: if isMember(tid);
allow write: if isAdmin(tid);
}
}
match magától örökli a külső path változóit (tid), és ezt használhatod a függvényhívásban. Ez a struktúra jól tükrözi az adatmodell hierarchiáját.allow utasítás OR-rel van összekapcsolva: ha bármelyik igaz, a kérés engedélyezett. Tehát ha azt akarod, hogy egy művelet csak akkor legyen engedélyezett, ha több feltétel együtt teljesül, akkor azt egyetlen allow-ban kell &&-vel összekötni.- security-rulesKözéphaladó
04 — Az objektumok
Request és resource — kettő, nem egy
request a beérkező kérésről, a resource az adatbázisban már meglévő dokumentumról. A kettő összekeverése a leggyakoribb szabály-bug forrása.| Mező | Mit jelent |
|---|---|
request.auth | A kérést küldő felhasználó (uid, token, claims). null, ha nincs bejelentkezve. |
request.auth.uid | A felhasználó UID-je |
request.auth.token | A teljes JWT — beleértve a custom claimeket |
request.resource.data | Az új érték írásnál — amit a kliens küld |
request.method | get, list, create, update, delete |
request.time | A kérés érkezésének időpontja a szerveren |
resource.data | A jelenlegi érték az adatbázisban — frissítés vagy törlés előtt |
Az írás művelet különbsége
create-nél nincs resource, mert a dokumentum még nem létezik. update-nél van resource (a régi érték) és request.resource (az új érték). delete-nél csak resource van.match /orders/{oid} {
// CREATE — no resource yet, only request.resource
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.status == 'pending';
// UPDATE — both resource (old) and request.resource (new)
allow update: if resource.data.userId == request.auth.uid // owner only
&& request.resource.data.userId == resource.data.userId; // userId must not change
// DELETE — only resource is present
allow delete: if resource.data.userId == request.auth.uid;
}
- custom-claimsKözéphaladó
05 — Authentication
Authentication és custom claims
Az alap auth-ellenőrzés
// Anyone signed in
allow read: if request.auth != null;
// Only this specific user
allow read: if request.auth.uid == resource.data.ownerId;
// Only verified-email users
allow read: if request.auth.token.email_verified == true;
Custom claims a Rules-ban
request.auth.token alatt találod őket:match /admin/{document=**} {
allow read, write: if request.auth.token.role == 'admin';
}
match /tenants/{tid}/{document=**} {
allow read, write: if request.auth.token.tenantId == tid
&& request.auth.token.role in ['admin', 'member'];
}
await user.getIdToken(true) erőlteti a frissítést. Egyébként az új claim csak a következő automatikus token-frissítéskor (~1 órán belül) lesz látható.- validationKözéphaladó
06 — Validáció
Adatvalidáció — típus, érték, alak
Típusellenőrzés
// The field exists and is a string
request.resource.data.title is string
// The field is a number between 0 and 1000
request.resource.data.price is number
&& request.resource.data.price >= 0
&& request.resource.data.price <= 1000
// The field exists at all
request.resource.data.keys().hasAll(['title', 'price'])
// Only these fields may be present (no extras)
request.resource.data.keys().hasOnly(['title', 'price', 'authorId'])
Hossz és minta ellenőrzés
// Title between 5 and 200 characters
request.resource.data.title.size() >= 5
&& request.resource.data.title.size() <= 200
// Email regex
request.resource.data.email.matches('^[^@]+@[^@]+\\.[^@]+$')
// Slug format
request.resource.data.slug.matches('^[a-z0-9]+(-[a-z0-9]+)*$')
Változatlan mezők (immutable)
createdAt, userId, tenantId:match /orders/{oid} {
allow update: if request.auth != null
&& request.resource.data.userId == resource.data.userId
&& request.resource.data.createdAt == resource.data.createdAt
&& request.resource.data.tenantId == resource.data.tenantId;
}
diff() függvénnyel — megnézi, mely mezők változtak meg:// Only the 'status' and 'updatedAt' fields may change
allow update: if request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly(['status', 'updatedAt']);
- security-rulesHaladó
07 — Cross-document
Cross-document lookup — get és exists
get() és az exists() — de van ára.match /tenants/{tid}/orders/{oid} {
// Only if the user is a member of the tenant (member doc exists)
allow read: if exists(/databases/$(database)/documents/tenants/$(tid)/members/$(request.auth.uid));
// Only if the member is in the admin role
allow write: if get(/databases/$(database)/documents/tenants/$(tid)/members/$(request.auth.uid))
.data.role == 'admin';
}
A költség
get() hívás egy plusz olvasásnak számít a számlán. list műveletnél ez minden visszaadott dokumentumra alkalmazódik. Ha tehát egy listában 100 dokumentum van, és a Rules minden dokumentumhoz egy get()-tel ellenőriz valamit, akkor egy „100 doc listázás" valójában 200 olvasás.get(). Ha a tenant-tagság stabilan ritkán változik, akkor a tenantId-t és a role-t tedd a JWT-be — ezt a Rules ingyen olvassa, mert az auth tokenben utazik. Cross-doc lookup-ra csak akkor van szükség, ha a döntés tényleg dinamikusan változó adatra épül.Cache-elés egy szabályon belül
function-ben elnevezni, hogy ne ismételd — és a kiértékelés csak egyszer történjen meg.- security-rulesHaladó
08 — Function-ök
Function-ök — kompozíció és olvashatóság
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// --- Helper functions ---
function isSignedIn() {
return request.auth != null;
}
function isOwner(userId) {
return isSignedIn() && request.auth.uid == userId;
}
function hasRole(role) {
return isSignedIn() && request.auth.token.role == role;
}
function isMember(tid) {
return isSignedIn() && request.auth.token.tenantId == tid;
}
function unchanged(field) {
return request.resource.data[field] == resource.data[field];
}
// --- Rules ---
match /tenants/{tid}/orders/{oid} {
allow read: if isMember(tid);
allow create: if isMember(tid)
&& request.resource.data.userId == request.auth.uid;
allow update: if isMember(tid)
&& unchanged('userId')
&& unchanged('createdAt');
allow delete: if hasRole('admin');
}
}
}
isOwner(userId)) és más függvényeket hívhatnak — így rétegezhetőek a szabályok.return utasítást tartalmazhatnak, és nem hívhatják önmagukat (nincs rekurzió). Maximum 10 függvényhívás engedélyezett egy szabály-kiértékelésben — ha bonyolultabb logika kell, az már Cloud Function-be való.- testingKözéphaladó
- emulatorKözéphaladó
09 — Tesztelés
Tesztelés emulátorral
A test setup
import { initializeTestEnvironment, assertSucceeds, assertFails } from '@firebase/rules-unit-testing';
import { setDoc, doc, getDoc } from 'firebase/firestore';
import fs from 'node:fs';
let testEnv: Awaited<ReturnType<typeof initializeTestEnvironment>>;
beforeAll(async () => {
testEnv = await initializeTestEnvironment({
projectId: 'demo-project',
firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') },
});
});
afterAll(() => testEnv.cleanup());
beforeEach(() => testEnv.clearFirestore());
Egy konkrét teszt
describe('orders/{oid}', () => {
it('only the owner can read', async () => {
// Seed: order belonging to usr_alice
await testEnv.withSecurityRulesDisabled(async (ctx) => {
await setDoc(doc(ctx.firestore(), 'orders/ord_001'), {
userId: 'usr_alice', total: 12990,
});
});
// Alice can read
const alice = testEnv.authenticatedContext('usr_alice').firestore();
await assertSucceeds(getDoc(doc(alice, 'orders/ord_001')));
// Bob cannot
const bob = testEnv.authenticatedContext('usr_bob').firestore();
await assertFails(getDoc(doc(bob, 'orders/ord_001')));
// Anonymous cannot
const guest = testEnv.unauthenticatedContext().firestore();
await assertFails(getDoc(doc(guest, 'orders/ord_001')));
});
});
withSecurityRulesDisabled a kezdeti adatok feltöltésére jó — a teszt környezetbe gyorsan és Rules nélkül beíródik a kezdeti állapot. Aztán a tényleges állítások a Rules-ON kontextusban futnak.match blokkhoz írj legalább 3 tesztet: jó eset (jogos kérés, succeeds), jogosulatlan (rossz user, fails), és nem hitelesített (nincs bejelentkezve, fails). Ha van változatlan-mező-ellenőrzés, akkor egy negyedik teszt is kell: manipuláció (a tulajdonos módosítja az immutable mezőt, fails).- security-rulesHaladó
10 — Pattern könyvtár
Hat visszatérő mintázat
Owner-only access
Mikor: a dokumentumot csak a létrehozója (vagy egy konkrét user) láthatja és módosíthatja.
A standard megoldás: a dokumentumon van egy userId vagy ownerId mező, és ezt az auth uid-vel egyeztetjük.
match /notes/{noteId} {
allow read, update, delete: if isOwner(resource.data.userId);
allow create: if isOwner(request.resource.data.userId);
}
Role-based access
Mikor: különböző szerepkörök (admin, editor, viewer) eltérő jogokat kapnak.
A szerepkör custom claim-ként él a JWT-ben, így a Rules ingyen olvassa.
match /articles/{articleId} {
allow read: if true; // public
allow create: if hasRole('editor') || hasRole('admin');
allow update: if hasRole('editor') || hasRole('admin');
allow delete: if hasRole('admin');
}
Tenant isolation
Mikor: multi-tenant SaaS, ahol minden tenant adatát szigorúan el kell szigetelni a többitől.
A tenantId a JWT-ben él, és a path is tartalmazza — kettős védelem.
match /tenants/{tid}/{document=**} {
allow read: if isMember(tid);
allow write: if isMember(tid);
}
// Tenant-bound member list needs its own rule
match /tenants/{tid}/members/{uid} {
allow read: if isMember(tid);
allow write: if isMember(tid) && hasRole('admin');
}
Soft delete
Mikor: a dokumentumokat nem fizikailag törlöd, csak megjelölöd deletedAt mezővel — és csak a nem törölt elemeket szabad olvasni.
match /posts/{postId} {
allow read: if resource.data.deletedAt == null
|| hasRole('admin'); // admin sees everything
}
Immutable createdAt + updatedAt
Mikor: a createdAt sosem változhat, az updatedAt mindig a szerveridőre kell álljon.
match /items/{itemId} {
allow create: if request.resource.data.createdAt == request.time
&& request.resource.data.updatedAt == request.time;
allow update: if unchanged('createdAt')
&& request.resource.data.updatedAt == request.time;
}
Public-read, owner-write
Mikor: egy publikus profil — bárki láthatja, de csak a tulajdonos szerkesztheti.
match /profiles/{uid} {
allow read: if true;
allow write: if isOwner(uid);
}
- security-rulesHaladó
11 — Anti-pattern-ek
Hat klasszikus hiba
1) A „nyitott" Rules — élesben
match /{document=**} {
allow read, write: if true; // EVERYTHING is open!
}
2) A request.resource és resource keverése
// WRONG: on update this checks the old userId, but the client may send a new one
allow update: if resource.data.userId == request.auth.uid;
// RIGHT: check that the old userId matches AND the new one didn't change
allow update: if resource.data.userId == request.auth.uid
&& unchanged('userId');
3) A list szabály elfelejtése
get/create/update/delete szabályt írnak, és a list kimarad — pedig a read a get + list. Ha csak get-et engedélyezel, akkor a kliens nem fog tudni listázni, ami nem feltétlenül baj — de tudd, hogy így csinálod.4) Cross-doc lookup, ami túl gyakran fut
// Every read = +1 read — a 100-doc list means +100 reads!
allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid))
.data.subscription == 'pro';
// RIGHT: put subscription into a custom claim — Rules read it for free
allow read: if request.auth.token.subscription == 'pro';
5) Túlságosan bonyolult feltételek
allow sora 10 sor hosszú, akkor ott valószínűleg üzleti logika van, ami nem ide való. A túl bonyolult ellenőrzések a Cloud Function-ben élnek, amit a kliens hív, és ami az Admin SDK-val ír — a Rules csak az általános „ki és milyen formában" kérdésre válaszol, az „ezt a tranzakciót szabad-e most" kérdés a backend dolga.6) Elhanyagolt Rules-tesztelés
A Security Rules nem akkor jó, ha működik a fejlesztés alatt. Akkor jó, ha tesztelve, dokumentálva és időnként újragondolva van.
- security-rulesHaladó
12 — Konklúzió
Mi marad a Rules-ban — és mi nem?
Amit a Rules-ba érdemes tenni
- Ki olvashatja és írhatja a dokumentumot (auth check, role check, owner check).
- Az írandó adat alapvető formája (típus, hossz, regex, enum értékek).
- Változatlan mezők védelme (createdAt, userId, tenantId).
- Tenant- és user-szigetelés (path alapján és claim alapján).
- Soft-delete állapot ellenőrzés.
Amit a Rules-ba ne tegyél
- Bonyolult üzleti validáció (pl. „a rendelés értéke a hitelkereten belül van"). Ez Cloud Function.
- Külső API hívás. Nem lehetséges, és nem is illik bele.
- Tranzakciós, több dokumentumot átfogó feltételek (pl. „akkor lehet törölni, ha a kapcsolódó subcollection üres" — ezt egyszerűbb és biztonságosabb backenden ellenőrizni).
- Audit log, történeti rekordok írása. Ez Cloud Function trigger.
- Fizetés-ellenőrzés, kvóta-számolás. Ez backend.
A jó Rules-fájl ismérvei
Egy jól megírt Rules-fájl maximum 200-300 sor, helper függvényekkel a tetején, alatta a path-ok hierarchikusan rendezve. Minden szabályhoz tartozik 3-4 emulátoros teszt. A CI minden push-on lefuttatja a teszteket. Élesben az App Check is be van kapcsolva, ami megfogja azokat a kéréseket, amik nem az eredeti appból érkeznek.