Firebase Realtime Database Security Rules #7 - Overwriting Existing Data

Right now we are able to set data and we have some criteria to meet in order to make a successful write.  This criteria corresponds to the set operation in the Simulator.

But what about overwriting existing data, which is the update operation in the Simulator?  You need rules for how and when existing data can be overwritten. 

Let’s suppose that once a player has obtained a sufficient score for some level they cannot try for a higher score at that level.  Once the score is written for that level it is permanent.

So let’s harden the security rules for Level2.

With our current rules a user can write and rewrite the Level2 score.  By adding validation dependent on the existing data at that location in the database we can disallow overwriting via the data variable.  The data variable corresponds to “the current data in Firebase Realtime Database at the location of the currently executing rule.

We’ll add this simple but powerful rule which states that the data at that location cannot already exist when we go to write it:

!data.exists()

Now our write rule is:

".write": "auth.uid != null && auth.uid == $userUID && root.child('Levels').child('Level1').child(auth.uid).child('score').val() > 15 && !data.exists()",

So let’s test it using the Simulator!  Assume the data in the database looks like:

{
  "Levels" : {
    "Level1" : {
      "1234567890123456789012345678" : {
        "score" : 18
      }
    },
    "Level2" : {
      "1234567890123456789012345678" : {
        "score" : 30
      }
    }
  }

So there is an existing value at /Levels/Level2/1234567890123456789012345678. Remember that the Simulator executes the rules based on the current state of your Realtime Database.

Set the Simulation type to set and write an object.  It fails as expected, since data exists at that location.

Set the Simulation type to “update” and try to write an object. It fails as expected, since data exists at that location.

Now go to your browser and delete the existing value in the database under /Levels/Level2/1234567890123456789012345678.

Run the Simulator with type set to set.  This time the write succeeds!

Run the Simulator with type set to update.  This time the write succeeds!

Publish the rules to save them.

Here is the current ruleset:

{
  "rules": { 
      "MyUsers": {
          "$userUID": {
          ".read": "auth.uid != null && auth.uid == data.child('userUID').val() == auth.uid",
          ".write": "auth.uid != null && auth.uid == $userUID && newData.child('userUID').val() == auth.uid",  
          ".validate": "newData.hasChildren(['firstName', 'lastName', 'userUID'])",
          "firstName" : {
              ".validate": "newData.isString() && newData.val().length <= 20"
          },
          "lastName" : {
              ".validate": "newData.isString() && newData.val().length <= 20"
          },
          "userUID" : {
              ".validate": "newData.isString() && newData.val().length === 28 && newData.val().matches(/^[A-z0-9]*/)"
          }
        }
    },
      "Levels": {
          "Level1": {
              "$userUID": {
                  ".read": "auth.uid != null && auth.uid == $userUID",
                  ".write": "auth.uid != null && auth.uid == $userUID && root.child('MyUsers').child(auth.uid).exists()",
              ".validate" : "newData.hasChildren(['score'])",
              "score": {
                ".validate" : "newData.isNumber() && newData.val() >=0"
              }
            }
          },
          "Level2": {
              "$userUID": {
                  ".read": "auth.uid != null && auth.uid == $userUID",
                            ".write": "auth.uid != null && auth.uid == $userUID && root.child('Levels').child('Level1').child(auth.uid).child('score').val() > 15 && !data.exists()",
              ".validate" : "newData.hasChildren(['score'])",
              "score": {
                ".validate" : "newData.isNumber() && newData.val() >=0"
              }
            }
          }
      }
  }
}