firebase-tools icon indicating copy to clipboard operation
firebase-tools copied to clipboard

resource.data is missing data when evaluating firestore rules in emulator

Open evelant opened this issue 2 years ago • 14 comments

[REQUIRED] Environment info

firebase-tools: 10.6.0

Platform: macOS

[REQUIRED] Test case

The following function in security rule function will return true even when the document in question definitely contains the userId field

function doesntExistOrMissingUserId(){
      return resource == null || !resource.data.keys().hasAll(['userId']);
}

[REQUIRED] Steps to reproduce

Add the above function to a read rule. Issue a query from a client for a doc that contains a userId field. The rule will pass when it should be rejected.

[REQUIRED] Expected behavior

The rule should be rejected

[REQUIRED] Actual behavior

The rule evaluates to true

evelant avatar Apr 08 '22 16:04 evelant

Hey @evelant - could you provide some sample documents where this rule misbehaves? We have a good guess as to what is causing this, but having a concrete example would be very helpful

joehan avatar Apr 11 '22 20:04 joehan

I just had the same issue.

Using local emulator.

Looking in the emulator portal, the item has 4 fields, but in the request trace it shows Detailed Information -> Resource with only a single field.

This is on a read operation, and I was subscribing using a query snapshot.

mcapodici avatar Jul 16 '22 02:07 mcapodici

@mcapodici (or @evelant) Could either of you help us out and give us a minimal reproducible example for this issue? We're still needing a little help to debug exactly what's happening and a specific example will help a lot

bkendall avatar Jul 18 '22 20:07 bkendall

Hey @evelant. We need more information to resolve this issue but there hasn't been an update in 7 weekdays. I'm marking the issue as stale and if there are no new updates in the next 3 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

google-oss-bot avatar Jul 27 '22 01:07 google-oss-bot

Since there haven't been any recent updates here, I am going to close this issue.

@evelant if you're still experiencing this problem and want to continue the discussion just leave a comment here and we are happy to re-open this.

google-oss-bot avatar Aug 01 '22 01:08 google-oss-bot

What I have found is that the resource data seems to match the query, not the resource, at least as shown in http://localhost:4000/firestore/requests/ view, as I was trying a hack to get around this:

image

mcapodici avatar Aug 07 '22 11:08 mcapodici

It is a pain to write minimal reproducible steps as I would need to develop a complete app just do to this. It seems to happen a lot - when doing a query over all documents on a where clause. It is probably not hard for you to find out why based on the information especially that that constraint ended up in the resource.

