Firebase Firestore security rules: Using queries in security rules? - firebase

I am developing a web app on Firebase and Firestore, and here is what I am trying to do:
Users can upload their own posts, and set its visibility to public or private.
Any signed in user can see public posts, but only the users who are subscribing to the writer can view private posts. I am trying to write security rules for this.
Here is the database structure:
db.collection('posts').doc({postid})
//When a user writes a post, a new document is created. Includes the boolean 'public' field and the 'uid' field, which stores the writer's uid.
db.collection('subscription').doc({viewer_user_id})
//Once the logged-in user subscribes to another user, a document is created under the user's UID. The doc includes array of the UID of the users the viewer is subscribing.
Here are the descriptions of the relevant fields under each of the docs in 'posts' and 'subscription' collections:
posts/{postid}: {
'uid': Writer's uid (String)
'public': Boolean value to reflect visibility. If false, it is private.
}
subscription/{viewer's uid}: {
'subscription': Array of uids of the users the viewer is subscribing.
}
So, for the private documents, the basic idea is to look at the viewer's document in the subscription collection, get the data, and check whether the uid of the writer is included there. This will require some Javascript codes, but I don't know what the syntax would be in Firestore security rules, or is possible or not there to begin with.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function publicOrSubscribed() {
return request.auth.uid!=null&&(resource.data.public==true|| /*What could be the syntax here??*/)
}
match /posts/{postid} {
allow read: if publicOrSubscribed();
allow create: if request.auth.uid!=null;
allow update, delete: if request.auth.uid==resource.data.userid;
}
}
}
Any suggestions? If it is not possible in security rules, what might be the workaround? Thanks in advance!

Related

Can a user read a collection of users in firestore from frontend?

I am saving the below Data in the user's collection in firebase
{
"uid":"randomid",
"name":"name",
"number":"1234"
}
when I try to check if the user exists the below code works ok
const result = await firestore().collection('users').where('uid', '==', userid).get()
so can an authenticated user read the whole users' collections?
const result = await firestore().collection('users').get()
What security rules I can write to prevent users from reading a collection but only reading their info based on uid?
In security rules you can split the read access to get and list. So if you want the give access to each user to get only his own data you need to use the following rule (I assume each user document in the collection is the uid of this user):
match /users/{user} {
function isUserOwner() {
return request.auth.uid == user
}
allow get: if isUserOwner();
allow list: if false;
}
First you need to set the uid field to the UID of the user who created the document.
To get the current user id See documentation
const uid = user.uid;
To add the currently logged in User id as a field visit stack overflow example link for javascript
After adding UID you can use request.auth and resource.data variables to restrict read and write access for each document to the respective users. Consider a database that contains a collection of story documents. Have a look at below example
{
title: "A Great Story",
content: "Once upon a time...",
author: "some_auth_id",
published: false
}
You can use below security rule to restrict read and write access for each story to its author:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{storyid} {
// Only the authenticated user who authored the document can read or write
allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
}
}
}
Note that the below query will fail for the above rule even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database
// This query will fail
db.collection("stories").get()
The appropriate query for the above rule is
// This query will work
var user = firebase.auth().currentUser;
db.collection("stories").where("author", "==", user.uid).get()
For additional information on the above rules and query see official documentation

Firebase fetches role-based documents

If I have a collection in firebase with several documents inside and each document has a field called for example "role", is it possible to show a user only the documents that belong to him when the whole collection is fetched?
Yes. You can do this.
Let's assume the user has the role 'student', you can fetch all documents .where('role', isEqualTo: 'student').
NB: role could also be the userId (and you can fetch all documents where role is userId).
You can use the following firebase rules to prevent unauthorised
access.
match /document/{docs=**} {
allow write, read: if isAllowed();
}
function isAllowed() {
// request.resource.data is the data being fetched.
return request.resource.data.role == 'student';
}
function userRole() {
// returns the user role. Use this if the user role is stored in a document.
return get(/databases/$(database)/documents/users/$(userId)).data.role;
}
NB: your query must contain .where('role', isEqualTo: 'student') if not it will fail with insufficient permissions.

