Skip to content

Instantly share code, notes, and snippets.

@kamiljozwik
Last active July 28, 2023 09:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kamiljozwik/2386a3725f0304bfee82a73429483f10 to your computer and use it in GitHub Desktop.
Save kamiljozwik/2386a3725f0304bfee82a73429483f10 to your computer and use it in GitHub Desktop.
Firebase Firestore security rules
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;
}
}
}
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);
}
}
}
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;
}
}
}
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
// 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();
};
@kamiljozwik
Copy link
Author

kamiljozwik commented Jul 28, 2023

@kamiljozwik
Copy link
Author

kamiljozwik commented Jul 28, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment