Missing or Insufficient permissions on Firestore Security Rule with get() - firebase

Pretty much no matter what I use for the get() request, getting Missing or insufficent permissions when logged in with a userID that is a "member":
function isSelf(userID) {
return request.auth != null && request.auth.uid != null && request.auth.uid == userID
}
function isMember(userID) {
return request.auth != null && request.auth.uid != null && get(/databases/$(database)/documents/'members'/$(request.auth.uid)).data.parent == userID
}
match /templates/{userID} {
allow read, write: if false
match /templates/{templateID} {
allow read: if isSelf(userID) || isMember(userID)
allow write: if isSelf(userID)
allow delete: if false
}
allow read: if isSelf(userID) || isMember(userID)
allow write: if isSelf(userID)
}
Have tried using get() with .data.parent and with .parent The member doc looks like this:
{
parent: 'USER_ID_OF_PARENT'
}
Call from the client app is:
export const getTemplate = async ({ userID, form }) => {
db.collection('templates').doc(userID).collection('templates').doc(form).get()
.then((doc) => {
})
.catch((err) => {
console.error(err)
})
}
Database structure is:
/templates/{userID} is a collection of docs with ids as userIDs that correspond to a doc with matching userID in /users/{userID}
/members/{memberID} is a collection of docs with ids as memberIDs, with a parent field with a string value SOME_USER_ID which matches a doc with userID SOME_USER_ID in /users/{userID}
Example:
/members/'MEMBER_1' doc:
{
name: 'Member 1',
parent: 'OWNING_USER_1'
}
/users/'OWNING_USER_1' doc:
{
name: 'Owning User 1',
parent: 'OWNING_USER_1'
}
/templates/'OWNING_USER_1' doc:
{
// no fields
}
/templates/'OWNING_USER_1'/templates/'FORM_1' doc:
{
name: 'Form 1'
}
With the following call:
getTemplate({
userID: 'OWNING_USER_1',
form: 'FORM_1'
})
When the authenticated user is OWNING_USER_1, the above call is successful (the isSelf() rule returns as true) and the found template document is returned
When the authenticated user is MEMBER_1, the above call gets Missing or insufficient permissions (the isMember() rule returns false)

Removed the quotes from around 'members' and this is now working correctly:
Replaced:
get(/databases/$(database)/documents/'members'/$(request.auth.uid)).data.parent
with:
get(/databases/$(database)/documents/members/$(request.auth.uid)).data.parent

Related

firestore security rules: allow logged in user to update field

I have the following firestore security rules
match /users/{user} {
allow read: if request.auth != null;
allow create: if request.resource.id == request.auth.uid;
match /replies {
allow update: if request.auth != null;
}
}
my understanding from the firestore security rules doc is that the two rules are independent of each other. However, a logged in user gets a permission denied message when trying to update the /user/{user}/replies field. It doesn't matter if I nest the rule or not. It still gets denied. (it also doesn't work in Rules Playground in Firebase console)
what am I doing wrong?
my client code is as follows:
const processNewMessage = async (evt, newMessage) => {
myMessage.value = myMessage.value.trim();
if (evt.srcElement.textContent === "send") {
if (replying.value === true) {
const message = {
originalMessage: {
text: props.selectedMessage.text,
photoUrl: props.selectedMessage.photoUrl,
},
reply: {
user: uid,
userDisplayName: auth.currentUser.displayName,
userName: userName.value,
text: newMessage,
createdAt: Timestamp.now(),
g: { geohash: geohash.value, geopoint: myLocation },
photoUrl: photoUrl,
},
};
await updateDoc(doc(db, `users/${props.selectedMessage.user}`), {
replies: arrayUnion(message),
});
}
}
}
Security rules work on a document-level. Your code is trying to update the replies array in the users document, which is not allowed by any of your rules.
The match /replies in your rules applies to a replies subcollection under the users document. It has no effect on fields in the users document.
If you want the owner to update their entire document, and other signed in users to only update the replies field, you'll have to allow that in the rule on users.
Something like this:
match /users/{user} {
allow read: if request.auth != null;
allow create: if request.resource.id == request.auth.uid;
allow update: if request.auth != null &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["replies"])
;
}
For more on how this works, see the release notes for the map diff function.

Firestore FirebaseError: Missing or insufficient permissions

I'm having a problem with my Firestore security rules. I have the following rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isOwner() {
return get(resource.data.owner).id == request.auth.uid;
}
match /projects/{project} {
function isMember() {
return /databases/$(database)/documents/users/$(request.auth.uid) in get(/databases/$(database)/documents/projects/$(project)).data.flatMembers;
}
allow create: if request.auth != null;
allow read: if isOwner() || isMember();
allow write: if isOwner();
match /sprints/{sprint} {
allow read: if request.auth != null;
allow create, write: if request.resource.data.startAt < request.resource.data.endAt;
}
match /backlog/{task} {
allow read, create: if request.auth != null;
allow write: if isMember();
}
}
match /users/{user} {
allow read, write: if request.auth != null;
allow create;
}
}
}
They seem to work in the Firestore rules playground. They succeed when I expect them to, but in my application I get the following error:
ERROR FirebaseError: Missing or insufficient permissions.
at new e (prebuilt-ac190846-5fb4dac2.js:188)
at prebuilt-ac190846-5fb4dac2.js:10430
at prebuilt-ac190846-5fb4dac2.js:10431
at e.onMessage (prebuilt-ac190846-5fb4dac2.js:10453)
at prebuilt-ac190846-5fb4dac2.js:10370
at prebuilt-ac190846-5fb4dac2.js:10401
at prebuilt-ac190846-5fb4dac2.js:15132
at ZoneDelegate.invoke (zone-evergreen.js:372)
at Zone.run (zone-evergreen.js:134)
at zone-evergreen.js:1276
My question basically is: is there anything wrong with my rules? Why do they succeed in the playground? How can I debug these rules? Currently it is a pain in the ass.
The code that is failing because of permission errors is the following:
return this.firestore.collection<Project>('projects', query => {
return query
.where('members', 'array-contains-any', [
{ user: this.authService.getCurrentUserRef(user), role: MemberRole.OWNER },
{ user: this.authService.getCurrentUserRef(user), role: MemberRole.DEVELOPER },
{ user: this.authService.getCurrentUserRef(user), role: MemberRole.CLIENT }
])
.where('archived', '==', archived);
})
.snapshotChanges()
.pipe(mergeMap(projects => {
return combineLatest(projects.map(project => {
const data = project.payload.doc.data();
return this.userService.one(project.payload.doc.data().owner.id).pipe(map(owner => ({
id: project.payload.doc.id,
name: data.name,
owner: owner?.displayName ?? 'Unknown',
ownerId: owner.uid,
description: data.description,
status: data.status,
archived: data.archived
})));
}));
}));
PS: I can edit a single project view in my application, just not the all projects view even though I have where filters which check if you are a member/owner of a project.

How to grant collection access to user based in other collection?

