Firestore Security Rules: get path as string

Viewed 646

With Firestore Security Rules (version 2), how can I turn a Path object into a String? The entire path as one String, not the individual segments.

I'm trying to write a generic function to use in various Match statements. Something like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // "pathobj" is a rules.firestore.Path object
    function getPathAsString(pathobj) {
      // Does not work:
      return pathobj.toString();
    }

    // I would expect the following to evaluate to True:
    match /foo/{fooid} {
      allow read: if getPathAsString(path('one')) == 'one'
                  && getPathAsString(path('one/!two')) == 'one/!two';
    }
  }
}

Does anyone know if this is possible? I read the documentation on Path and have tried various things in the Firestore Rules Simulator, all with no success.

2 Answers

As far I can tell, there isn't a direct way to turn a path into a string; however, after trying for quite some time to do exactly this, I came up with a few workarounds depending on exactly what you want to do.

You know how many path segments there should be:

The most complete way would be to access each path segment and concatenate them with slashes.

let originalPath = path('one/!two');
let pathString = originalPath[0] + '/' + originalPath[1];

This only works if you know the number of path segments, though. I tried concatenating an arbitrarily large number of path segments in the hopes that any that go beyond the path length would be ignored, but it doesn't seem to work (although it's hard to tell because the Firestore security rules emulator is terrible with debugging).

You only need a path comparison:

It may be that you don't actually need to use strings at all. In your example, you could get the proper results if you just hardcoded a path instead of a string, i.e. allow read: if path('one') == /one; and allow read: if path('one/!two') == /one/!two;

There's a different way of doing what you want:

The least helpful of these workarounds is the idea that there's an entirely different way of doing what you want, and you just have to figure out what that is. I can't give you any more advice on what your intended use may be, but I can tell you what happened in my pursuit of this answer: I was trying to find a way of determining whether a user was trying to read or write documents in the file locations devoted to them, and at first, I was trying to do something along the lines of a regular expression match which would require a string. However, after spending far too long trying to figure out how to make that work, it occurred to me that I only needed to compare the first segment of the wildcard path ({docPath=**}) to the request's UID, and my complicated function was replaced with one simple line of code. Even if it seems like strings are the only solution, there may yet still be other ways of doing what you want to do. I do agree, however, that there should be an easy way of converting a path into a string.

This will return the resource path as rules.List, with some caveats.

// firestore.rules

// Workaround for rules.Path not having size(). Assumes docs have
// unique ids across collections and max n path segments.
function resourcePathToList(rsc) {
  let p = rsc.__name__;
  let id = rsc.id;
  return
    p[0] == id ? [p[0]] :
    p[1] == id ? [p[0],p[1]] :
    p[2] == id ? [p[0],p[1],p[2]] :
    p[3] == id ? [p[0],p[1],p[2],p[3]] :
    p[4] == id ? [p[0],p[1],p[2],p[3],p[4]] :
    p[5] == id ? [p[0],p[1],p[2],p[3],p[4],p[5]] :
    p[6] == id ? [p[0],p[1],p[2],p[3],p[4],p[5],p[6]] :
    p[7] == id ? [p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7]] :
    p[8] == id ? [p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7],p[8]] :
    p[9] == id ? [p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7],p[8],p[9]] :
                 []
}

Firestore rules don't currently support converting rules.Path to rules.String (or rules.List). The rules.Path api lacks a .size() method so rules.Path values cannot be safely iterated.

Instead of parsing the resource path, it's more natural to explicitly pass the path bindings into functions, and I prefer this approach.

match /a/{a}/b/{b} {
  allow read: if someTest([a,b]) || someOtherTest(a,b)
}
Related