hooperdev
hooperdev

Reputation: 951

How To Detect Array Entry Deletion Request In Firestore Rules

I need to delete an entry in an array in one of my documents in firestore (that's a lot of "in's")

The only way to do that (according to this) is through an update request that sets the entry's value to some weird non-number value using FieldValue.arrayRemove(val) or FieldValue.delete().

How do I detect this "hippity hoppity delete your property" value in Firestore rules? Because currently I have it set that you can only write your own UID to that array, and I want it to also allow you to delete your own entry.

My Dart code:

FirebaseFirestore.instance.collection("requests").doc(id).update({"likes." + FirebaseAuth.instance.currentUser.uid: FieldValue.delete()});

My Firestore rules (likes is the array):

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
  match /requests/{requestID} {
    allow update: if (request.resource.data.likes.hasOnly([request.auth.uid])) && (resource.data.likes.size() + 1 == request.resource.data.likeCount);
        }
    }
}

My firestore: Firestore Pic

Upvotes: 1

Views: 85

Answers (2)

samthecodingman
samthecodingman

Reputation: 26196

In Cloud Firestore Security Rules, an array is called a List and an array of unique values is a Set.

Keeping this terminology, you want to check if the set of values for likes had request.auth.uid added to/removed from it, or was left unchanged. You also want to assert that the only changes were related to that value.

We can do this with the aid of Custom Functions in our rules.

First, let's write a function to assert that a list is unique:

// Checks whether the given list is unique
function isListUnique(list) {
  return list.toSet().size() == list.size();
}

// Examples:
isListUnique(['a', 'b']) == true
isListUnique(['a', 'a']) == false

Next, we want to make sure that the initial list of unique values and the final list of unique values differ only by the given list of values.

// Checks whether the two sets differ by only the given changes set
function doesSetDifferBy(setA, setB, setChanges) {
  return setA.difference(setB).union(setB.difference(setA)) == setChanges;
}

// Examples:
doesSetDifferBy(['a', 'b'].toSet(), ['a', 'c'].toSet(), ['b'].toSet()) == false // differs by ['b','c'].toSet() not ['b'].toSet()
doesSetDifferBy(['a', 'b'].toSet(), ['a'].toSet(), ['b'].toSet()) == true
doesSetDifferBy(['a', 'b'].toSet(), ['a', 'b'].toSet(), ['b'].toSet()) == false // sets are equal, not different
doesSetDifferBy(['a'].toSet(), ['a', 'b'].toSet(), ['b'].toSet()) == true

While we could call the above function using this next line, we should first make sure that the input lists are unique:

doesSetDifferBy(resource.data.likes.toSet(), request.resource.data.likes.toSet(), [request.auth.uid].toSet())

To assert that two lists are unique and differ by only the given list of changes, you would use this function:

// Checks whether the two lists are both unique and differ by only the given changes list
function doesUniqueListDifferBy(listA, listB, listChanges) {
  return isListUnique(listA) && isListUnique(listB)
    && doesSetDifferBy(listA.toSet(), listB.toSet(), listChanges.toSet())
}

// Examples:
doesUniqueListDifferBy(['a', 'b'], ['a', 'c'], ['b']) == false // differs by ['b','c'] not ['b']
doesUniqueListDifferBy(['a', 'b'], ['a'], ['b']) == true
doesUniqueListDifferBy(['a', 'b'], ['a', 'b'], ['b']) == false // lists are equal, not different
doesUniqueListDifferBy(['a'], ['a', 'b'], ['b']) == true
doesUniqueListDifferBy(['a', 'a'], ['a', 'b'], ['b']) == false // first list isn't unique

For your application, you also want to make sure that if the likes list was unchanged, that it is allowed through. We can achieve this by tweaking the doesUniqueListDifferBy() function from above to:

// Checks whether the two lists are both unique and either are
// equal or differ by only the given changes list
function isUniqueListUnchangedOrDiffersBy(listA, listB, listChanges) {
  return isListUnique(listA) && isListUnique(listB)
    && (listA.toSet() == listB.toSet()
      || doesSetDifferBy(listA.toSet(), listB.toSet(), listChanges.toSet()))
}

// Examples:
isUniqueListUnchangedOrDiffersBy(['a', 'b'], ['a', 'c'], ['b']) == false // differs by ['b','c'] not ['b']
isUniqueListUnchangedOrDiffersBy(['a', 'b'], ['a', 'b'], ['b']) == true
isUniqueListUnchangedOrDiffersBy(['a', 'b'], ['a'], ['b']) == true
isUniqueListUnchangedOrDiffersBy(['a'], ['a', 'b'], ['b']) == true
isUniqueListUnchangedOrDiffersBy(['a', 'a'], ['a', 'b'], ['b']) == false // first list isn't unique

Rolling the above functions together, your rules will look like:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
  
    // Checks whether the given list is unique
    function isListUnique(list) {
      return list.toSet().size() == list.size();
    }

    // Checks whether the two sets differ by only the given changes set
    function doesSetDifferBy(setA, setB, setChanges) {
      return setA.difference(setB).union(setB.difference(setA)) == setChanges;
    }
  
    // Checks whether the two lists are both unique and either are
    // equal or differ by only the given changes list
    function isUniqueListUnchangedOrDiffersBy(listA, listB, listChanges) {
      return isListUnique(listA) && isListUnique(listB)
        && (listA.toSet() == listB.toSet()
          || doesSetDifferBy(listA.toSet(), listB.toSet(), listChanges.toSet()))
    }
  
    match /requests/{requestID} {
      allow update: if isUniqueListUnchangedOrDiffersBy(resource.data.likes, request.resource.data.likes, [request.auth.uid]);
    }
  }
}

You also need to update your client's Dart code to:

FirebaseFirestore.instance.collection("requests").doc(id)
  .update({
    "likes": FieldValue.arrayRemove(FirebaseAuth.instance.currentUser.uid)
  });

If you are a fan of syntactic sugar, you could also tweak the function to take just the property name, but this won't support nested properties.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
  
    // Checks whether the given list is unique
    function isListUnique(list) {
      return list.toSet().size() == list.size();
    }

    // Checks whether the two sets differ by only the given changes set
    function doesSetDifferBy(setA, setB, setChanges) {
      return setA.difference(setB).union(setB.difference(setA)) == setChanges;
    }
  
    // Checks whether the two lists are both unique and either are
    // equal or differ by only the given changes list
    function isUniqueListUnchangedOrDiffersBy(listA, listB, listChanges) {
      return isListUnique(listA) && isListUnique(listB)
        && (listA.toSet() == listB.toSet()
          || doesSetDifferBy(listA.toSet(), listB.toSet(), listChanges.toSet()))
    }

    // Checks whether the property contains a unique list that was either
    // unchanged or differs by only the given changes list
    function isUniqueListDataPropUnchangedOrDiffersBy(propName, listChanges) {
      return isUniqueListUnchangedOrDiffersBy(resource.data[propName], request.resource.data[propName], listChanges)
    }
  
    match /requests/{requestID} {
      allow update: if isUniqueListDataPropUnchangedOrDiffersBy("likes", [request.auth.uid]);
    }
  }
}

Upvotes: 2

Anton L
Anton L

Reputation: 450

You can check if the array that comes in has not the uid and if the size is only one less so it only deleted one and it should be the uid one.

It should be something like:


match /databases/{database}/documents {
    match /requests/{requestID} {
        allow update: if
            resource.data.likes.hasAll(request.resource.data.likes) &&
            resource.data.likes.hasAll([request.auth.uid]) &&
            !request.resource.data.likes.hasAll([request.auth.uid]) &&
            request.resource.data.likes.size() == resource.data.followers.size() - 1          
    }
}

Upvotes: 1

Related Questions