FirebaseExtended / bolt

Bolt Compiler (Firebase Security and Modeling)
Apache License 2.0
896 stars 108 forks source link

Feature Request: Be able to reference map keys in validation rules #219

Open alixaxel opened 6 years ago

alixaxel commented 6 years ago

I'm having trouble with a DB structure that looks like this:

{
  "post-flag": {
    "-KvXU5eiR4oZM-vBihib": {
      "6t5KEJjAOSYbf8dleabDlTyxhux2": 1507542071604,
      "6t5KEJjAOSYbf8dleabDlTyxhux3": 1507542071605
    }
  }
}

I'd like to validate that the data is valid and that the keys follow a certain format.

I've tried several variations of the following, but none of them really work.

path /post-flag is Map<PushID, Map<UserID, UnixTimestamp>>;
path /post-flag/{post}/{user} is UnixTimestamp {
  write() {
    return isCurrentUser(user) && root.post[post] != null
  }
}

type PushID extends String {
  validate() { return this.test(/^[-_0-9a-zA-Z]{20}$/); }
}

type UserID extends String {
  validate() { return this.test(/^[0-9a-zA-Z]{27}[0-9]$/); }
}

type UnixTimestamp extends Number {
  validate() { return this >= 0; }
}

For the example above I get the following JSON:

{
  "rules": {
    "post-flag": {
      "$key1": {
        ".validate": "newData.hasChildren() && $key1.matches(/^[-_0-9a-zA-Z]{20}$/)",
        "$key2": {
          ".validate": "$key2.matches(/^[0-9a-zA-Z]{27}[0-9]$/) && newData.isNumber() && newData.val() >= 0"
        }
      },
      ".validate": "newData.hasChildren()",
      "$post": {
        "$user": {
          ".validate": "newData.isNumber() && newData.val() >= 0",
          ".write": "auth != null && auth.uid == $user && newData.parent().parent().parent().child('post').child($post).val() != null"
        }
      }
    }
  }
}

Which yields the following error in Firebase simulator:

Cannot have multiple default rules ('$key1' and '$post').

I'm not sure if I'm writing my rules wrongly or if this is a limitation of Bolt.

rockwotj commented 6 years ago

Possibly you want something like:

function isCurrentUser(user) {
  return auth != null && auth.uid == user;
}

path /post-flag is Map<PushID, Map<UserID, UnixTimestamp>> {
  write() {
    return isCurrentUser(key2) && root.post[key1] != null
  }
}

type PushID extends String {
  validate() { return this.test(/^[-_0-9a-zA-Z]{20}$/); }
}

type UserID extends String {
  validate() { return this.test(/^[0-9a-zA-Z]{27}[0-9]$/); }
}

type UnixTimestamp extends Number {
  validate() { return this >= 0; }
}

Slightly tricky but you have to know the first map gets a key generated of $key1. Bolt doesn't really understand how to merge paths between types and wildcards.

alixaxel commented 6 years ago

@rockwotj Thanks for the quick reply.

I thought that might be the case... user should then become key2, right?

path /post-flag is Map<PushID, Map<UserID, UnixTimestamp>> {
  write() {
    return isCurrentUser(key2) && root.post[key1] != null
  }
}

This generates the following JSON:

    "post-flag": {
      "$key1": {
        ".validate": "newData.hasChildren() && $key1.matches(/^[-_0-9a-zA-Z]{20}$/)",
        "$key2": {
          ".validate": "$key2.matches(/^[0-9a-zA-Z]{27}[0-9]$/) && newData.isNumber() && newData.val() >= 0"
        }
      },
      ".validate": "newData.hasChildren()",
      ".write": "auth != null && auth.uid == key2 && newData.parent().child('post').child(key1).val() != null"
    },

And error message:

Unknown variable 'key2'.

rockwotj commented 6 years ago

Oops yes, good catch. I've updated my answer as well.

alixaxel commented 6 years ago

So the only way I've made it to work is if duplicate the rule and use keyX as variable name:

path /post-flag is Map<PushID, Map<UserID, UnixTimestamp>>;

path /post-flag/{key1} {
  write() {
    return root.post[key1] != null
  }
}

path /post-flag/{key1}/{key2} {
  write() {
    return isCurrentUser(key2)
  }
}

Isn't there any sort of aliasing to instruct Bolt to map variables to certain names? Like:

path /post-flag is Map<PushID as post, Map<UserID as user, UnixTimestamp>>;
rockwotj commented 6 years ago

Currently there is not, but feel free to open a PR :smile:

alixaxel commented 6 years ago

I'll have a go at it. :smile: