Firebase delete method with insufficient permissions - firebase

I have the following rules for a Firestore database:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{user} {
allow create, update: if request.auth != null;
allow read, delete, list: if request.auth.token.email == resource.data.email;
}
}
}
In my app I can read the data by using getDocs(), but even though the same security is on the delete function, the following does not work:
const removeUser = async(number) => {
console.log(number);
console.log(auth.currentUser?.email || "No User");
const usersRef = collection(db, 'users');
const qU = query(usersRef, where('number', '==', number));
const usersQuerySnapshot = await getDocs(qU);
console.log(usersQuerySnapshot.docs.map(d => d.data().email);
usersQuerySnapshot.forEach((user) => {
deleteDoc(doc);
});
};
After some debugging, I know that deleting the deleteDoc() lines still gives me an insufficient permissions error.
This function is called from another function, update(). This function also calls a getUsersOfTeacher() (a teacher has its own users) which uses the same code as removeUser() but without the deleting part. This function works with no errors.
Does anyone know what's going on here?

Firebase's security rules do not filter the data. Instead they merely ensure that the code doesn't request more data than it's authorized for.
Since your rules require that the user's email address is in the email field of the document, your query must do the same. So something like:
query(
usersRef,
where('number', '==', number),
where('email', '==', auth.currentUser?.email) // 👈
);

Related

How to use Firestore Security Rules to let users only access their own resources

I'm pretty new to Firebase and am trying to make a simple todo list app with different users (Mainly to get the hang of Firestore and security rules).
I've created two accounts with two different emails, as follows:
And this is picture of one of the documents with userID field to identify the owner. (Notice it matches one of the Auth users.
I am trying to create security rules so that each user can only view their todos by checking request.auth.uid and the todo.userID as per the following screenshot:
The write rule is working, I tested it with different UIDs and it properly rejected the request. However, I keep getting permission-denied when trying to read the document. I tried so many different things, went through the documentation but still could not resolve this. Any idea on how to resolve this?
Thank you in advance
RESOLVED
Code before solution
const collectionRef = collection(db, "todos");
export function subscribeToDatebase(setTodos, setErr, uid) {
function snapshot(snapshot) {
const todosCollection = snapshot.docs;
const todos = [];
todosCollection.forEach((t, i) =>
todos.push({
firebaseID: t.id,
...t.data(),
timeStamp: t.data().timeStamp,
})
);
setTodos(todos);
}
function error(err) {
setErr(err.code);
}
const unSub = onSnapshot(collectionRef, snapshot, error);
return unSub;
}
Code after solution
const collectionRef = collection(db, "todos");
export function subscribeToDatebase(setTodos, setErr, uid) {
function snapshot(snapshot) {
const todosCollection = snapshot.docs;
const todos = [];
todosCollection.forEach((t, i) =>
todos.push({
firebaseID: t.id,
...t.data(),
timeStamp: t.data().timeStamp,
})
);
setTodos(todos);
}
function error(err) {
setErr(err.code);
}
// This is the missing line
const q = query(collectionRef, where("userID", "==", uid));
// Changing the reference to the query
const unSub = onSnapshot(q, snapshot, error);
return unSub;
}
Firebase security rules don't filter data, but merely make sure that the app is only trying to read data it is allowed to read.
It's hard to be certain without seeing the code that fails, but my educated guess is that you're not actually querying for uid in the read operations.
So to match with your rules, you need a query that only tries to read the user's own data, like this (in the v8 JavaScript syntax):
todosCollectionRef.where('userID', '==', firebase.auth.currentUser.uid)
Also see the documentation on query based rules

Cloud Functions, deleting Firestore SubCollections, is AdminToken necessary?

I am trying to build callable cloud functions, when users delete a post, it also try to delete the comments, which is a sub-collection of the post. so I saw the example and implemented just like a documentation example
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
serviceAccountId: 'xxxxxx-xxxxx#appspot.gserviceaccount.com'
}
);
exports.mintAdminToken = functions.https.onCall(async (data: any, context: any) => {
const uid = data.uid;
const token = await admin
.auth()
.createCustomToken(uid, { admin: true });
return { token };
});
exports.recursiveDelete = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall(async (data: any, context: any) => {
// Only allow admin users to execute this function.
if (!(context.auth && context.auth.token && context.auth.token.admin)) {
throw new functions.https.HttpsError(
'permission-denied',
'Must be an administrative user to initiate delete.'
);
}
const path = data.path;
console.log(
`User ${context.auth.uid} has requested to delete path ${path}`
);
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and I succeeded in receiving the custom token to the client. but what I have to do now? after getting token I called the "recursiveDelete" function from client but it occurs error PERMISSION_DENIED
Should the user who received the token be initialized with a new custom admin token? (if I misunderstand let me know)
Is the admin token really necessary when deleting a sub collection like this? It's difficult to use, so I ask.
I don't think that you really need a custom token for this use case and I suggest that you use firebase firestore rules rather than implementing your own role based authentication.
Steps to follow:
1- create a collection that you may call "users" and include in it a field of the role that this user may have such as "ADMIN". every document id in this collection can be the auth uid of users that firebase auth generates. you can get this uid from your frontend by using the currentUser prop and it's all explained here
2- protect your database with firestore rules as such:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// only admins can remove posts
match /posts/{postID} {
allow read, write: if isAdmin();
}
// only admins can remove comments
match /comments/{commentID} {
allow read, write: if isAdmin();
}
// this function will check if the caller has an admin role and allow or disallow the task upon that
function isAdmin() {
return get(/databases/$(database)/documents/
users/$(request.auth.uid)).data.role == "ADMIN";
}
}
}
3- after you succefully deletes a post document you can create a function with onDelete trigger that get invoked and deletes the comments subcollection recursivley and to do that you should include this bit of code:
const client = require('firebase-tools');
exports.recursiveDelete = functions.firestore
.document('posts/{postID}')
.onDelete((snap, context) => {
.....
await client.firestore
.delete(collectionPath, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true
});
}

