Skip to content

Instantly share code, notes, and snippets.

@nilsreichardt
Created May 27, 2021 16:47
Show Gist options
  • Save nilsreichardt/9f72898f8bf2aa655d0ef2cce74ac2cf to your computer and use it in GitHub Desktop.
Save nilsreichardt/9f72898f8bf2aa655d0ef2cce74ac2cf to your computer and use it in GitHub Desktop.
This script will generate the Cloud Firestore rules bases on multiple protobuf files.
const execSync = require('child_process').execSync;
const fs = require("fs");
const path = require("path");
const rulesPath = path.join(__dirname, "..", "rules", "firestore.rules");
function getRules() {
return fs.readFileSync(rulesPath).toString();
}
// Recursively find all files in a directory with given extension in node.js
// Copied from https://gist.github.com/victorsollozzo/4134793
function recFindByExt(base, ext, files, result) {
files = files || fs.readdirSync(base)
result = result || []
files.forEach(
function (file) {
var newbase = path.join(base, file)
if (fs.statSync(newbase).isDirectory()) {
result = recFindByExt(newbase, ext, fs.readdirSync(newbase), result)
}
else {
if (file.substr(-1 * (ext.length + 1)) == '.' + ext) {
result.push(newbase)
}
}
}
)
return result
}
// Store current security rules as variable.
const rules = getRules();
var rulesWithoutProtobuf;
if (rules.indexOf("// @@START_GENERATED_FUNCTIONS@@") == -1) {
// There is are no generated protobuf in the firestore.rules file
rulesWithoutProtobuf = rules;
} else {
// Removing the protobuf methods
rulesWithoutProtobuf = rules.substring(rules.lastIndexOf("@") + 3, rules.length);
}
const protobuf_files = recFindByExt('src/protobuf', 'proto');
// Generate Protocol Buffers: The following lines will read a proto files from
// "src/protobuf", generate the rules, copy it and repeat this with all proto
// files in this directory.
var protobuf = "";
protobuf_files.forEach((file) => {
execSync(`protoc --firebase_rules_out=src/rules ${file}`, { encoding: 'utf-8' }); // the default is 'buffer'
const generatedRules = getRules();
protobuf += generatedRules.substring(33, generatedRules.indexOf("// @@END_GENERATED_FUNCTIONS@@") - 1) + "\n";
});
// Now we are building us our new firestore.rules file with the protobuf methods
// and our security rules.
const newRules = "// @@START_GENERATED_FUNCTIONS@@\n" + protobuf + "// @@END_GENERATED_FUNCTIONS@@\n\n" + rulesWithoutProtobuf;
fs.writeFileSync(rulesPath, newRules);
@nilsreichardt
Copy link
Author

I wrote a little, which generate me my firestore.rules from multiple .proto files. It also keeps the security rules current rules in firestore.rules.

This is my folder structure:

cloud_firestore
   src
     protobuf
       - person.proto
       - user.proto
     rules
       - firestore.rules
     scripts
       - generate_protobuf_methods.js
     test
       - (my unit tests)

This is my firestore.rules before running the script:

// Start your rules (these don't get generated!)
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read: if request.auth.uid == userId;
      allow write: if isPersonMessage(request.resource.data) &&
                      request.auth.uid == userId;
    }
  }
}

This is the result after running my script (node src/scripts/generate_protobuf_methods.js):

// @@START_GENERATED_FUNCTIONS@@
function isPersonMessage(resource) {
  return resource.keys().hasAll(['name']) &&
          (resource.keys().hasOnly(['name','phone','email'])) &&
          ((resource.name is string)) &&
          ((!resource.keys().hasAny(['email'])) || (resource.email is string)) &&
          ((!resource.keys().hasAny(['phone'])) || (isPerson_PhoneNumberMessage(resource.phone)));
}
function isPerson_PhoneNumberMessage(resource) {
  return resource.keys().hasAll([]) &&
          (resource.keys().hasOnly(['type','number'])) &&
          ((!resource.keys().hasAny(['number'])) || (resource.number is string)) &&
          ((!resource.keys().hasAny(['type'])) || (isPerson_PhoneTypeEnum(resource.type)));
}
function isPerson_PhoneTypeEnum(resource) {
  return resource == 0 ||
          resource == 1 ||
          resource == 2;
}
function isUserMessage(resource) {
  return resource.keys().hasAll([]) &&
          (resource.keys().hasOnly(['phone','email'])) &&
          ((!resource.keys().hasAny(['email'])) || (resource.email is string)) &&
          ((!resource.keys().hasAny(['phone'])) || (isUser_UserNumberMessage(resource.phone)));
}
function isUser_UserNumberMessage(resource) {
  return resource.keys().hasAll([]) &&
          (resource.keys().hasOnly(['type','number'])) &&
          ((!resource.keys().hasAny(['number'])) || (resource.number is string)) &&
          ((!resource.keys().hasAny(['type'])) || (isUser_UserTypeEnum(resource.type)));
}
function isUser_UserTypeEnum(resource) {
  return resource == 0 ||
          resource == 1 ||
          resource == 2;
}
// @@END_GENERATED_FUNCTIONS@@

// Start your rules (these don't get generated!)
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read: if request.auth.uid == userId;
      allow write: if isPersonMessage(request.resource.data) &&
                      request.auth.uid == userId;
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment