Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firestore security rule get() not working

In firestore I want a user to only access a document if the user is in the teamid mentioned in the document. Now I have a different collection called teams where I have users mapped as { user_id = true }. So I have the following in the Firestore rules

return get(/databases/$(database)/documents/teams/$(resource.data.teamid)).data.approvedMembers[request.auth.uid] == true;

Now this rule does not work and fails any request made to the database by the frontend. But when I replace $(resource.data.teamid) with my actual teamid value as follows,

return get(/databases/$(database)/documents/teams/234234jk2jk34j23).data.approvedMembers[request.auth.uid] == true;

... it works as expected.

Now my question is am I using resource in a wrong way or is resource not supported in get() or exists() queries in Firestore rules?

Edit Complete rules as follows

service cloud.firestore {
  match /databases/{database}/documents {

    function isTeamMember() {
        return get(/databases/$(database)/documents/teams/$(resource.data.teamid)).data.approvedMembers[request.auth.uid] == true;
        // return exists(/databases/$(database)/documents/teams/$(resource.data.teamid));
    }

    match /{document=**} {
        allow read, write: if isTeamMember();
    }
  }
}

If you notice the commented out rule, exists() does not work in this case either.

like image 332
Anubhav Avatar asked Mar 14 '18 10:03

Anubhav


2 Answers

Thanks for reaching out on Twitter. Following our chat, here is the conclusion:

Querying documents using your security rule

You cannot use a security rule as a filter. To get this to work, you must also add .where('teamid', '==', yourTeamId)

You have confirmed that this works for you, but you don't always want to restrict on one teamid

Using custom claims

You can set up custom claims in your authentication tokens. Here is an example of how to set these and them use them in your rules

Setting custom claim

You will need to use the Admin SDK for this.

auth = firebase.auth();

const userId = 'exampleUserId';
const customClaims = {teams: {myTeam1: true, myTeam2: true}};

return auth.setCustomUserClaims(userId, customClaims)
  .then(() => {
    console.log('Custom claim created');
    return auth.getUser(userId);
  })
  .then(userRecord => {
    console.log(`name: ${userRecord.displayName}`);
    console.log(`emailVerified: ${userRecord.emailVerified}`);
    console.log(`email: ${userRecord.email}`);
    console.log(`emailVerified: ${userRecord.emailVerified}`);
    console.log(`phoneNumber: ${userRecord.phoneNumber}`);
    console.log(`photoURL: ${userRecord.photoURL}`);
    console.log(`disabled: ${userRecord.disabled}`);
    console.log(`customClaims: ${JSON.stringify(userRecord.customClaims)}`);
  })
  .catch(err => {
    console.error(err);
  });

Apply security rule

allow read, write: if request.auth.token.teams.$(resource.data.teamId) == true;

I'm still not 100% certain that this will not also require you to filter by the teamid. Please test it and feed back.

like image 74
Jason Berryman Avatar answered Nov 04 '22 01:11

Jason Berryman


You have this match:

match /{document=**} {
    allow read, write: if isTeamMember();
}

That will match any document in the database. That means when you call isTeamMember() it's not guaranteed that resource represents a team document. If you have any subcollections on teams and write to those then resource will be the subcollection document.

like image 29
Sam Stern Avatar answered Nov 04 '22 01:11

Sam Stern