Let's assume that there is 3 collections and they are at the same hierarchy level:
User
UserAndOtherCollectionRelationship
OtherCollection
I desire to grant access on "OtherCollection" records to the users that own that record or are related to it (only read access).
Understand "UserAndOtherCollectionRelationship" as
UserAndOtherCollectionRelationship: {
'userId': uid, //user id provided by Firebase Auth Service
'otherCollectionId': 000,
'roles': ['owner', 'reader', ...]
}
This is what I have:
match /databases/{database}/documents {
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
match /user/{userId} {
allow read, write: if(isSignedIn() && isUserOwner(userId));
}
match /userAndOtherCollectionRelationship/{userAndOtherCollectionRelationshipId} {
allow read: if(resource.data.userId == request.auth.uid && isSignedIn());
allow create: if(isSignedIn());
allow update, delete: if(resource.data.userId == request.auth.uid);
}
// Functions
function isSignedIn() {
return request.auth != null;
}
function isUserOwner(userId) {
return request.auth.uid == userId;
}
function isOtherCollectionOwner() {
return isUserOwner(getUserAndOtherCollectionRelationshipData().userId) && getOtherCollectionData().roles.hasAll(['owner']);
}
//This is the function that I believe that it's not working propertly
function getuserAndOtherCollectionRelationshipData() {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/{document=**}).data;
}
}
Considering that the client (the app) must create a filter (where clause) to get only the desired records, I could not find a way to do that with this schema too.
So I put the user roles as a field on the "otherCollection" record:
otherCollection: {
...,
'userAndRoles': {
'replaceByUID': ['owner', ...]
},
}
updated the security rule function to:
function isOtherCollectionOwner() {
return get(/databases/$(database)/documents/OtherCollection/$(otherCollectionId)).data.roles[request.auth.uid].hasAll(['owner']);
}
Here is the client call:
final querySnapshot = await firestore.collection('otherCollection')
.where('user.$userId', arrayContains: 'owner')
.where('otherCollectionId', whereIn: otherCollectionIdList)
.get();
What is the best solution?
Change the data model to...
Set a different security rule as...
When a user wants to access an another collection, we have to set a rule under that collection.
So when you create a document in a collection, you have to create the same id in the other collection and create a field called owner.
This field owner contains the uid of the person who created the document.
userAndOtherCollectionRelationshipId === otherCollectionId
UserAndOtherCollectionRelationship: {
'owner': uid
}
In this way, when a user try to read the document, we check if he is owner or not with isOwner(otherCollectionId, request.auth.uid) function with Collectionid, and the userId. In the function you check if the owner who created the document is the same who is trying to read the document.
You can do create a rule as following:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if isOwner(otherCollectionId, request.auth.uid);
allow create: if isSignedIn();
}
function isOwner(docId, userId) {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/$(docId)).data.owner == userId;
}
To solve the issue, I updated the data model removing the userAndOtherCollectionRelationship collection and add the owner attribute to the otherCollection.
Any other relationship would be added as an attribute to otherCollection.
So the otherCollection looks like this now:
otherCollection: {
owner: ["user_uid", "other_user_id"],
..., //other atributes
}
The security rules were updated to:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
function isOtherCollectionOwner() {
return ([request.auth.uid] in (resource.data.owner));
}
The security rules tests were updated to:
const myAuth = {uid: 'my_user_uid', email: 'my#mail.com'};
const MY_PROJECT_ID = "my_project_id";
function getAdminFirestore() {
return firebase.initializeAdminApp({projectId: MY_PROJECT_ID, auth: myAuth}).firestore();
}
function getFirestore(auth) {
return firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth}).firestore();
}
describe("MyApp", () => {
it("Can list if is owner", async () => {
const admin = getAdminFirestore();
const setupOtherCollection = admin.collection('otherCollection').doc('otherCollectionId');
await setupOtherCollection.set({'name': 'myOtherCollection', 'owner': [myAuth.uid]});
const db = getFirestore(myAuth);
const otherCollectionCollection = db.collection("otherCollection").where("owner", "array-contains", [myAuth.uid]);
await firebase.assertSucceeds(otherCollectionCollection.get());
});
});

Firestore security rule: Null value error when using Set.hasAny()

