Firebase Realtime Database Rules

Revision as of 20:18, 29 August 2017 by Neil (Talk | contribs)

Revision as of 20:18, 29 August 2017 by Neil (Talk | contribs)

PreviousTable of ContentsNext
Reading Firebase Realtime Database DataWorking with Firebase Realtime Database Lists



Firebase database rules control the way in which the data stored in a Firebase Realtime Database data is secured, validated and indexed. These rules are defined using a rules expression language similar to JSON which may be configured on a per-project basis using either the Firebase console or Firebase command-line interface.

In this chapter we will review the basics of implementing database rules using the Firebase console.

Accessing and Changing the Database Rules

The current database rules for a Firebase project may be viewed within the Firebase console by selecting the project and clicking on the Database option in the left-hand navigation panel followed by the Rules tab in the main panel as illustrated in Figure 22-1 below:


Firebase database rules console.png

Figure 22-1


The rules may be changed within this screen simply by editing the declarations. Once modifications have been made, the console will display buttons providing the option to either discard the changes or publish them so that they become active. A simulator is also provided that allows the rules to be tested before they are published. If the rules contain syntax errors, these will be reported when the Publish button is clicked.

By default, all new Firebase projects are configured with the following database rules:

 {
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

When a user is authenticated, the auth variable referenced in the above rules will contain the user’s authentication identity. A null value, on the other hand, indicates that the current user is not signed in using any of the Firebase authentication providers. As declared, these rules dictate that only authenticated app users are allowed read and write access to the project’s realtime database data.

To test these rules, click on the Simulator button, verify that the Read option is selected and the Authenticated switch turned off before clicking on the Run button:


Firebase database rules simulator.png

Figure 22-2


The console will display a status bar across the top of the rules panel indicating that the read request has been denied. Clicking on the Details button will display a panel containing more information on the simulation results.

Remaining within the Rules screen, edit the .read rule to allow full read access to the database as follows:

{
  "rules": {
    ".read": true,
    ".write": "auth != null"
  }
} 

With the rule changed (there is no need to publish rules in order to test them) click on the Run button in the simulator panel and note that the status bar changes to indicate the read operation was permitted.

Since only the read rule was changed, writing to the database will still require that the user be authenticated. To verify this is the case, select the Write simulation type in the simulator panel, move the Authenticated switch to the on position and choose an authentication provider from the drop down menu. Finally, click on the Run button and note that the write request was allowed.

Understanding Database Rule Types

Database rules are grouped into the following four categories:


Read and Writing Rules

As previously outlined, the .read and .write rule types are used to declare the circumstances under which the data in a realtime database may be read and written by users.

Data Validation Rules

The .validate rule type allows rules to be declared that validate data values before they are written to the database. This provides a flexible way to ensure that any data being written to the database conforms to specific formats and structures. Validation may, for example, be used to ensure that the value written to a particular database node is of type String and does not exceed a specific length, or whether a set of child node requirements have been met.

Indexing Rules

The Firebase database allows child nodes to be specified as indexes. This can be useful for speeding up ordering and querying operations in databases containing large amounts of data. The .indexOn rule type provides a mechanism by which you, as the app developer, notify Firebase of the database nodes to be indexed.

Structuring Database Rules

So far in this chapter the only rules that have been explored have applied to the entire database tree. Very rarely will this “all or nothing” approach to implementing database rules be sufficient. In practice, rules will typically need to be applied in different ways to different parts of a database hierarchy. A user’s profile information might, for example, be restricted such that it is only accessible to that user, while other data belonging to the user may be required to be accessible to other users of the app.

Database rules are applied in much the same way that Firebase databases are structured, using paths to define how and where rules are to be enforced. Consider the database structure illustrated in the following figure:


Firebase database rules tree.png

Figure 22-3


Now assume that the following database rules have been declared for the project within the Firebase console:

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

Since no path is specified, the above rules apply to the root of the tree. A key point to be aware of when working with rules is that read and write rules cascade (unlike validation rules which do not cascade). This essentially means that rules defined at a specific point in the database tree are also applied to all descendent child nodes. As currently configured, all users have read (but not write) permission to all nodes in the tree. Instead of allowing read access to all data for all users, the rules could instead be changed to allow all users to read the message data while permitting only authenticated users to access the profile data. This involves mirroring the data structure when declaring the database rules.

The JSON tree for the database shown in Figure 22-3 above reads as follows:

{
  "data" : {
    "messages" : {
      "message1" : {
        "content" : "hello",
        "title" : "Hi"
      }
    },
    "profile" : {
      "email" : "[email protected]",
      "name" : "Fred"
    }
  }
}

If we want to create rules that prohibit writing, but allow reading of messages data, the rules would need to be declared as follows:

{
  "rules": {
    ".write" : false,
    "data" : {
      "messages" : {
         ".read" : true 
      }
    }
  }
} 

To test out these rules, enter them into the Rules panel of the Firebase console and display the simulator panel. Within the simulator, select the Read simulation type and enter /data/messages/message1 into the Location field as shown in Figure 22-4:


Firebase database rules simulator settings.png

Figure 22-4

Click on the Run button and verify that the read request was allowed. Next, change the location to /data/profile/email and run the simulation again. This time the request will be denied because read permission has been configured only for the messages branch of the tree.

Modify the rules once again to add read permission to the profile node, this time restricting access to authenticated users only:

{
  "rules": {
    ".write" : false,
    "data" : {
      "messages" : {
      ".read" : true 
      },
      "profile" : {
       ".read" : "auth != null"
      }
    }
  }
}

Within the simulator panel, enable authentication and attempt a read operation on the /data/profile/email tree location. The request should now be allowed.

As a final test, turn off authentication in the simulator, re-run the test and verify that read access is now denied.

Securing User Data

An important part of implementing database rules involves ensuring that data that is private to a user is not accessible to any other users. The key to this involves the use of Firebase Authentication together with the auth variable. The auth variable is one of a number of predefined variables available for use within database rules. It contains information about the authentication provider used, the Firebase Auth ID token and, most importantly, the user’s unique ID (uid). This allows rules to be defined which, when applied to an appropriately structured database tree, restrict access to data based on the user identity.

As outlined in the chapter entitled An Introduction to the Firebase Realtime Database, uid information can be used to separate user data. Figure 22 5, for example, shows a basic example of this approach to data separation:


Firebase database rules tree diagram.png

Figure 22-5


Since the uids are contained within the database, the database rules can be configured to compare the uid stored in the database against the uid of the current user, thereby ensuring that only the owner of the data is given access:

{
  "rules": {
    "profiles": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid"
      }
    }
  }
}

The above example establishes a rule that checks the uid in the profiles section of the database. If the auth variable is null (in other words the user has not been authenticated), or the uid read from the database node for which access is being requested does not match the uid of the current user access is denied. In order to fully explain the way in which the rule works, it is first necessary to understand the concept of $ variables.

$ Variables

In establishing the rule outlined in the previous section, use is made of a variable named $uid. Variables of this type, referred to as $ variables or capture variables, are used to represent the value assigned to a node location within a requested database path. The path being requested in this case might, for example, read as follows:

/profiles/userid/name

The value assigned to the userid key will, of course, depend on which node is being read (since each user ID node will have a unique value assigned to it). The $ variable provides a way to store the value assigned to the key and reference it within rule expressions.

A $ variable can be given any name as long as it is prefixed with a $ character, though a name that clearly describes the value is recommended. In the case of the $uid example, the sequence of events when an app tries to read a profile entry from the database will unfold as follows:

1. The app will request access to one of the userid nodes in the profiles branch of the tree.

2. Firebase will receive the request and refer to the database rules for the Firebase project associated with the app.

3. The $uid variable will be assigned the uid value stored in the requested database node.

4. The rule compares the uid from the requested database node with the uid of the current user and either allows or denies access to that section of the tree.

5. Firebase either returns the requested data or reports an access denial depending on whether the user’s ID matches the uid stored in the userid node of the requested database path.

Predefined Variables

In addition to allowing $ variables to be created, Firebase also provides a number of pre-defined variables available for use when declaring database rules, including the auth variable covered earlier in the chapter. The full list of pre-defined variables is as follows:

auth – Contains data if the current user has signed in using a Firebase Authentication provider. The variable contains fields for the authentication provider used, the uid and the Firebase Auth ID token. A null value assigned to this variable indicates that the user has not been authenticated.

now – The current time in milliseconds elapsed since 1 January 1970.

data – A snapshot of the current data in the database associated with the current read or write request. The data is supplied in the form of a RuleDataSnapshot instance on which a range of methods can be called to extract and identify the data content.

newData - A RuleDataSnapshot instance containing the new data to be written to the database in the event that the write request is approved.

root – RuleDataSnapshot of the root of the current database tree.

RuleDataSnapShot Methods

Both the data and newData predefined variables provide the rule with database data in the form of a RuleDataSnapshot instance. RuleDataSnapshot provides a range of methods (Table 22-1) that may called within the rule.

Method Description
child() Returns a RuleDataSnapshot of the data at the specified path.
parent() Returns a RuleDataSnapshot of the parent node of the current path location.
hasChild(childPath) Returns a Boolean value indicating whether or not the specified child exists.
hasChildren([children]) Returns a Boolean value indicating whether or not the children specified in the array exist.
exists() Returns a Boolean value indicating whether the RuleDataSnapshot contains any data.
getPriority() Returns the priority of the data contained in the snapshot.
isNumber() Returns a true value if the RuleDataSnapshot is a numeric value.
isString() Returns a true value if the RuleDataSnapshot contains a String value.
isBoolean() Returns a true value if the RuleDataSnapshot contains a Boolean value.
isVal() Used in conjunction with the child() method to extract a value from a RuleDataSnapshot.

