Firebase’s Firestore rules: How to? Part 2

Gerard Lamusse
DAY4
Published in
5 min readMay 11, 2019

This is a continuation on setting up rules for a very simple Firestore collection set. If you haven’t yet you can read the first part here. Read Part 1

Firestore-rules-starter is a GitHub repo containing the custom functions that we created in part 1. You can either use the entire file to start off with or just copy out the functions as you need.

We finished the rules for the users’ collection in part1 and can now start with the groups, polls & votes collection. Here is a reminder of what our Firestore database looks like:

As you can see groups contain polls which in turn contain votes. In the firestore.rules file we end up with rules for these three groups as follows:

match /databases/{database}/documents {

// ……… our user rules
match /groups/{groupId} {
allow read: if isSignedIn()
&& userInMap(resource.data.members);
allow write: if isSignedIn()
&& isUserRef(userWithRole(resource.data.members.keys(), 'owner')
// Get the group data for use on sub collections
function groupData() {
return get(/databases/$(database)/documents/groups/$(groupId)).data
}
match /polls/{pollId} {

// Allow any member in the group to view polls
allow read: if isSignedIn()
&& userInMap(groupData().members)

// Only allow 'creator' members to create polls
allow create: if isSignedIn()
&& (userWithRole(groupData().members, 'creator')
|| isUserRef('owner'))
&& createdWith(['question', 'options', 'closed'])
&& hasLength('question', 5, 200)
&& request.resource.data.options != null
&& request.resource.data.options.keys().size() > 2
&& isBool('closed');
match /votes/{votesId} { // Allow only one vote per member
allow write: isSignedIn()
&& request.auth.uid == votesId
&& createdWith(['choice', 'voted']);
}
}
}
// ……… all other rules // Is the given field a reference of the current user
function isUserRef(field) {
return field in resource.data
&& resource.data[field] == /databases/$(database)/documents/users/$(request.auth.uid)
}
// Is user id a key in the given map
function userInMap(map) {
return request.auth.uid in map.keys()
}
// Does user have the given role in the given map
function userWithRole(map, role) {
return userInMap(map)
&& map[request.auth.uid] == role
}
// Ensure document is created with only the given fields
function createdWith(fields) {
return request.resource.data.size() == fields.size() && request.resource.data.keys().hasOnly(fields)
}
}

This is about the complete set of rules we’ll need. But what is a solution without explanation?

First some more background info about this entire use case:

  1. When users signup we create a user profile for them using their auth.uid
  2. Users will be able to create groups and invite other members by email via by calling an API implemented in Firebase’s Cloud Functions.
  3. Owners and creators of a team can create a poll asking a question to all members.
  4. Members vote by choosing from the possible options. Each member’s own id is used to create one vote per poll, limiting them to only casting one vote per poll.
  5. Once a vote is created or updated a Cloud Function is triggered updating the count of the specific option within the poll.

The details

Sub-collections can be matched relative to their parents like so:

match /databases/{database}/documents {    match /groups/{groupId} {
………
match /polls/{pollId} {
………
match /votes/{voteId} {
………
}
}
}
)

In order for us to match any of the rules to a specific user, we need to look up the members property on the groups' collection. When accessing a group then we already have access to members via resource.data.members however, in the sub-collections we’ll need to fetch the group’s data like so:

get(/databases/$(database)/documents/groups/$(groupId)).data

Declaring a custom function within the scope of the groups' collection we can directly access the groupId without having to send it in as an argument.

The members property is a map field that contains the users’ id as keys and their respective roles as their values. In our example, we’ll only allow the owner to assign the role of creator or member. With these two roles, we can differentiate between who may create new polls and not.

NOTE:
In production it is better to shorten your keys and static values to as short as possible since you’ll be paying for each byte you transfer. So the groups` collection will probably look more like the following:

{
n: "The Red Group",
o: /databases/database/documents/users/kd2hh7u2wl7im3z9,
m: {
RR5bAOGx9AyevQ84: 'c',
gJtvEwRAQm61ytKN: 'm',
GYOcgL7L39o5H5Qo: 'm',
},
p: true
}

So, What the F?

The rest of the functions and rules I’ve commented on within the code so I’ won’t repeat that here. The documentation on the Firebase website also explains quite a bit the basics of Firestore rules.
However, that is where the problem and these articles come in since the documentation just highlights the more basic usage of rules but not really production quality examples of rules.

As an example, it is hard to write rules correctly to handle checking for read-only and required fields. That is why I’ve released these custom functions to help anyone out instead of also sitting hours trying to find something to work.

I do hope that I am proven wrong sooner, rather than later and that someone finds better solutions to how these rules can work together.

request.writeFields

Firestore rules had a property called writeFields which only contained the fields the user actually sent for updating, making it very easy to check for read-only and typed fields. However, it is now deprecated :(

My conclusion

So after 3 Firebase projects, I have come to the conclusion that Firestore has made it so hard to check and validate user input that it seems like they thought it isn’t important anymore!

My other reason for this conclusion is the fact that someone assumed an entire project’s rules will just fit into one file, so no need to support writing rules in different files! That just adds to the complexity of writing and maintaining rules.

In the end, a platform for building mobile and web apps should be making data handling easier, secure and less complex, not more.

I would like to hear from you and your experience using Firestore rules if you have had the same experience or quite the opposite.

--

--