8
votes

I have two collections - tenancies and users.

A tenancy doc has a field called "landlordID" and is of type REFERENCE (not String).

Now in my Firestore Security Rules I want to allow a tenancy to be updated ONLY IF the landlordID field of that tenancy matches with the uid of the user making the request, namely request.auth.uid.

Read it as " allow a tenancy document to be updated if the user making the user is authenticated, hence request.auth.uid != null, and the landlordID field's ID should be equal to that of the request.auth.uid.

Hence the code should me something like this:

    service cloud.firestore {

      match /databases/{database}/documents {

        match /tenancies/{tenancyID}{

            allow update: if request.auth.uid != null && 
                        request.auth.uid == get(resource.data.landlordID).id
    }

}

I have also tried get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id

Supporting screenshot of my database

enter image description here

This should be very simple but get() simply does not work. Firebase Docs, scroll to "Access other documents" was not helpful at all for my situation and I am not sure how to get it working.

It would be a shame if references can't be used like this as they are just like any other field of a document.

6
Are you showing the actual rules that aren't working the way you expect? If not, please edit the question to show exactly what you're doing. As show, I'd expect them not to compile because there is no nill. Did you mean null?Doug Stevenson
This is just a typo, on Security rulls it is "null". And yes I am showing a standalone rule that fails a simulation request that is supposed to go through. Read it as " allow a tenancy document to be updated if the user making the user is authenticated, hence request.auth.uid != null, and the landlordID field's ID should be equal to that of the request.auth.uid.John Dough
Your get() is definitely not correct. You need to pass a full document specifier to it, as you might see in the documentation.Doug Stevenson
See the second answer and my response. I have tried various versions of get with full paths etc including the version in the second answer, and it did not work. The second answer also indicates that it might not be possible to use references like this, which is a shocker to me.John Dough

6 Answers

4
votes

I see a couple of issues here. A first problem is that the get() function expects a fully specified ducument path, something like:

get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id

A second problem is that you are trying to use the reference type in your rules, I do not think that is possible unfortunately.

The reference type in Firestore is not very helpfull (yet), I think you should store the landlordID as a string, then you can simply do something like:

service cloud.firestore {

      match /databases/{database}/documents {

        match /tenancies/{tenancyID}{

            allow update: if request.auth.uid != resource.data.landlordID;

    }

}
4
votes

Here is a function I made that works for me. I guess you have a user collection with users having the same id as their auth.uid

    function isUserRef(field) {
      return field in resource.data
        && resource.data[field] == /databases/$(database)/documents/users/$(request.auth.uid)
    }

Adjusting to your use case you'd call the function so: isUserRef('landlordID') although the ID at the end of it is a bit misleading as this field is in fact a reference.

3
votes

I had the same issue I needed an answer for. See this Google-thread with the answer from someone from google. To quote it:

You can get an id out of a path using the "index" operator:

some_document_ref should look like /databases/(default)/documents/foo/bar

which has 5 segments: ["databases", "(default)", ...]

some_document_ref[4] should be "bar" allow create: if request.resource.data.some_document_ref[4] == "bar";

You can also use the normal get and exists functions on them.

A few difficult aspects of this that you may run into:

  • There's no way to retrieve the number of segments in a path at the moment (we're adding this soon), so you'll need to know some information about the reference ahead of time

  • There's not great support for writing references using the simulator in the Firebase Console. I used the Firestore emulator to test out this behavior (gist1, gist2)

2
votes

might be too late, but I was able to piece together (despite a lack of docs) that a document reference is just a path, and complete path can be created with

/databases/$(database)/documents/users/$(request.auth.uid)

Then I have an array/list in firestore of references, called reads that I can grab with:

get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads

Leaving me able to create a bool, and a rule with:

/databases/$(database)/documents/users/$(request.auth.uid) in get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads

obviously your data structure will vary, but knowing the ref is a path is the important part here.

1
votes

I had to experiment a little to get this working. Here the function that worked for me

function isUserRef(database, userId) {
  return 'user' in resource.data
    && resource.data.user == /databases/$(database)/documents/users/$(userId);
}

And I call it like:

match /answers/{answer} {
   allow read:
        if isUserRef(database, request.auth.uid);
}
0
votes

As mentioned by some other answers, a reference has a path property that is just a string that will look something like users/randomuserid123. You can split that into an array and match it against the user making the update request.

...

match /tenancies/{tenancyID}{

 allow update: if request.auth.uid != null && 
   resource.data.landlordID.path.split('/') == ['users', request.auth.uid]
 }

...