Ben Clarke
Ben Clarke

Reputation: 1287

Firebase Realtime Database Rules: Validation is cascading to child. How to ignore .validate on parent node?

I'm trying to write firebase realtime database rules that firstly, allow a new game to be created if the $gameID has exactly 6 characters AND the $gameID doesn't already exist on the games node. This rule actually validates perfectly when creating a new game.

The problem comes when a player then tries to join that game: the aforementioned validate rule denies them because it requires the game to not exist - but I only want that rule — "$gameID.length === 6 && !data.exists()" — to validate the writes ONLY on games/$gameID, not on games/$gameID/players.

My understanding was that .validate rules don't cascade down, so why is it not allowing the write to games/$gameID/players?

My current database.rules file is:

{
  "rules": {
    "games": {
      "$gameID": {
        ".read": "auth !== null",
        ".write": "auth !== null",
        ".validate": "$gameID.length === 6 && !data.exists()",
          "players": {
          ".read": "auth !== null",
          ".write": "auth !== null"
        }
      }
    }
  }
}

The javascript code for the players write is:

await push(ref(db, `games/${gameID}/players`), playerName)

set simulation denied with .validate rule ($gameID node ABC123 exists in database): Denied with .validate rule

set simulation allowed without .validate rule ($gameID node ABC123 exists in database): Allowed without .validate rule

Upvotes: 4

Views: 869

Answers (1)

samthecodingman
samthecodingman

Reputation: 26246

When security rules talk of "cascading rules", this refers to the behaviour where a higher tier ".read" and ".write" rule will override any nested rules (a higher tier rule succeeding will override a nested rule failing). This is in contrast to ".validate" rules where every rule in the chain must evaluate to true for a write to succeed (a higher tier rule succeeding will not override a nested rule failing).

From your comments your data looks like this:

{
  "games": {
    "abcdef": {
      "players": {
        "-MoSHoV6kC4cV_jCdssT": "player A",
        "-MoSI4begYNOBYVmiVBi": "not player A",
        "-MoSI5nVWYD5VnSaaWP0": "tom"
      }
    },
    /* more rooms */
  }
}

The problem with your current rules is that you must allow the room to be created, and you must also allow players to join that room. As you've deduced, your rules permit the first action, but block the second.

To fix this, you'll need to make a slight tweak to your client-based code so that you can differentiate the "I am creating this room" and the "I am joining this room" actions. With the current structure, I do not see how this is possible as the two actions don't have anything different between them - they both look like:

SET /games/abcdef/players/-MoSHoV6kC4cV_jCdssT = "some user"
SET /games/abcdef/players/-MoSI4begYNOBYVmiVBi = "not player A"

Which of these requests should be allowed to create the room?

To help this, we need to introduce a way to tell the database that "I am creating this room". We can do this by adding a new "owner" property which allows us to apply the following rules:

  • If the owner doesn't exist, allow room creation.
  • If the owner already exists, deny room creation.
  • If the room is created without an owner, deny room creation.
  • If the room is created with an invalid ID, deny room creation.
  • Players may only be added/removed by the owner.

Because your users are signed in, you should use their user ID instead of a push ID to help with keeping rooms secure later on, although the below setup will still work even if you don't switch.

{
  "games": {
    "abcdef": {
      "players": {
        "BzzmNyT7hlMz3ElRLMYS0jaGKgE3": "player A",
        "BSfHksARFnYco5LenfxevOpnwe63": "not player A",
        "vDnPE4DqE3cz1IYHwXQvDFw3W7r2": "tom"
      },
      "owner": "BzzmNyT7hlMz3ElRLMYS0jaGKgE3"
    },
    /* more rooms */
  }
}

with the rules:

{
  "rules": {
    "games": {
      "$gameID": {
        // any authenticated user may read this entire room's data
        // future: restrict to only members?
        ".read": "auth !== null",

        // a room must have an ID of 6 characters
        // and must have an owner assigned to it
        ".validate": "$gameID.length === 6 && newData.hasChild('owner')",

        "players": {
          // use $pushId here if keeping your "push" method instead of switching to user IDs
          "$playerId": {
            // only the owner may add/remove players
            ".write": "auth !== null && newData.parent().child('owner').val() == auth.uid",
            // as long as the value is a string
            ".validate": "newData.isString()"
          }
        },

        "owner": {
          // an owner prop can be created, but not edited/deleted
          ".write": "auth !== null && !data.exists()"
          // an owner prop must be the writer's user ID
          ".validate": "newData.isString() && newData.val() === auth.uid"
        }
      }
    }
  }
}

Note: The above rules to nothing to asset that the owner of a room is also a player.

Upvotes: 2

Related Questions