Firestore Security rules without Authentification

I store data in a Cloud Firestore database. Users in my app donĀ“t need to create an account to get data and they can also write data without to login.
Google reminds me every few days that my database is insecure and can be abused by anyone. How can I improve it without accessing Auth variables?
My firebase rules
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
Is there a way to make my database more secure without using authentication?
The logic of my app:
My database contains surnames and their origin. If someone enters a name, he gets the origin back from the database. Example: "Doe" -> "Mexican". If the last name does not exist in my database, I call an API and save the value to my database. Every user needs both read and write permission.
What can I do here?
Since the operation that you require writes for is limited (only inserting new items) you have some options:
You could deny writes to end user clients, and instead send a request to a cloud function that does exactly the operation you need (after verifying the input, or any other checks you might want, rate limiting, etc). Cloud functions ignore the security rules as they run with administrative access.
Here is a sample node function that performs a write to a realtime database, and it succeeds when both read and write are false in the security rules (your associated package.json obviously needs to depend on firebase-admin and firebase-functions):
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
let db = admin.firestore();
// This pushes the "text" parameter into the RDB path /messages/(hash)/original
exports.addMessage = functions.https.onRequest(async (req, res) => {
// Grab the text parameter.
const original = req.query.text;
// Push the new message into the Realtime Database using the Firebase Admin SDK.
const snapshot = await admin.database().ref('/messages').push({original: original});
// Respond to the user (could also be a redirect).
res.send('got it: ' + snapshot.ref.toString());
});
You may want to read about how the firebase admin SDK does access control but within a cloud function you should have admin rights by default.
Using the rules language you could only allow create operations. This removes the ability of the client to update or delete existing data. This isn't quite as secure as the prior method, but might be ok for you:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow create;
}
}
}
Also, note this works for firestore (which you are using) but not for realtime database.
Obviously both of these methods could be in some way abused to write lots of data into your database, though the former gives you a lot more control about what is allowed (e.g. you could prevent more than N entries, or more than Y bytes per entry). The later still lets anyone create whatever they want.
The first thing is to start with the documentation. It's strongly recommended that you have an understanding of what rules can do, and translate that into requirements for your app.
What you're describing for your app right now is too vague to come up with good rules. To be honest, without Firebase Authentication, it's not possible to accept writes to a database without Authentication and also avoid abuse, since anyone could write anything from anywhere on the internet. This could also cost you large amounts of money if someone discovers your "open" database.
Check out this documentation. https://firebase.google.com/docs/firestore/security/rules-structure. Configure the writing of unauthenticated users only in the collections you specify.
service cloud.firestore {
match /databases/{database}/documents {
// authentication required
function issignedin() {
return request.auth != null;
}
// authentication not required
function notAuthenticated() {
return request.auth == null;
}
// A read rule can be divided into get and list rules
match /cities/{city} {
// Applies to single document read requests
allow get: if notAuthenticated();
// Applies to queries and collection read requests
allow list: if notAuthenticated();
}
// A write rule can be divided into create, update, and delete rules
match /cities/{city} {
// Applies to writes to nonexistent documents
allow create: if notAuthenticated();
// Applies to writes to existing documents
allow update: if notAuthenticated();
// Applies to delete operations
allow delete: if notAuthenticated();
}
}
}
as a consideration, this will be insecure if the calling API allows indiscriminate writing.
Note: If the API you are referring to is the only one you can write, you must configure only the reading as public
service cloud.firestore {
match /databases/{database}/documents {
// authentication required
function issignedin() {
return request.auth != null;
}
// authentication not required
function notAuthenticated() {
return request.auth == null;
}
// A read rule can be divided into get and list rules
match /cities/{city} {
// Applies to single document read requests
allow get: if notAuthenticated();
// Applies to queries and collection read requests
allow list: if notAuthenticated();
}
// A write rule can be divided into create, update, and delete rules
match /cities/{city} {
// Applies to writes to nonexistent documents
allow create: if issignedin();
// Applies to writes to existing documents
allow update: if issignedin();
// Applies to delete operations
allow delete: if issignedin();
}
}
}