Firebase Auth rule read if exists in subcollection not working

I am attempting to read a clan only if the users uid is a document in the subcollection members but i cant seem to get it to work just more permission errors.
match /clans/{clanName} {
allow create;
allow read: if exists(/databases/$(database)/documents/clans/{clanName}/members/$(request.auth.uid))
match /members/{uid} {
allow create,write,delete: if uid == request.auth.uid;
}
}
The code that is calling this is using a reference stored in another document
this.getProfile
.data()
.clan.get());
which equates to
firebase
.firestore()
.collection("clans")
.doc(clanName)
.get()
leaveClan() {
console.log(this.getUser.displayName + " leaving " + this.getProfile);
console.log(this.getProfile.data().clan.get().collection("members")
.doc(this.getUser.uid));
this.getProfile
.clan
.get()
.collection("members")
.doc(this.getUser.uid)
.delete()
.then(() => {
firebase
.firestore()
.collection("profiles")
.doc(this.getUser.uid)
.update({
clan: null
});
});
},
The way your embedding {clanName} in the get() call is incorrect. Variables are all in $(variablename) format, similar to how you already have $(database) and $(request.auth.uid).
So something like:
if exists(/databases/$(database)/documents/clans/$(clanName)/members/$(request.auth.uid))
Also see:
The Firebase documentation on accessing other documents in security rules.

Firebase Rules are not working. Returning data even when I set up rules in Firestore

I set up Firebase Functions to make calls to my Firestore. I'm using admin.auth() and returning data. I set up custom rules in the Firestore Rules section, but the Functions are not following the Rules i.e. when I use the URL in Postman, I shouldn't be getting the data because it doesn't fulfill "if read, write: if request.auth != null". How do I address this?
Here is my Firebase Function Code:
const admin = require('firebase-admin');
module.exports = function(req, res) {
const uid = req.body.uid;
admin
.auth()
.getUser(uid)
.then(user => {
admin
.firestore()
.collection('discover')
.get()
.then(snapshot => {
res.send(
snapshot.docs.map(doc => {
const data = Object.assign({ doc_id: doc.id }, doc.data());
return data;
})
);
})
.catch(err => {
res.send({ message: 'Something went wrong!', success: false });
});
})
.catch(err => {
res.send({ error: 'Something went wrong!', success: false });
});
};
Firestore Rules:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{users} {
allow read: if request.auth != null;
}
match /discover/{discover} {
allow read: if request.auth != null;
}
match /favorites/{favorite} {
allow read, write: if request.auth != null;
}
}
}
I shouldn't be able to get this data from Postman (Since I'm not authenticated) but I'm still getting the data. I don't want the data to be accessible if the user is not logged in.
Security rules don't apply when you access the database via the Admin SDK, or any other time you use a service account. It doesn't matter at all that you're using postman (or any other HTTP client). The thing actually doing the database access here is the Admin SDK.

Firestore permission denied when using signInWithCredential(), React Native Expo

firebase.initializeApp(config);
const db = firebase.firestore();
const googleSignIn = async () => {
return await Expo.Google.logInAsync({
androidClientId,
iosClientId,
scopes: ['profile', 'email'],
});
};
const firebaseLogin = async (accessToken) => {
const cred = firebase.auth.GoogleAuthProvider.credential(null, accessToken);
await firebase.auth().signInWithCredential(cred).catch(console.error);
const idToken = await firebase.auth().currentUser.getIdToken(true).catch(console.error);
};
await firebaseLogin(googleSignIn().accessToken);
db.collection("any").doc().set({test: "OK"})
I get a permission denied error when trying to write to Firestore using a request.auth.uid != null; security rule, but when I replace it with true it works.
It seems that the Firestore component of the web SDK does not send authentication details, even though the API on the client reports Firebase is logged in, and the user last login date appears in the web GUI.
Do I need to pass authentication details to the Firestore component when logging in directly with Google (instead of using the Firebase login APIs)?
The code is running in a React Native app via Expo.
Another example that gets a permission denied:
firebase.auth().onAuthStateChanged((user) => {
if (user) {
firebase.firestore().collection("any").doc().set({test: "OK"});
}
});
Rules
// This is OK:
service cloud.firestore {
match /databases/{database}/documents {
match /any/{doc} {
allow read, write: if true;
}
}
}
// Permission denied
service cloud.firestore {
match /databases/{database}/documents {
match /any/{doc} {
allow read, write: if request.auth.uid != null;
}
}
}
Related
Firebase Firestore missing or insufficient permissions using Expo (React Native)
https://forums.expo.io/t/firestore-with-firebase-auth-permissions-to-read-write-only-to-signed-in-users/5705
This solution, and possibly this whole issue, may be specific to React Native.
In a similar question, Jade Koskela noticed that requests were missing the Origin header, and applies a patch to React's XHRInterceptor to work around the missing auth object:
const xhrInterceptor = require('react-native/Libraries/Network/XHRInterceptor');
xhrInterceptor.setSendCallback((data, xhr) => {
if(xhr._method === 'POST') {
// WHATWG specifies opaque origin as anything other than a uri tuple. It serializes to the string 'null'.
// https://html.spec.whatwg.org/multipage/origin.html
xhr.setRequestHeader('Origin', 'null');
}
});
xhrInterceptor.enableInterception();

Resources