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

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.

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 Firestore security rules: Using queries in security rules?

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!

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 to protect email addresses but make them searchable with Firestore

I couldn't find anything on the topic, so here goes.
I'm creating an app with a Firebase Cloud Firestore database with users.
My goal is to "prevent people from stealing all email addresses but still make them searchable"
My user data is saved per user like so:
in /users/{userId}
{
email: 'user#gmail.com',
displayName: 'James Liverstone'
}
I can protect the user data with these rules:
match /users/{userId} {
allow write, read: if request.auth.uid == userId
&& request.auth.uid != null;
}
But what if I want to make it so someone can search for a friend in my app, by email or display name?
eg.
const searchVal = 'user#gmail.com' // search value from <input>
firebase.firestore().collection('users').where('email', '==', searchVal)
This is not possible because of the read rule. However, if I open up read to allow everyone, you could steal all email addresses of my users like so:
firebase.firestore().collection('users').get()
how can I prevent people from stealing all email addresses but still make them searchable?
So in short:
allow: firebase.firestore().collection('users').where('email', '==', searchVal)
prevent: firebase.firestore().collection('users').get()
It seems you can't enforce this with security rules, so your best best would be to write a Cloud Function (http or callable) that will perform the query safely and return the desired result to the client. This function would take the email address as an input argument and minimally output some boolean that indicates if the user exists.
There is a workaround without using Cloud Functions
One workaround for this using only firestore is to create an additional collection like so:
Every time a user is created, set an empty document with email address as the key:
const email = 'user#gmail.com' //get the email of the new user
firestore().doc(`searchUsers/${email}`).set({})
This way we have a collection called searchUsers with a bunch of empty documents with the email address as key.
Required security rules:
Prevent users from getting all these emails
with .collection('searchUsers').get()
Allow checking the existence for a single email address
with .doc('searchUsers/user#gmail.com').get()
Set the security rules like so:
match /searchUsers/{value} {
allow create: if request.auth != null
&& value == request.auth.token.email;
allow list: if false;
allow get;
}
These security rules explained:
allow create rule: "only allow users to create a doc with their own email address"
allow list rule: "Prevent users from getting all these emails"
with .collection('searchUsers').get()
allow get rule: "you can query for a single doc with the email as key to check existence"
with .doc('searchUsers/user#gmail.com').get()
In practice
You will have a search form <input> and target this to execute:
const searchVal = 'user#gmail.com' // search value from <input>
const docRef = await firestore().doc(`searchUsers/${searchVal}`).get()
const userExists = docRef.exists

Firebase rules - Simulator says yes, code says no

I'm working on a Flutter app using Firebase as a backed. I've set up group based roles in Firebase and the rules simulator in Firebase tells me the user I'm testing has access to the document. When I do a query in my Flutter code, I can see it finds the document and I can see it for a split second before it changes it mind and I get a "Listen for query at students failed: Missing or insufficient permissions." and the document is removed from the snapshot.
The query I use in the Flutter code is as follows:
Firestore.instance.collection('students').where('test', arrayContains: userID).orderBy('name').snapshots()
I have been playing with the document and tried different approaches for the current user to query for the document, and just to test it out I created an array with the userId and look for that.
If I completely skip the rules and just put the "need to be logged in" as requirement then I get a document back but as soon as I use the role based one then it's back to the drawing board. The rules I've set up are:
service cloud.firestore {
match /databases/{database}/documents {
match /students/{student} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
allow read, write : if isOneOfRoles(resource,['teacher', 'student', 'parent']);
}
}
}
Any idea what's causing this?

Resources