Here is my example that lead to the query above.

  const q = query(collection(firestore, "users") as CollectionReference<UserData>, 
    where("email", "==", email.toLowerCase()),

mcapodici avatar Aug 07 '22 11:08 mcapodici

Since there haven't been any recent updates here, I am going to close this issue.

@evelant if you're still experiencing this problem and want to continue the discussion just leave a comment here and we are happy to re-open this.

google-oss-bot avatar Aug 09 '22 01:08 google-oss-bot

Could be related : https://github.com/firebase/firebase-js-sdk/issues/5229

Includes a possible workaround

Google internal tracking bug: b/129293104

christhompsongoogle avatar Aug 09 '22 19:08 christhompsongoogle

I suspect that other issue is the same thing basically.

The workaround mentioned there is:

if your security rule checks for admin == uid you need to include .where("admin", "==", uid) even though all your documents may have admin == uid

Or more generally, include everything your rule checks for in a where clause.

Yes this is my experience too. It means you might need 'dummy' clauses, but this is good enough workaround to unblock me I think. I will post back if I find a situation where this doesn't work.

All this kind of makes me think: unless it is a super simple security rule, then use a function as the gatekeeper rather than rules.

mcapodici avatar Aug 09 '22 22:08 mcapodici

I have experienced a similar issue with the Cloud Firestore Emulator where the request.resource.data property is not 'hydrated' as I thought it would be with a doc().set() request on an existing document.


Expected: The request.resource will be 'The new resource value, present on write requests only.'   Actual: The request.resource only contains the fields that are being updated, not all fields of the document.   I've reproduced this using the a Mocha test with the Cloud Firestore Emulator running locally. I am building an appointment booking application with a rule that after an appointment has been created, the customer cannot be changed. This is represented with the following rule:   match /appointments/{appointmentId} {   allow update: if request.auth != null &&     resource.data.user.uid == request.resource.data.user.uid; }   This is the test I've written to reproduce this issue:       it.only('Appointments can be updated if the user does not change', async () => {         // Data Setup         const user_abc = { uid: 'user_abc', email: '[email protected]' }         // Create the document         const admin = firebase.initializeAdminApp({ projectId: MY_PROJECT_ID }).firestore();         const testDoc = admin.collection('appointments').doc(randomUid);         await testDoc.set({ user: { uid: user_abc.uid }, serviceProvider: { uid: owner_abc.uid } });            // Update the document         const db = getFirestore(user_abc);         const testDoc2 = db.collection('appointments').doc(randomUid);         // await firebase.assertSucceeds(testDoc2.set({ startTime: 'Feb 22, 2023', })); // This actually fails         await firebase.assertSucceeds(testDoc2.update({ startTime: 'Feb 22, 2023', })); // This succeeds     });   What I have found is that calling doc().update() results in a properly 'hydrated' request.resource.data object that contains all the fields of the document.   However, if  I use doc().set() on an existing document which subsequently makes a doc().update() call, the request.resource.data object only contains the fields being updated. This causes the security rule to produce an error because the request.resource.data does not contain the expected user property:

FirebaseError: 7 PERMISSION_DENIED: Property user is undefined on object. for 'update' @ L69   Results of unit test: If I uncomment the testDoc2.set() line in the above test, it results in a create request which fails because the document exists. Then an update request occurs, but the update() request contains the same request.resource object as the create()  request.

image image

Now contrast that with the testDoc2.update() call which has the 'hydrated' request.resource object as expected:

image image

Hope this helps. Steve

Steven-Douglass avatar Feb 23 '23 03:02 Steven-Douglass

Hi Steven-Douglass, thank you for the detailed repro this helps narrow down the issue a lot. I took a good look at your example with some colleagues and I think this is WAI; let me tell you why:

testDoc2.set({ startTime: 'Feb 22, 2023', }) without the merge option actually overwrites the data in testDoc2. Afterwards, when the rules evaluation checks resource.data.user.uid == request.resource.data.user.uid the request.resource.data.user portion doesn't exist and thus the check fails. Can you try with the merge option and let me know if you still are encountering the issue?

https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document

christhompsongoogle avatar Apr 05 '23 00:04 christhompsongoogle

Actual data in collection:

{
        inviterId: TEAM_OWNER_ID,
        teamId: TEAM_A_ID,
        inviteeId: USER_NOT_IN_MY_TEAM_ID,
 }

Test case:

    it('Inviter can read their invitations to a team', async () => {
      const db = getFirestore( { uid: 'TEAM_OWNER_ID' });
      const testDoc = db.collection('TEAM_INVITATIONS_COLLECTION').where('inviterId', '==', 'TEAM_OWNER_ID');
      await firebase.assertSucceeds(await testDoc.get());
    });

Rules:

match /team_invitations/{invitationId} {
  allow read: if debug(resource.data.inviteeId) == request.auth.uid;
}

Error: Property inviteeId is undefined on object. for 'list' @ L40

Output of debug is correct. It prints 'USER_NOT_IN_MY_TEAM_ID'.

So it fails every time when we check other field than mentioned in query. The correct behavior would be that resource.data has all fields that are visible in "Data" tab in emulator UI.

adriank avatar Jan 26 '24 15:01 adriank

I got similar issue where when i use resource.data.members == null && request.auth.uid in resource.data.members, And the rule passed which is make no sense.

ASE55471 avatar Mar 02 '24 02:03 ASE55471

Running into same issue where the goal is to allow reading of "group" docs if request comes from user that is listed as part of that group.

A "group" collection exists where documents have a map of userId:public_data for the members of that group.

Unless I'm wrong, this kind of implementation is essentially exactly what's suggested here https://firebase.google.com/docs/firestore/solutions/role-based-access

example group doc:

{
  name: "group A",
  ownerID: "user1_ID",
  members: {
    user1_id: { name: "user1 name", otherPublicData: ...},
    user2_id: { name: "user2 name", otherPublicData: ...},
    ...
}

and firestore rules as such

function isGroupMember(req, res) {
   return debug(req.auth != null && req.auth.token.email_verified) && debug(req.auth.uid in debug(res.data.members.keys()));
}
match /groups/{groupId} {
  allow read: if isGroupMember(request, resource);
}

and the query being made is utilizing rxFire in angular like so

import { Firestore, collection, query, orderBy, documentId, where, limit, startAfter } from '@angular/fire/firestore';
import { collection as collect } from 'rxfire/firestore';

// in service
private firestore: Firestore = inject(Firestore);
// ...

// in constructor of service (user object comes from auth service. it works)
const q: any = query(collection(this.firestore, 'groups'), where('ownerID', '==', user.uid));
this.myGroup$ =  collect(q).pipe(
    map((doc: any) => {
        //...
    }),
);

Firestore emulator.requests shows a failure on a query to "groups" where that failed query has a resource value of: data:{ownerID: "some_user_id"}

I've tried accessing resource.data in different ways, but also run into an issue where im unable to use "get()" in order to check that doc

scrowley-Datirium avatar Mar 27 '24 22:03 scrowley-Datirium