Last active
July 28, 2023 09:00
-
-
Save kamiljozwik/2386a3725f0304bfee82a73429483f10 to your computer and use it in GitHub Desktop.
Firebase Firestore security rules
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
rules_version = '2'; | |
// Example rules for chat app | |
// https://www.youtube.com/watch?v=zQyrwxMPm88 | |
// collections: "users", "messages", "banned" | |
service cloud.firestore { | |
match /databases/{database}/documents { | |
match /{document=**} { | |
allow read, write: if false; | |
} | |
match /messages/{docId} { | |
allow read: if request.auth.uid != null; // user is signed in | |
allow create: if canCreateMessage(); | |
} | |
function canCreateMessage() { | |
let isSignedIn = request.auth.uid != null; | |
let isOwner = request.auth.uid == request.resource.data.uid; // message being created has the same userId as the user creating it | |
let isNotTooLong = request.resource.data.text.size() < 255; | |
let isNow = request.time == request.resource.data.createdAt; // valid time stamp | |
// exists used as last because it counts as a read (we pay for reads) | |
let isNotBanned = exists( | |
/databases/$(database)/documents/banned/$(request.auth.uid) | |
) == false; | |
return isSignedIn && isOwner && isNotTooLong && isNow && isNotBanned; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
rules_version = '2'; | |
// cli: firebase deploy --only firestore:rules | |
service cloud.firestore { | |
// root of the DB | |
match /databases/{database}/documents { | |
// match sigle document (kamkam = documentId) | |
match /users/kamkam { | |
allow read, write: if false; | |
} | |
// match all documents in collection (userId jest teraz zmienną) | |
// this rule doesn't apply to subcollections | |
match /users/{userId} { | |
allow read, write: if request.auth.uid == userId; | |
// allow create: if a && b && c; (&& - and, || - or, ! - not) | |
// https://firebase.google.com/docs/rules/rules-language#building_conditions (All operators) | |
// read = get | list | |
// write = create | update | delete | |
// better to use get, list, create, update, delete for better control. read, write is too general. | |
// request - object that contains all the information about the request | |
// request.auth - JWT (id, email, etc.) | |
// request.resource - data payload that is being written to the DB (attempting to modify DB) | |
// request.time - timestamp of the request | |
// request.path - path of the request in the DB (collection/document/subcollection) | |
// request.method - read, create, delete, etc. | |
// resource - object that contains all the information about the existing resource (document) in the DB | |
// ⚠️ resource != request.resource (to co już jest vs. to co do nas przychodzi) | |
// example: allow write: if request.resource.data.username == resource.data.username; (username can't be changed) | |
} | |
// apply to parent collection and all subcollections (nested collections) | |
match /users/{userId==**} { | |
allow read, write: if request.auth.uid == userId; | |
} | |
// EXAMPLE 1 | |
match /users/{userId} { | |
allow read: if isLoggedIn(); // allow read for all authenticated users (tylko zalogowani mogą odczytywać dane, np. profil innych użytkowników) | |
allow write: if belongsTo(userId); // allow write only for the owner of the document (tylko właściciel może zmieniać dane) | |
} | |
// EXAMPLE 2 (user can have many todos, na każdym TODO jest klucz "uid", który jest równy uid właściciela tego TODO) | |
match /todos/{docId} { | |
allow read: if resource.data.status == 'published'; // allow read only if status is published | |
allow create: if canCreateTodo(); // serverTimestamp() required in the app to make it work | |
allow update: if belongsTo(docId) && request.resource.data.keys().hasOnly(['text', 'status']); // allow update only if user is the owner of the document and only "text" and "status" keys can be updated. Useful if don't want to change some keys like "createdAt" or "username", which should stay immutable. | |
// https://firebase.google.com/docs/reference/rules/rules.List - inne metody (tutaj docs dla np. "hasOnly") do sprawdzania zawartości przychodzącego lub obecnego w DB obiektu | |
} | |
// It is important where the function is defined. | |
// Function will have access to the variables in the scope where it is defined. | |
function isLoggedIn() { | |
return request.auth.uid != null; | |
} | |
function belongsTo(userId) { | |
return request.auth.uid == userId || request.auth.uid == resource.data.uid; | |
} | |
function canCreateTodo() { | |
let uid = request.auth.uid; | |
let hasValidTimestamp = request.time == request.resource.data.createdAt; | |
return belongsTo(uid) && hasValidTimestamp; | |
} | |
// GET / EXISTS allow access other documents in the DB (it is billed as a standard read operation) | |
exists(/databases/$(database)/documents/users/$(SOME_DOC_ID)) | |
get(/databases/$(database)/documents/users/$(request.auth.uid)) | |
// usage example | |
allow create: if request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid)); | |
allow delete: if request.auth != null && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true; | |
// can be used in the function as well | |
function isAdmin(uid) { | |
let profile = get(/databases/$(database)/documents/users/$(uid)); | |
return profile.data.isAdmin == true; | |
} | |
// atomic operations that updates multiple documents at the same time | |
existsAfter(/databases/$(database)/documents/users/$(SOME_DOC_ID), request.time) | |
getAfter(/databases/$(database)/documents/users/$(SOME_DOC_ID), request.time) | |
// apply to all documents in all collections | |
match /{document=**} { | |
allow read, write: if request.time < timestamp.date(2023, 8, 30); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
rules_version = '2'; | |
// document in "users" collection has a key called "roles", which is an array of strings. ⚠️ Make sure user is not allowed to update this keys!!! | |
// document in "posts" collection has keys "content", "uid", "createdAt", "updatedAt", "published" | |
service cloud.firestore { | |
match /databases/{database}/documents { | |
match /users/{userId} { | |
allow read: if isSignedIn(); | |
allow update, delete: if hasAnyRole(['admin']); | |
} | |
match /posts/{postId} { | |
allow read: if ( isSignedIn() && resource.data.published ) || hasAnyRole(['admin']); | |
allow create: if isValidNewPost() && hasAnyRole(['author']); | |
allow update: if isValidUpdatedPost() && hasAnyRole(['author', 'editor', 'admin']); | |
allow delete: if hasAnyRole(['admin']); | |
} | |
function isSignedIn() { | |
return request.auth != null; | |
} | |
function hasAnyRole(roles) { | |
return isSignedIn() | |
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles.hasAny(roles) | |
} | |
function isValidNewPost() { | |
let post = request.resource.data; | |
let isOwner = post.uid == request.auth.uid; | |
let isNow = request.time == request.resource.data.createdAt; | |
let hasRequiredFields = post.keys().hasAll(['content', 'uid', 'createdAt', 'published']); | |
return isOwner && hasRequiredFields && isNow; | |
} | |
function isValidUpdatedPost() { | |
let post = request.resource.data; | |
let hasRequiredFields = post.keys().hasAny(['content', 'updatedAt', 'published']); | |
let isValidContent = post.content is string && post.content.size() < 5000; | |
return hasRequiredFields && isValidContent; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { assertFails, assertSucceeds } = require("@firebase/rules-unit-testing"); | |
const { setup, teardown } = require("./helpers"); | |
const mockUser = { uid: "bob" }; | |
const mockData = { | |
"users/bob": { | |
roles: ["admin"], | |
}, | |
"posts/abc": { | |
content: "hello world", | |
uid: "alice", | |
createdAt: null, | |
published: false, | |
}, | |
}; | |
// firebase emulators:start | |
// firebase emulators:start --only firestore | |
// ⚠️ Always have emulators running before running tests | |
describe("Database rules", () => { | |
let db; | |
// Applies only to tests in this describe block | |
beforeAll(async () => { | |
db = await setup(mockUser, mockData); | |
}); | |
afterAll(async () => { | |
await teardown(); | |
}); | |
test("deny when reading an unauthorized collection", async () => { | |
const ref = db.collection("secret-stuff"); | |
expect(await assertFails(ref.get())); | |
}); | |
test("allow admin to read unpublished posts", async () => { | |
const ref = db.doc("posts/abc"); | |
expect(await assertSucceeds(ref.get())); | |
}); | |
test("allow admin to update posts of other users", async () => { | |
const ref = db.doc("posts/abc"); | |
expect(await assertSucceeds(ref.update({ published: true }))); | |
}); | |
}); | |
// Test coverage: localhost:8080/emulator/v1/projects/<project-id>:ruleCoverage.html | |
// Test coverage: localhost:8080/emulator/v1/projects/fireship-dev-17429:ruleCoverage.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// npm install --save-dev jest @firebase/rules-unit-testing firebase-admin | |
// "test": "jest --env=node --forceExit", -> package.json | |
const { | |
loadFirestoreRules, | |
initializeTestApp, | |
clearFirestoreData, | |
initializeAdminApp, | |
} = require("@firebase/rules-unit-testing"); | |
const { readFileSync } = require("fs"); | |
// auth - mock user, data - mock data | |
module.exports.setup = async (auth, data) => { | |
const projectId = `fireship-dev-17429`; | |
// now we can interact with the test app using regular firebase SDK | |
const app = initializeTestApp({ | |
projectId, | |
auth, | |
}); | |
// console.log(app.auth().currentUser) | |
const db = app.firestore(); | |
// Write mock documents before rules. | |
// Admin SDK can write mock data directly to Firestore without respecting the rules, because server is considered trusted. | |
// Dzięki temu możemy zapełnić bazę danych, która będzie nam potrzebna do testów. | |
if (data) { | |
const admin = initializeAdminApp({ | |
projectId, | |
}); | |
for (const key in data) { | |
const ref = admin.firestore().doc(key); | |
await ref.set(data[key]); | |
} | |
} | |
// Apply rules | |
await loadFirestoreRules({ | |
projectId, | |
rules: readFileSync("firestore.rules", "utf8"), | |
}); | |
return db; | |
}; | |
// Cleanup everything between tests | |
module.exports.teardown = async () => { | |
Promise.all(firebase.apps().map((app) => app.delete())); | |
await clearFirestoreData(); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Docs:
Other CLI commands: https://firebaseopensource.com/projects/firebase/firebase-tools/