Firebase Realtime Database Security Rules #5 - Dependencies on Other Branches

So we have rules for the /MyUsers branch and are ready to work on a new part of our app that writes to another branch.

For the sake of having an example let’s assume we have a game app that we are writing. And for each level of the game we’ll have tree branches such as /Levels/Level1, /Levels/Level2, /Levels/Level3.

And for each Level we’ll have a model object named LevelModel with one field named “score”. Its just a super simple POJO for our example.

public class LevelModel {
   int score;
   public int getScore() {
       return score;
   }
   public void setScore(int score) {
       this.score = score;
   }
}

Here’s the code that writes to the database:

LevelModel levelModel = new LevelModel();
levelModel.setScore(10);

DatabaseReference dbRef1 = FirebaseDatabase.getInstance().getReference()
       .child("Level1")
       .child(mAuth.getUid().toString());

dbRef1.setValue(levelModel).addOnSuccessListener(new OnSuccessListener<Void>() {
   @Override
   public void onSuccess(Void aVoid) {
       Log.i(TAG, "Write to database is successful!");
   }
}).addOnFailureListener(new OnFailureListener() {
   @Override
   public void onFailure(@NonNull Exception e) {
       Log.e(TAG, "Write to database failed.");
       Log.e(TAG, e.getMessage());
   }
});

For most of these exercises we’re going to keep the same read rule:  user must be logged in and the path the user is reading must contain the logged in user’s auth UID.

In our code try to write the model to /Levels/Level1/userUID we get an error:

W: setValue at /Levels/Level1/1234567890123456789012345678
 failed: DatabaseError: Permission denied
E: Write to database failed.
E: Firebase Database error: Permission denied

The reason is because we haven’t defined any reads or writes to /Levels or /Levels/Level1!

Recall that Firebase security rules are path-based. And when an explicit read or write is provided for a user then that user has the privilege from that path through the depth of the tree.

With that in mind let’s create a rule on /Levels/Level1. Let’s start by creating rules that allow read and write if your current auth.uid matches the one in the path.

Add the following rule as another rule beneath “rules” and as a peer rule to the MyUsers rule:

"Levels": {
   "Level1": {
       "$userUID": {
          ".read": "auth.uid != null && auth.uid == $userUID",
          ".write": "auth.uid != null && auth.uid == $userUID",
        }
     }
   }
}

That looks pretty good - and it works!

I: Write to database is successful!

But we should go further.  Since all users playing the game are required to have a user profile why don’t we check that they have one before we allow the write?

For that we can use the root variable, which corresponds to the current data at the root of your Firebase Realtime Database, along with the child snapshot method:

root.child('MyUsers').child(auth.uid).exists()"

So now our rules are:

"Levels": {
  "Level1": {
     "$userUID": {
        ".read": "auth.uid != null && auth.uid == $userUID",
        ".write": "auth.uid != null && auth.uid == $userUID && root.child('MyUsers').child(auth.uid).exists()",
       }
   }
 }
}

Try the rule in the Simulator and use your own UID value.  It works!

Now let’s simulate a valid Google user who does not yet have a user profile in the database.

Change the Location in the Simulator as well as the Authenticated Google user UID to be a different value.  Note that you need to change BOTH of these places!

To test our new rule we want our test to match 

auth.uid != null && auth.uid == $userUID

but we don’t want our test to match

root.child('MyUsers').child(auth.uid).exists()"

Click Run on the simulation and you’ll see that simulates writes fail using our new write rule. Terrific!

Click Publish to save the rules which currently look like:

{
  "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()",
              }
            }
          }
      }
}

But we aren’t done. For completeness, let’s add a validation rule for the data model. Using more methods on newData, namely isNumber() and val(), we’ll validate that the data model has a field named “score”, that the value is a number and that the value is greater than 0:

".validate" : 
    "newData.hasChildren(['score'])",
    "score": {
      ".validate" : "newData.isNumber() && newData.val() >=0"
    }
}

Using the simulator test our value of 10. It works!

Now test passing a value of -1 for score, a string and without passing a “score” field in the model. All write simulations fail as expected.

Here are our complete rules so far. Looking good!

{
  "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"
              }
            }
          }
      }
  }
}