A Guide to Firebase Cloud Storage Security Rules
Previous | Table of Contents | Next |
Firebase Cloud Storage | A Firebase Cloud Storage Tutorial |
Security is of paramount importance when storing a user’s files and data in the cloud. In recognition of this, Firebase Cloud Storage provides an system of security rules that provide a flexible way to control access to stored files.
When a client makes a cloud storage request (in the form of a file upload, download or deletion) the request is passed through the rules defined for the Firebase project to which the client belongs. Based on these rules, the request will either be fulfilled or denied. Understanding how to write these rules is a key part of working with Firebase Cloud Storage.
Understanding Cloud Storage Security Rules
Security for Firebase Cloud Storage is configured using rules that are declared within the Firebase console. Each Firebase project has its own set of rules which can be viewed and edited by selecting the project within the console and navigating to the Rules page of the Storage screen as shown in Figure 34-1 below:
Figure 34-1
Rules can be defined to meet a wide range of requirements. Examples of the types of rules that may be specified include restricting access to specific users on a per file basis, allowing only files of a certain content type to be uploaded, or files of a certain size to be deleted. A rule could be declared, for example, that only allows image files to be stored in a particular folder.
By default, the following rules are configured allowing only authenticated users to read and write to the bucket:
service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if request.auth != null; } } }
In the above example, the rule uses a standard reference /b/{bucket}/o which indicates that the rules are to apply to all Google Cloud buckets belonging to the Firebase project (the use of braces indicates a wildcard, a topic which will be covered later in this chapter). The reason for the /b/ and /o is that the internal path to a file stored in a cloud bucket will typically be structured as follows:
/b/<cloud storage bucket id>/o/path/to/file/myfile.jpg
Alternatively, the rule to allow read and write access to all files and sub-directories to all users (authenticated or otherwise) would read as follows:
service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write; } } }
Security Rules Structure
As with Firebase Realtime Database rules, cloud storage rules are structured hierarchically. In the above examples, the read and write permissions applied to all files and folders within the project’s Google Cloud bucket. It helps to think of the rules in terms of “what, how and when”. The rules control what is accessible within the cloud storage bucket, how those items are protected and, finally, the circumstances when access is permitted. These three requirements are declared using the match, allows and if keywords. A typical rule element will, therefore, be structured as follows:
match /file/path/pattern { allow read, write: if <condition>; }
A single match declaration may also contain multiple allow statements. The following rule declares separate permissions for reading and writing:
match /file/path/pattern { allow read: if <condition 1>; allow write: if <condition 2>; }
The match Keyword
The match keyword is used to define the path or paths within the storage filesystem to which a rule is to apply. In the above examples, the rule applied to all files and directories contained within the bucket. Matches can also be declared to target specific files. The following rule, for example, provides read-only access for authenticated users to a file with a path that matches /profiles/photos/userphoto.png:
service firebase.storage { match /b/{bucket}/o { match /profiles/photos/userphoto.png { allow write: if request.auth != null; } } }
Match statements may also be nested as in the following example:
service firebase.storage { match /b/{bucket}/o { match /calendar { match /uservideo.3gp { allow write: if request.auth != null; } } } }
As will be covered in a later section, matching also supports the use of wildcards.
The allow Keyword
Having defined the paths for which rules are to apply using the match keyword, the allow keyword controls the kind of access permitted for any matching paths. Acceptable options are read, write or both read and write permissions.
Each match rule element will contain at least one allow statement. In the absence of an if statement, the allow permissions are applied unconditionally to matched paths, regardless of the storage request being made by the client.
The if Statement
The allow keyword transforms from a blunt instrument into a precision tool when used in conjunction with the if conditional statement. In the above examples, the if statement has been used repeatedly to allow access only to authenticated users with statements similar to the following:
allow read, write: if request.auth != null;
In this case, read and write access is only permitted when the current user has been authenticated. The authentication status is obtained by accessing the auth property of the request object.
When a client attempts to upload, download or delete a cloud storage based file, it arrives at the cloud server in the form of a request object. This object, which contains a number of properties, is available for use within the if statement as the basis on which to create rule conditions.
The auth property, for example, will be null if the user making the request has not signed in using a Firebase Authentication provider. In the event the user has been authenticated, the auth property contains the user’s uid (request.auth.uid) and the Firebase authentication token (request.auth.token).
A full list of the properties provided by the request object is contained in Table 34-1:
Property | Description |
---|---|
auth | Contains the uid and Firebase authentication token for the current user. If the user is not authenticated, this property is set to null. |
path | Contains the path at which the request is being made by the client. This is provided in the form of an array allowing individual path components to be accessed in an if statement. For example, request.path[1]. |
resource | An object containing the metadata of the file associated with the storage request. This allows information such as the file size, type, creation time and name to be accessed and used in an if statement. |
time | The current time on the server. Typically used to compare against the creation time of a file to avoid uploading of outdated files or to restrict upload and download operations at certain dates and times. |
Table 34-1
The resource property of the request object also contains a number of properties specific to the file associated with the request. Table 34 2 outlines the most commonly used resource properties:
Property | Description |
---|---|
name | The name of the file associated with the request including the full path. |
bucket | The name of the Google Cloud Storage bucket in which the file is stored. |
size | The size in bytes of the file. |
timeCreated | The time and date when the file was first created. |
updated | The time at which the file was last updated. |
contentEncoding | A string identifying the encoding of the file (for example ‘gzip’). |
contentLangauge | A string identifying the language of the file content (for example ‘en’ or ‘ja’). |
contentType | A string identifying the type of the file content (for example ‘image/jpg’). |
metadata | A string map containing developer specific metadata (if provided) included with the file. |
Table 34-2
Clearly, there are quite a few options for inclusion in an if statement so it would be beneficial to work through some example rules that make use of some of the properties outlined above. The following allow statement, for example, prevents files being uploaded that exceed 15Mb in size:
allow write: if resource.size < 15 * 1024 * 1024;
A rule to allow reading only if the content of the file is English would read as follows:
allow read: if resource.contentLanguage == 'en';
Conditions may also be combined using AND and OR operators. The following rule, for example, only allows a file to be written if the user is authenticated, the file contains a PNG image, does not exceed 25Mb in size and was created less than two hours ago:
allow write: if request.resource.size < 25 * 1024 * 1024 && request.auth != null && request.resource.contentType.matches('image/png') && resource.timeCreated < request.time + duration.value(120, "m");
Wildcard Matching
When specifying matching criteria for a rule it is often useful to use wildcards to match multiple paths. Consider, for example, the following match statement:
match /calendar/january/scenicphoto.jpg { . . }
Clearly any allow statement associated with this match will apply only to a file named scenicphoto.jpg residing in the /calendar/january directory. If the match was intended to apply to all files located in the /calendar/january directory, the match statement could read as follows:
match /calendar/january/{imageName} { . . }
The {imageName} section of the match path represents a wildcard. The expression syntax for a wildcard is as follows:
{variable name}
When included in a match path, this syntax essentially tells cloud storage to accept any value as a match and to assign that current value to a variable with the specified name. That variable can then be used within an if statement to add conditions to the rule. The following rule allows any file to be written to the /calendar/january directory as long as the name of the file being written does not exceed 10 characters in length:
match /calendar/january/{imageName} { allow write: if imageName.size() > 10; }
Wildcards may be used to replace any section of a path. Consider, for example, the following rule which allows any file to be written to any path that matches /calendar/*:
match /calendar/{monthName} { allow write: if request.auth != null; }
With the above rule applied, a client will be permitted, for example, to upload a file with any filename to directories such as /calendar/january, /calendar/february, /calendar/2017 and so on.
When working with rules it should be noted that rules do not, by default, cascade to the sub-directories of a matched directory. In other words, while the above rule would apply to directory path /calendar/2017, it would not apply to directory /calendar/2017/january. To declare a cascading wildcard it is necessary to use the multiple segment wildcard syntax (represented by =**) as follows:
match /calendar/{monthName=**} { allow write: if request.auth != null; }
The declared write permission will now cascade down through all levels of sub-directories beneath /calendar in the storage bucket. Note that the multiple segment wildcard may only be used to replace the last segment in a path. The following, therefore, is not a valid wildcard:
match /calendar/{monthName=**}/photos { // <---- Invalid wildcard . . }
Protecting User Files
An important part of implementing cloud storage security rules involves ensuring that files that are private to a user are not accessible to any other users. The key to this involves the use of Firebase Authentication together with the auth property of the storage request object.
As previously outlined, the auth property contains the Firebase Auth ID token and the user’s unique ID (uid). This allows rules to be defined which, when applied to an appropriately structured storage filesystem, restrict access to stored files based on user identity.
As with Firebase Realtime Databases, the uid information can be used to separate one user’s files from another. Consider a client app that allows each user to record and upload an introductory video about themselves. While anyone is allowed to view the video, only the owner of the video is allowed to upload and update the video file. The cloud storage filesystem structure for this app might be implemented as follows where <uid> is replaced by each user’s id:
/videos/<uid>/userIntro.3gp
Since the uids are contained within the filename path, the storage security rules can be configured to compare the uid in the path against the auth.uid property (in other words the uid of the user making the request) of the of the request object:
service firebase.storage { match /b/{bucket}/o { match /videos/{userId}/userIntro.3gp { allow read; allow write: if request.auth.uid == userId; } } }
The above example establishes a rule that uses a wildcard to assign the uid segment of the request path to a variable named userId. If the uid segment of the path does not match the uid of the current user write access is denied.
Summary
Firebase Cloud Storage rules provide a mechanism for controlling the circumstances under which files may be stored and accessed. Storage rules are declared using the match, allow and if statements to define sets of rules and the files to which those rules are to apply. As with the Firebase Realtime Database, user files can be protected by making use of Firebase Authentication.
Previous | Table of Contents | Next |
Firebase Cloud Storage | A Firebase Cloud Storage Tutorial |