Table 22-1

Combining the predefined variable with these RuleDataSnapshot methods provides significant flexibility when writing database rules. The following rule, for example, only allows write access if the node at path /profiles/userid/email exists:

".write" : "data.child('profiles').child('userid').child('email').exists()"

Similarly, the following rule allows write access if the value of the node at /profiles/userid/name is of type string:

".write" : "data.child('profiles').child('userid').child('name').isString()"

The val() method is used in the rule below to restrict read access unless the value assigned to the node at /profiles/userid/name is a set to “John Wilson”:

".read" : "data.child('profiles').child('userid').child('name').val() 
		=== 'John Wilson'"

The above examples test whether data already contained within the database meets specified criteria. The newData variable can be used in a similar manner to verify that the data about to be written to the database complies with the database rule declaration. In the following example, the rule requires that the data being written to the database contain a string value at the path /data/username and that the existing database has a true Boolean value at the /data/settings/updates key path:

".write" : "newData.child('data').child('username').isString() && 
    data.child('data').child('settings').child('updates').val() === true"

When testing complex .write rules it is useful to know that these can also be tested with real data within the Firebase console rules simulator. When the Write simulation type is selected, an additional field is displayed titled Data (JSON) into which JSON data structures may be entered to be used during the write simulations:


Firebase database rules simulator json.png

Figure 22-6

Data Validation Rules

The purpose of data validation rules is to ensure that data is formatted and structured correctly before it is written to the database. Validation rules are enforced only after requirements have been met for .write rules. The .validate rule type differs from .write and .read rules in that validation rules do not cascade to child nodes allowing validation to be enforced with greater precision. Otherwise, validation rules have access to the same predefined variables and methods as the read and write rule types covered earlier in the chapter.

The following validation rule, for example, verifies that the value being written to the database at path location /data/username is a string containing less than 30 characters:

{
  "rules": {
      ".write": true,
      ".validate" : "newData.child('data').child('username').isString() && 
               newData.child('data').child('username').val().length < 30"
  }
}

As a general guideline, validation rules should not be used as a substitute for validating user input within the app. It is much more efficient to catch formatting errors within the app than to rely entirely on the Firebase database rules system to catch errors and pass them back to the app.

Indexing Rules

The Firebase realtime database allows data to be queried and ordered based on key and value data. To make sure that the performance of these operations do not degrade as the database grows in size, Google recommends the use of indexing rules to declare any nodes that are likely to be used frequently to query and order data.

Indexing rules are declared using the .indexOn rule type and can be implemented for ordering either by child or value.

Consider the following sample data structure for a database designed to store the driving range and pricing of electric and hybrid cars:

{
"cars" : {
	"Model S" : {
		"range" : 210,
		"price" : 68000
        },
	"Leaf" : {
		"range" : 107,
		"price" : 30000
	},
	"Volt" : {
		"range" : 420,
		"price" : 33000
	}
    }
}

The model names are categorized as key only nodes (in other words the key essentially serves as both the key and the value, much like user IDs in earlier examples). Nodes of this type are indexed automatically by Firebase and do not need to be indexed specifically in indexing rule declarations. If queries are likely to be performed on the price and range child nodes, however, an indexing rule could be declared as follows:

{
  "rules": {
    "cars": {
        ".indexOn": ["range", "price"]
    }
  }
}

By default, Firebase assumes that indexes are to be created for ordering by child which is appropriate for the above rule. In other situations it may make more sense to index by value. The following JSON represents a different data structure where key-value pairs are used to store the model name and price of each car:

{
    "cars" : {
	"Model S" : 68000,
        "Model X" : 90000,
	"Leaf" : 30000,
	"Volt" : 33000
    }
}

If an app needed to list cars ordered by price from lowest to highest, it makes more sense to index the value (i.e. the price) than the key (the car model name). To achieve this, the .value keyword is used when declaring the rule:

{
  "rules": {
    "cars": {
        ".indexOn" : ".value"
    }
  }
}

Summary

Firebase provides three types of database rules to control read and write access, data validation and to designate nodes to be indexed for improved querying and data ordering performance. These rules may be configured using either the Firebase console or command-line tool and are declared using JSON, much like database tree structures. A wide range of variables and methods are available for use within declarations, allowing for considerable flexibility in controlling data access and maintaining database integrity and performance.




PreviousTable of ContentsNext
Reading Firebase Realtime Database DataWorking with Firebase Realtime Database Lists