The way to split the file of Cloud Firestore Security Rules
September 26, 2021
This article is a translation of a Japanese article I posted earlier.
firestore.rules
in the Firebase project generated by the Firebase CLI is the file that defines the security rules.
The initial contents are as follows.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
All the security rules will be written in this file, but it’s not hard to imagine that the content of this file will become so large that you will get tired of it one day.
“OK, Let’s split the file!” That’s what everyone thinks, but unfortunately this is not a JavaScript file.
This is a .rules
file, which is written in a Firestore-specific way, so-called DSL.
So, unfortunately, there is no import mechanism. I think there is a lot of expectation for a file splitting mechanism, but I wonder why the official does not provide it. In fact, you can find a good number of people who have the same feeling.
Here, one of the respondents above has created a package to concatenate files of security rules.
I saw these and had an idea.
Why don’t you create a security rule binding shell script?
All I wanted to do was to combine multiple files simply into a single firestore.rules
, so I thought a shell script would be sufficient for that kind of processing.
I’m going to introduce a script for those who have the same problem, but first let me make an excuse. First of all, the code is messy because I made it as long as I could use it, and secondly, this is the second or third time I’ve written a shell script, so I don’t even know what a clean shell script is.
But don’t worry, I guaranteed it to work correctly.
If you are good at writing shell scripts, feel free to customize it as you like.
TL;DR #
I list each file first, then explain later.
Structure and files #
├ concat
└ rules/
├ header.rules
├ footer.rules
├ routes.rules
├ helper.rules
└ functions/
├ xxx.rules
├ yyy.rules
└ zzz.rules
concat #
#!/bin/bash
readonly rules_dir='/rules'
readonly header_file='header.rules'
readonly footer_file='footer.rules'
readonly route_file='routes.rules'
readonly helper_file='helper.rules'
readonly funcs_dir='/functions'
readonly in_files=(
'xxx.rules'
'yyy.rules'
'zzz.rules'
)
readonly out_file='firestore.rules'
print_source_files() {
echo $route_file
for file in ${in_files[@]}; do
echo $file
done
echo $helper_file
}
main() {
readonly work_file='firestore.rules.work'
touch $work_file
cat ".${rules_dir}/${header_file}" >> $work_file
cat ".${rules_dir}/${route_file}" | sed -e '/^$/d' | sed 's/^/ /g' >> $work_file
echo >> $work_file
for file in ${in_files[@]}; do
echo " // ${file} //" >> $work_file
cat ".${rules_dir}${funcs_dir}/${file}" | sed -e '/^$/d' | sed 's/^/ /g' >> $work_file
echo >> $work_file
done
echo " // ${helper_file} //" >> $work_file
cat ".${rules_dir}/${helper_file}" | sed -e '/^$/d' | sed 's/^/ /g' >> $work_file
cat ".${rules_dir}/${footer_file}" >> $work_file
cat $work_file > $out_file
rm $work_file
}
print_source_files
main
header.rules #
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
footer.rules #
}
}
routes.rules #
match /xxx/{xxxId} {
allow get: if getXxx();
}
match /yyy/{yyyId} {
allow get: if getYyy(yyyId);
match /zzz/{zzzId} {
allow get: if getZzz(yyyId, zzzId);
}
}
healper.rules #
// @return {boolean}
function isAuthenticated() {
return request.auth != null;
}
// @param {string} id - User id
// @return {boolean}
function uidMatches(id) {
return id == request.auth.uid;
}
// @param {array} paths - e.g. ['collection name', 'document id']
// @return {boolean}
function docExists(paths) {
return exists(getPath(paths));
}
// @return {string} - Authentication UID
function getAuthUid() {
return request.auth.uid;
}
// @return {string} - Authentication Email
function getAuthEmail() {
return request.auth.token.email;
}
// @return {object} - Requested Data
function getRequestedData() {
return request.resource.data;
}
// @return {object} - Resource Data
function getResourceData() {
return resource.data;
}
// @param {array} paths - e.g. ['collection name', 'document id', 'collection name', 'document id']
// @return {path} - Document path
function getPath(paths) {
return path([['databases', database, 'documents'].join('/'), paths.join('/')].join('/'));
}
xxx.rules #
function getXxx() {
return isAuthenticated();
}
yyy.rules #
function getYyy(yyyId) {
return uidMatches(yyyId);
}
zzz.rules #
function getZzz(yyyId, zzzId) {
let authUid = getAuthUid();
let reqData = getRequestedData();
return ~ && ~ && ~; // practically, write some conditions at ~.
}
Explanation #
The concat
is the shell script to concatenate files, and the rest is the material (=splitted files) to make firestore.rules
.
The print_source_files()
in concat
is just a confirmation function to print the target files to the console, and the main()
is the actual concatenation function.
For all other files, the description is as follows
header.rules
: A file for the header section.footer.rules
: A file for the footer section.routes.rules
: A file that specifies the path to the document.helper.rules
: A file that defines helper functions (this is useful).- Each file under
functions/
is actually a function that returnstrue
orfalse
, one for each of theroutes.rules
. (The contents ofxxx.rules
,yyy.rules
, andzzz.rules
are temporary.)
Now, run the concat
and firestore.rules
will be generated.
./concat
If you get a Permission denied
message and cannot execute the file, change the permissions on the file.
chmod 755 concat
Result #
I run concat
with the above contents, and the following firestore.rules
was created.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /xxx/{xxxId} {
allow get: if getXxx();
}
match /yyy/{yyyId} {
allow get: if getYyy(yyyId);
match /zzz/{zzzId} {
allow get: if getZzz(yyyId, zzzId);
}
}
// xxx.rules //
function getXxx() {
return isAuthenticated();
}
// yyy.rules //
function getYyy(yyyId) {
return uidMatches(yyyId);
}
// zzz.rules //
function getZzz(yyyId, zzzId) {
let authUid = getAuthUid();
let reqData = getRequestedData();
return ~ && ~ && ~; // practically, write some conditions at ~.
}
// helper.rules //
// @return {boolean}
function isAuthenticated() {
return request.auth != null;
}
// @param {string} id - User id
// @return {boolean}
function uidMatches(id) {
return id == request.auth.uid;
}
// @param {array} paths - e.g. ['collection name', 'document id']
// @return {boolean}
function docExists(paths) {
return exists(getPath(paths));
}
// @return {string} - Authentication UID
function getAuthUid() {
return request.auth.uid;
}
// @return {string} - Authentication Email
function getAuthEmail() {
return request.auth.token.email;
}
// @return {object} - Requested Data
function getRequestedData() {
return request.resource.data;
}
// @return {object} - Resource Data
function getResourceData() {
return resource.data;
}
// @param {array} paths - e.g. ['collection name', 'document id', 'collection name', 'document id']
// @return {path} - Document path
function getPath(paths) {
return path([['databases', database, 'documents'].join('/'), paths.join('/')].join('/'));
}
}
}
Thank you for reading. #
I hope this article was helpful!