Search code examples
google-cloud-firestorefirebase-securityrtk-query

How to only allow a negative value in Cloud Firestore Security Rules?


I'm working on a client side application using React, RTK Query and Cloud Firestore. Every user on the app can collect points and then use it to buy merch. But I'm currently stuck on the order part of the application.

When buying merch I have to give the user access in security rules so that points can be withdrawn from their account:

function isOwner(userId){
    return request.auth.uid == userId;
  }
function isSignedIn() {
    return request.auth != null;
  }

match /users/{userId} {
  allow update: 
    if request.resource.data.diff(resource.data).changedKeys().hasOnly(["points"])
       && isSignedIn() && isOwner(userId);
}

But having it likes this would technically allow the user to add points to their account. This is also what the mutation looks like in RTK Query:

    userPayment: builder.mutation({
      async queryFn(data) {
        try {

          const docRef = doc(db, "users", data.userId);

          await updateDoc(docRef, {
            points: increment(-data.totalPrice)
          })

          return {data: "payment successful"}
        } catch (err) {
          return {error: err}
        }
      },
      invalidatesTags: ["UserData"]
    }),

I'm only guessing that RTK Query is not safe enough, so is there a way to write the security rules so that update is only allowed when the request.resource.data is negative?


Solution

  • In your rules you have two states of the document available:

    • resource: the document as it exists before the write operation
    • request.resource: the document as it exists after the write operation, if that write operation is allowed.

    So you can compare the field's value between these two documents, and only allow the write if the latter is lower than the former.

    (resource.data().points > request.resource.data().points)
    

    The above will implement the question you asked, but doesn't fully secure the use-case yet. With this rule a user could still just decrement their points value by 1, regardless of the actual cost of the item they bought.

    To better secure this, you'll want to ensure that the points value is decrement by the same amount as the cost of the item they bought. To do this, you'll want to ensure that both the document indicating the item they bought and the points decrementing happen in a single write operation (so a batches write or a transaction), and then use the get() and getAfter() functions to compare the values.

    The get() function gives you the resource version of the document you load, while getAfter() gives you the equivalent request.resource version of the document.