I have a user collection with two fields memberOf and managerOf (that is, of an organisation; both are arrays of doc id).
I would like to restrict a manager to list only users that are members of an organisation they managed.
In JS, it would be something like this:
const memberOf = [1, 2, 3, 4, 5]
const managerOf = [6, 7, 1, 9, 0]
console.log(memberOf.some(el => managerOf.includes(el))) // 👈 returns true
This is what I have so far:
function isSignedIn() {
return request.auth.uid != null
}
function isAdmin() {
return isSignedIn() && 'admin' in request.auth.token && request.auth.token.admin
}
match /users/{userId} {
allow get: if isSignedIn() && (request.auth.uid == userId || isAdmin());
allow list: if isAdmin() || ???; // 👈 how can I express the above condition?
allow write: if isAdmin();
}
And that's the query:
const unsubscribe = db.collection('users')
.where('memberOf', 'array-contains', organisationId)
.orderBy('email', 'asc')
.onSnapshot(snap => {
console.log(`Received query snapshot of size ${snap.size}`)
var docs = []
snap.forEach(doc => docs.push({ ...doc.data(), id: doc.id }))
actions.setMembers(docs)
}, error => console.error(error))
First, I wanted to use the organisationId from the request in the security rule, but it's not available as it's not a write operation (https://firebase.google.com/docs/reference/rules/rules.firestore.Request#resource)
I thought about:
function hasMemberManagerRelationship(userId) {
return isSignedIn() && get(/databases/$(database)/documents/users/$(userId)).data.memberOf in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.managerOf
}
match /users/{userId} {
allow get: if isSignedIn() && (request.auth.uid == userId || isAdmin());
allow list: if isAdmin() || hasMemberManagerRelationship(userId);
allow write: if isAdmin();
}
or
function hasMemberManagerRelationship(userId) {
return isSignedIn() && get(/databases/$(database)/documents/users/$(userId)).data.memberOf.toSet().hasAny(get(/databases/$(database)/documents/users/$(request.auth.uid)).data.managerOf.toSet())
}
(https://firebase.google.com/docs/reference/rules/rules.Set#hasAny)
But it's not working and I have the error FirebaseError: Null value error. for 'list' # L27. AND on top of that, that could generate a lot of extra read operations (not billing-wise optimised).
I could do something like the following:
allow list: if isAdmin() || (isManagerOf('jJXLKq7p9wWSNLsHcVIn') && 'jJXLKq7p9wWSNLsHcVIn' in resource.data.memberOf);
where jJXLKq7p9wWSNLsHcVIn is the id of an organisation (and used in the query), but I don't know how I can retrieve the id from the request "context"..
Any help would be appreciated!
Ok. First, thank you #Doug Stevenson for mentioning debug() in another post! I didn't know it exists, and it rocks!
The result of debug(resource.data.memberOf) in the debug log was:
constraint_value {
simple_constraints {
comparator: LIST_CONTAINS
value {
string_value: "jJXLKq7p9wWSNLsHcVIn"
}
}
}
LIST_CONTAINS forced me to have a look at List: https://firebase.google.com/docs/reference/rules/rules.List#hasAny
toSet() does not apply to a list, but a list has already the hasAny() function.
(in fact, it does exist but it didn't work in my case 🤔)
In the end, this rule works:
function hasMemberManagerRelationship() {
return isSignedIn() && resource.data.memberOf.hasAny(getUser(request.auth.uid).data.managerOf)
}
Now I'm just wondering if getUser(request.auth.uid).data.managerOf is somehow cached (1 read for multiple user entries) or re-run every time (100 users, 100 extra reads).
Any thoughts on that?
I sincerely hope this is the first case ^^
I tested rules which are pretty similar to your attempt using the firestore "Rules Playground" and it seems to be working.
You do not need to get() the current userId because you already have it in the resource object.
I am not sure it would generate a lot more read operations because we are only using get() for request.auth.uid.
function getUser(userId) {
return get(/databases/$(database)/documents/users/$(userId));
}
function isSignedIn() {
return request.auth.uid != null;
}
function isAdmin() {
return isSignedIn() && 'admin' in request.auth.token && request.auth.token.admin;
}
function hasMemberManagerRelationship() {
return isSignedIn() && resource.data.memberOf.toSet().hasAny(getUser(request.auth.uid).data.managerOf.toSet());
}
match /users/{userId} {
allow read: if isAdmin() || hasMemberManagerRelationship();
}
Where exactly are you getting the FirebaseError: Null value error?

Error: Missing or insufficient permissions

I got a firestore like this:
:stores
|
$Store
:orders
|
$Order
:items
I want to read orders from my database using a user having an workerUid same as the request.auth.uid but geht the Error: Missing or insufficient permissions.
The important part of my firebase rules:
service cloud.firestore {
match /databases/{database}/documents {
//Matches any document in the stores collection
match /stores/{store} {
function isStoreAdmin(uid) {
return get(/databases/stores/$(store)).data.adminUid == uid;
}
function isStoreWorker(uid) {
return get(/databases/stores/$(store)).data.workerUid == uid;
}
allow read: if request.auth.uid != null;
allow write: if request.auth.uid == resource.data.adminUid;
//Matches any document in the orders collection
match /orders/{document=**} {
allow read, write: if isStoreAdmin(request.auth.uid) || isStoreWorker(request.auth.uid);
}
}
}
}
Funny thing is, that it works if I do this:
match /orders/{document=**} {
allow read, write: if isStoreWorker(request.auth.uid);
}
or this:
match /orders/{document=**} {
allow read, write: if request.aut.uid != null;
}
When deploying the rules I get no syntax error so I really can't understand why this is not working. Does anyone have any ideas? Thank you so much!
Edit:
function readAllDocuments(collectionReference, callback,finishedCallback){
collectionReference.get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
callback(doc.id,doc.data());
});
finishedCallback();
});
}
const storeDocument = getRootCollection(STORES_COLLECTION_ID).doc(storeId);
const orderCollection = storeDocument.collection(STOREORDERS_COLLECTION_ID);
orders=new Map();
readAllDocuments(orderCollection, function (id, data) {
orders.set(id,data);
},function(){
finishedLoading();
});
The documentation for use of get() in a security rule states:
...the path provided must begin with /databases/$(database)/documents
Make these changes to the get() paths:
function isStoreAdmin(uid) {
return get(/databases/$(database)/documents/stores/$(store)).data.adminUid == uid;
}
function isStoreWorker(uid) {
return get(/databases/$(database)/documents/stores/$(store)).data.workerUid == uid;
}

Resources