13

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.

Anubhav
  • 7,138
  • 5
  • 21
  • 33

2 Answers2

2

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.

Jason Berryman
  • 4,760
  • 1
  • 26
  • 42
1

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.

Sam Stern
  • 24,624
  • 13
  • 93
  • 124
  • `isTeamMember()` will run for every document. But that is the intended behaviour. It only needs to access `teamid` in the requested resource. And right now we have made a conscious decision to include `teamid` in every document of every collection and subcollection Every document in every collection has `teamid`. Shouldnt the query work then? – Anubhav Mar 16 '18 at 05:04