How do you keep your custom claims in sync with roles stored in Firebase database

This question is lifted from a comment on another thread and I figued that it was a good question that yet didn't have any answer on SO.
How would i link my users in auth, to their specific profiles in the database? (profile contains, full name, usertype, etc.)
The context of the question is referring to the different strategies of storing user auth access logic in the database and/or as custom claims. And how do you maintain those two to be in sync (Is there any function in User Authentication for User Roles?).
Custom claims must be set with the admin SDK, which already hints that cloud functions can be utilized to set claims according to your logic.
If the user profile data contains the user type and roles, you can create a trigger for updates on the user object. If you store your roles as an array you can implement a function like this.
functions.firestore.document('users/{uid}').onUpdate((change, context) => {
const uid = context.params.uid;
const userBefore = change.before.data();
const userAfter = change.after.data();
const hasDifferentNumberOfRoles = userBefore.roles.length !== userAfter.roles.length;
const hasOtherRoles = userBefore.roles.some((role: string) => userAfter.roles.indexOf(role) === -1);
if (hasDifferentNumberOfRoles || hasOtherRoles) {
return admin.auth().setCustomUserClaims(uid, { appRoles: userAfter.roles});
}
return null;
});
This ensures that every time the user roles in data base change, your custom claims auth will also change.
Arrays are convenient for roles since it's easy to query Firestore to find users with certain roles using array-contains and it's also easy to check roles in your database rules as a list.
service cloud.firestore {
match /databases/{database}/documents {
match /protected-content/{documentId} {
allow read: if hasUserRole('admin');
allow write: if hasUserRole('super-admin');
}
function hasUserRole(role) {
return role in request.auth.token.appRoles;
}
}
}
A small side-note about storing your roles in a appRoles attribute on the custom claims, is that it makes in conveniently easy to remove all current roles if you change your structure or want to replace all roles for a user.

How to get documents which is readable by user on Firestore Cloud depends on rules

I have a basic rule on my Firestore Cloud database. I want to give user cafes that user is in but i am always getting the error "Missing or insufficient permissions.". What is the thing that I miss?
service cloud.firestore {
match /databases/{db}/documents {
match /cafes{
allow read: if request.auth.uid != null;
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
}
}
}
here is the code that i use to reach to documents
export class MyApp {
private rootPage:any;
// rootPage:any = HomePage;
constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, afAuth: AngularFireAuth, afs: AngularFirestore) {
platform.ready().then(() => {
statusBar.styleDefault();
splashScreen.hide();
afAuth.auth.onIdTokenChanged(user=>{
if(user){
console.log(user.uid)
afs.collection('cafes').valueChanges().subscribe(console.log)
} else {
console.log('logged out')
}
})
});
}
}
One of the key sentences from the docs on security rules is
Every database request from a Cloud Firestore mobile/web client
library is evaluated against your security rules before reading or
writing any data.
and this should answer your question: You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document.
If you read this rule
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
as "Allow every user to view a cafe document if it contains his user id in the participants list" it becomes clear that Firestore needs to read the document to answer the question.
What you can try is allow read on all cafe documents and then query for those "the user is in" with the implication that users can read the other cafes as well.
You should take a look at this official guide for a concept on role based access on Firestore documents.
Update
I want to be more precise on the statement "You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document."
You cannot query for all documents and expect to get those visible to the requesting user (like you do in afs.collection('cafes'), i. e. security rules are not filters.
What you can do is qualify (add a where clause) your query to only include the documents visible to the user. Firestore will match the qualified query against your security rules and if both match, fetch the desired results.

Resources