I am unit testing the firestore security rules below which allows a member of a group to read the groups collection if their userId is a key in the roles map.
function isMemberOfGroup(userId, groupId) {
return userId in get(/databases/$(database)/documents/groups/$(groupId)).data.roles.keys();
}
match /groups/{groupId} {
allow read: if isMemberOfGroup(request.auth.uid, groupId);
}
Sample groups collection
{
name: "Group1"
roles: {
uid: "member"
}
}
My unit test performs a get() of a single document which succeeds.
It then performs a get() using a where() query which fails.
it("[groups] any member of a group can read", async () => {
const admin = adminApp({ uid: "admin" });
const alice = authedApp({ uid: "alice" });
// Groups collection initialisation
await firebase.assertSucceeds(
admin.collection("groups")
.doc("group1")
.set({ name: "Group1", roles: { alice: "member" } })
);
// This succeeds
await firebase.assertSucceeds(
alice.collection("groups")
.doc("group1")
.get()
);
// This fails
await firebase.assertSucceeds(
alice.collection("groups")
.where(new firebase.firestore.FieldPath('roles', 'alice'), '==', "member")
.get()
);
});
It fails with the error:
FirebaseError:
Null value error. for 'list' # L30
where L30 points to the allow read security rule
I know that firestore rules are not responsible for filtering and will deny any request that could contain documents outside of the rule.
However from what I understand, my where() should be limiting this correctly.
Is there a problem with my where() query or my rules?
The problem is still that security rules are not filters. Since your rule depends on a specific document ID (in your case, groupId), that value can never be used in the rule as part of a comparison. Since rules are not filters, the rule must work for any possible value of groupId without knowing it an advance, since the rule will not get() and check each individual group document.
The security check you have now will simply not work for queries for this reason. Instead, consider storing a list of per-user group access in custom claims or a single document that the user can read, so they can then iterate the list and get() only the specific groups they've been told they have access to.
Related
I have the following collection group query:
const userInRooms = await firestore()
.collectionGroup('userRooms')
.where('uid', '==', authenticatedUser.uid)
.get();
And it works fine.
But since I added security rule:
match /rooms/{docId} {
allow read;
allow write;
match /userRooms/{docId} {
allow read;
allow write;
}
}
userRooms is subcollection in rooms.
It stopped working and I getting:
NativeFirebaseError: [firestore/permission-denied] The caller does not have permission to execute the specified operation.
Cascading the sub-collections rules that way doesn't work for collection group queries. A recursive wildcard must be present at the beginning of the path so it'll match any collections with that name. Try:
match /rooms/{docId} {
//...
}
match /{path=**}/userRooms/{docId} {
allow read, write: if true;
}
Do change the rules as required instead of allowing everyone to read the database (unless they are allowed to).
Some reason, I just am not getting it.
I want to add a new document to a sub-collection. Here is my layout as follows:
Users----------- Collection
UID----------- Document
Lists------- Collection
Category-- Document
Category-- Document
...--
For the documents in the "Lists", I want to add a Doc to Lists.
The Doc is custom named.
I've tried the following:
async function AddCategory (category) {
return new Promise((resolve, reject) => {
const uid = auth.currentUser.uid
console.log(`UID: ${uid}`)
setDoc(doc(db, 'users', uid, 'lists', category), {
name: 'Johnny Doey'
}).then((res) => {
resolve(res)
}).catch((err) => {
reject(err)
})
})
}
This does not seem to work. The error I am receiving is 'Undefined'.
I almost feel like there is something simple I am missing.... I've checked my auth rules. Everything checks out. Tried to test with hard strings in place of my variables, still no luck...
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/{documents=**} {
allow read, write: if request.auth != null && request.auth.uid == userId
}
}
}
now, the firestore data shows the initial user (UID) document to be italicized,
Even though non-existent ancestor documents appear in the console,
they do not appear in queries and snapshots. You must create the
document to include it in query results.
What in the heck...
Could someone please overlook this? Thanks!
According to this post:
"When you create a reference to a subcollection using a document id like this:
db.collection('coll').doc('doc').collection('subcoll')
If document id doc doesn't already exist, that's not a problem at all. In fact, after adding documents to subcoll, doc will appear in the Firebase console in italics, indicating that it doesn't exist."
What you can do is: First, create a setDoc() for collection "users" with its own document (whether auto-generated or manually coded), second, you can input your setDoc() query: setDoc(doc(db, 'users', uid, 'lists', category)....
For a better visualisation, here's a sample code:
setDoc(doc(db, 'users', uid), {
name: 'Johnny Doey'
}).then(() => {
setDoc(doc(db, 'users', uid, 'lists', category), {
// some additional inputs here...
})
}).catch((e) => {
console.log(e)
})
For additional reference, you can check, Non-existent ancestor documents.
Well - it works. I think one of the issues I was facing was altering the generated users.
My fix was to delete the users, and build it from scratch. That seemed to solve it for me. Not sure what exact constraints the generated user collection/doc has, but it seems it does have some restrictions tied into it.
app : {
users: {
"some-user-uid": {
email: "test#test.com"
username: "myname"
}
},
usernames: {
"myname": "some-user-uid"
}
}
I want to make unique usernames, like this: I would store all the usernames in a collection in firebase and than check if that doc with that username exists. This works perfectly fine but when I make a username with the same name on 2 accounts at the same time, user 1 makes the file and then user 2 overwrites that file.
How can I get around this?
This is not a copy of an asked question, all the answers answer how to make unique usernames but this bug is in all of them.
To prevent somebody being able to overwrite an existing username document, you'd use Firebase's security rules:
service cloud.firestore {
match /databases/{database}/documents {
// Allow only authenticated content owners access
match /usernames/{username} {
allow create: if !exists(/databases/$(database)/documents/usernames/$(username))
}
}
}
The above example allows creating the document if it doesn't exist yet. So with this the second write will be rejected.
You'll probably want to expand this to allow a user to only claim with their own UID, and unclaim a name too. To learn more on how to do that, I recommend reading the Firebase documentation on security rules.
I am building an app with Firebase that requires private messaging between users.
What I need is to build single chats for 1-1 chats, storing messages in Firestore.
My idea: I guess the best way is to build a single collection for each chat, with the right security rules. Let's say an user with tokenID 1234 wants to talk with user with tokenID 1111, a new collection called 1234_1111 will be created (if not existing), and the security will allow only these two users to read and write.
My question: Is it the right way? And how to do that in Javascript? I'm not sure how to define security rules directly in the JS code, neither how to create a collection with the two users ID.
Security Rules are not defined in your JavaScript code, they are defined separately. What you suggest would be a reasonable approach to take although I'd use a subcollection for it, and a simplified version of your security rules might look something like:
service cloud.firestore {
match /databases/{database}/documents {
match /dms/{idPair}/messages/{msgId} {
allow read, write: if
idPair.split('_')[0] == request.auth.uid ||
idPair.split('_')[1] == request.auth.uid;
}
}
}
Then in your JS code you could do something along the lines of:
// generate idPair
function dmCollection(uid) {
const idPair = [firebase.auth().currentUser.uid, toUid].join('_').sort();
return firebase.firestore().collection('dms').doc(idPair).collection('messages');
}
// send a DM
function sendDM(toUid, messageText) {
return dmCollection(toUid).add({
from: firebase.auth().currentUser.uid,
text: messageText,
sent: firebase.firestore.FieldValue.serverTimestamp(),
});
}
// retrieve DMs
function messagesWith(uid) {
return dmCollection(uid).orderBy('sent', 'desc').get();
}
Note that the idPair is constructed by joining a sorted pair of UIDs so that it will be stable no matter which user sends.
In firestore security rule, the resource.data is an emtpy object always, is this a bug or something ?
My firestore rules:
service cloud.firestore {
match /databases/{database}/documents {
match /hospitals/{document=**}{
// allow read :if resource.data.size() == 0; //this return true, resource.data is an empty object
allow read :if resource.data.name != null; // this doesn't work
}
}
}
My javascript:
auth().onAuthStateChanged((user) => {
if (user) {
//db is the firestore instance
db.collection('/hospitals').get()
.then(printResult)
} else {
}
})
this is my current database snapshot
solved :
thanks for Frank's answer
the issue rely on that firestore security doesn't evaluate the actual document value when we query a over multiple document , in my case
//this doesn't firestore doesnt' evaluate the documetn
db.collection('hospitals').get()
//this will work ,if you need to compare the actual value
db.document('hospitals/somehospital').get()
Security rules don't filter data by themselves. They merely enforce rules on what data a client can read. Your client is currently trying to read all hospitals. Since your security rules have restrictions on what data a client can read, they reject this operation.
You need to ensure that what your client requests is no more than what the security rules allow, by reading the data through a query that matches the security rules. So something like
db.collection('/hospitals')
.where("name", ">=", "")
.get()
.then(printResult)
Note that this does require that the document has a name field, otherwise the name can't be empty.
For more info, see:
the Firestore documentation on securing queries
Firestore select where is not null