Firestore Security Rules - Same Rule Works for Write But Not for Read

Viewed 65

I am developing a Firebase project where I am using firestore. I am querying Firestore to get Chat Messages from the messages collection. I want only messages beloning to a certain conversation:

const messages = await db
    .collection('messages')
    .where('room', '==', room)
    .onSnapshot(snap => {//stuff})

So fa so good. It works. Things go wrong when I set up security rules.

If I do somthing simple, such as:

allow read: if request.auth != null;

everything is fine. But if I want to allow access only to users whose uid is included in the 'partiesIDs' message object property, things go wrong:

allow read: if
    request.auth.uid == resource.data.pertiesIDs[0] ||
    request.auth.uid == resource.data.parties[1];

The strangest thing of all is that I have in place a very similar rule for update, which works as expected:

allow update: if
    (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['read', 'notified'])) &&
    (request.auth.token.name == resource.data.parties[0] ||
        request.auth.token.name == resource.data.parties[1]);

The query for the second rule (which works) looks like this:

const update = await db
    .collection('messages')
    .doc(docid)
    .update({read: true, notified: true})

I m stuck! Can anybody shed some light into this mistery?

2 Answers

The problem is that Firestore security rules are not filters. I strongly suggest reading that documentation.

When you write rules to place conditions on the reading of documents within a collection, the client is obliged to make a query that matches exactly the conditions of the rules. When you make a requirement that some data must exist in some field, then your query must match that by filtering for only documents that would satisfy the contents of the fields required by the rules. The rules will not extract only the matching documents to return them. You can think of the client query as demanding the full set of documents that match the given filters, and the rules as rejecting that demand because the conditions are not satisfied.

However, you have a bit of a problem here, because queries don't have a way of specifying array indexes. It's not possible to make a query that requires index 0 of an array field must contain a certain value.

I suggest rethinking your document data and rules, and structure them in such a way that the client app can exactly match their requirements.

Solved!

@Doug Stevenson thanks for having pointed me to the right direction.

The rules I have in place are the following:

allow read: if request.auth.token.name == resource.data.receiver || request.auth.token.name == resource.data.sender;
allow create: if request.auth != null;
allow update: if (request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['read'])) && (request.auth.token.name == resource.data.parties[0] || request.auth.token.name == resource.data.parties[1]);}
allow delete: if false;

With these rules I am able to do the following:

  1. First Load: load chats messages of a given room through a cloud function (it has admin priviledges, so no security rules issues there)

  2. Get Incoming Messages: add an onsnapshot realtime listener to get all messages where receiver is the logged in user and property room is equal to the open room

    await db.collection('messages').where("receiver","==",user.displayName).where("room", "==", room).orderBy('timestamp').onSnapshot(snapshot => { //Do Stuff })

  3. Updated Read Status: when a message is displayed, the receiver updates the read property of the mssage in firestore:

    const update = await db.collection("messages").doc(id).update({read: true});

  4. Render Read Status in the UI in real time: add a second onsnapshot realtime listener to get all messages where sender is the logged in user and room is equal to the open room

    const readUpdate = await db.collection('messages') .where("sender", "==", user.displayName) .where("room", "==", room) .orderBy('timestamp') .onSnapshot(snapshot => { //Do Stuff })

  5. Send Message: user sends new messages through firestore. On the sender side, new messages are rendered locally through JS (Not through a real time listener). On the receiving side, new messages are delivered and displayed throught the listener described in 2)

    const deliverMSG = await db.collection('messages').doc(newID).set(newMsgObj); //Render Message renderMSG({id: newID, data: newMsgObj});

Perhaps this is not the most elegant way to handle this, but it works pretty well and is definitelly secure. If you have any extra tip, I ll be very happy to read it.

Related