Understanding Firebase's rules for user-write, global-read

1.4k views Asked by At

I am building a simple Firebase application with AngularJS. This app authenticates users through Google. Each user has a list of books. Anyone can see books, even if they are not authenticated. Only the creator of a book can edit it. However, individual users need to be able to record that they've read a book even if someone else added it.

I have rules.json like so:

{
  "rules": {
    ".read": false,
    ".write": false,
    "book": {
      "$uid": {
        ".write": "auth !== null && auth.uid === $uid",
      }
      ".read": true,
    }
  }
}

And I am trying to write a book simply with:

$firebaseArray(new Firebase(URL + "/book")).$add({foo: "bar"})

I get a "permission denied" error when trying to do this although I do seem to be able to read books I create manually in Forge.

I also think that the best way to store readers would be to make it a property of the book (a set of $uid for logged-in readers). ".write" seems like it would block this, so how would I do that?

"$uid": {
  ".write": "auth !== null && auth.uid === $uid",
  "readers": {
    ".write": "auth !== null"
  }
},

It seems like a validation rule would be appropriate here as well ... something like newData.val() == auth.uid, but I'm not sure how to validate that readers is supposed to be an array (or specifically a set) of these values.

2

There are 2 answers

2
Frank van Puffelen On BEST ANSWER

Let's start with a sample JSON snippet:

  "book": {
    "-JRHTHaIs-jNPLXOQivY": { //this is the generated unique id
      "title": "Structuring Data",
      "url": "https://www.firebase.com/docs/web/guide/structuring-data.html",
      "creator": "twiter:4916627"

    },
    "-JRHTHaKuITFIhnj02kE": {
      "title": "Securing Your Data",
      "url": "https://www.firebase.com/docs/security/guide/securing-data.html",
      "creator": "twiter:209103"
    }
  }

So this is a list with two links to articles. Each link was added by a different user, who is identified by creator. The value of creator is a uid, which is a value that Firebase Authentication provides and that is available in your security rules under auth.uid.

I'll split your rule into two parts here:

{
  "rules": {
    ".read": false,
    "book": {
      ".read": true,
    }
  }
}

As far as I see your .read rule is correct, since your ref is to the /book node.

$firebaseArray(new Firebase(URL + "/book"))

Note that the ref below would not work, since you don't have read-access to the top-level node.

$firebaseArray(new Firebase(URL))

Now for the .write rules. First off is that you'll need to grant users write-access on the book level already. Calling $add means that you're adding a node under that level, so write-access is required.

{
  "rules": {
    "book": {
      ".write": "auth != null"
    }
  }
}

I leave the .read rules out here for clarity.

This allows any authenticated user to write to the book node. This means that they can add new books (which you want) and change existing books (which you don't want).

Your last requirement is most tricky. Any user can add a book. But once someone added a book, only that person can modify it. In Firebase's Security Rules, you'd model that like:

{
  "rules": {
    "book": {
      ".write": "auth != null",
      "$bookid": {
        ".write": "!data.exists() || auth.uid == data.child('creator').val()"
      }
    }
  }
}

In this last rule, we allow writing of a specific book if either there is no current data in this location (i.e. it's a new book) or if the data was created by the current user.

In the above example $bookid is just a variable name. The important thing is that the rule under it is applied to every book. If needed we could use $bookid in our rules and it would hold -JRHTHaIs-jNPLXOQivY or -JRHTHaKuITFIhnj02kE respectively. But in this case, that is not needed.

3
André Kool On

First off the "permission denied" error. You are getting this error because you are trying to write directly in the "book" node instead of "book/$uid".

Example of what you do now:

  "book": {
    "-JRHTHaIs-jNPLXOQivY": { //this is the generated unique id
      "foo": "bar"
    },
    "-JRHTHaKuITFIhnj02kE": {
      "foo": "bar"
    }
  }

In your rules you have a global rule for write set to false so that will be the default and next to that you have made a rule for the specific node book/$uid. So when trying to write directly in "book" it will take the default rule that was set to false. Have a look at Securing your data for more information about firebase rules.

And for the last part of your question i suggest you take a look at Structuring data for more information about the best ways to structure your data inside firebase.

So taka a good look at what and how you want to save and write in firebase and make sure your rules are structured accordingly.