Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase Firestore: custom admin access

In Firebase Firestore, I'm trying to allow only (custom-assigned) admins to write/update/delete resources, and for that I've got these security rules:

service cloud.firestore {
  match /databases/{database}/documents {
    match /resources {
      allow read;
      allow write, update, delete: if get(/users/$(request.auth.uid).isAdmin);
    }
    match /resources/{resource} {
      allow read;
      allow write, update, delete: if get(/users/$(request.auth.uid).isAdmin);
    }
  }
}

I'm signing in with the user that is marked as an admin in the users collection:

users collection has a single admin

NfwIQAjfNdS85yDvd5yPVDyMTUj2 is the UID gotten from the Authentication pane:

The user exists

However, for some reason (UPDATE: reasons identified; see answer), I'm getting PERMISSION_DENIED errors when writing to the resources collection after being absolutely sure I'm signed in with the admin user.

Perhaps it is possible to view request logs from Firestore? Then I could have a look at what request.auth.uid looks like to match it up with my collections and rules.

like image 619
sindrenm Avatar asked Nov 07 '17 09:11

sindrenm


People also ask

Can I create admin panel with Firebase?

Start by creating a new app on Retool, and give this app a name. We'll call it “Firebase Admin.” Next, create a Firebase resource by clicking "create a new resource" from the Resource field at the bottom panel. Select Firebase from the options to create a Firebase resource.

How do I get firebase admin config?

Google Service AccountOpen https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk and select the project you want to generate a private key file for. Click Generate New Private Key, then confirm by clicking Generate Key. Securely store the JSON file containing the key.

Can I use firebase Admin client side?

The Firebase Admin SDK should only be run in a privileged environment, like your server or Firebase Cloud Functions. It provides direct administrative access that is not secure on the client.

How do I get firebase admin credentials?

To authenticate a service account and authorize it to access Firebase services, you must generate a private key file in JSON format. To generate a private key file for your service account: In the Firebase console, open Settings > Service Accounts. Click Generate New Private Key, then confirm by clicking Generate Key.


2 Answers

While writing my question, I made it work! I made two mistakes, both of which could have been avoided if I read the docs properly.

Firstly, all calls to the service-defined function get needs to prefix the path with /databases/$(database)/documents/. So that this rule:

allow write: if get(/users/$(request.auth.uid)).isAdmin;

becomes this:

allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin;

It's long, I know, but that's how it is. I'm not sure why Firestore isn't able to do that by itself, though, seeing as that same path prefix will stay the same across all calls to get, but perhaps this is for some future feature that isn't ready yet, like cross-database querying or something.

Second, the get function will return a resource, which in turn you'll need to call .data on to get the actual data that it contains. Thus, instead of doing this:

get(/path/to/user/).isAdmin

you'll need to do this:

get(/path/to/user/).data.isAdmin

Now I just wish I was able to extract that logic into a user-defined function:

function isAdmin() {
  return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin;
}

But doing so results in a PERMISSION_DENIED again, and without knowing what's actually going on in the function, I'm not sure if I'll spend more time trying to figure this out now.

UPDATE: @Hareesh pointed out that functions must be defined within the scope of a matcher, so it's possible to put the function in the default top-level matcher like this:

service cloud.firestore {
  match /databases/{database}/documents {
    function isAdmin() {
      return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
    }

    // ...
  }
}
like image 179
sindrenm Avatar answered Oct 26 '22 06:10

sindrenm


Some points i noticed

match /resources is pointing to a collection, that rules has no effect on its documents. here i am quoting from the doc

Rules for collections don't apply to documents within that collection. It's unusual (and probably an error) to have a security rule that is written at the collection level instead of the document level.

so you don't have to write rules for collections

Then in the rules allow write, update, delete: you can say either allow write: or specifically allow create, update, delete: any of the three options or combine them.

try this

service cloud.firestore {
    match /databases/{database}/documents {
      match /resources/{resource} {

        function isAdmin() {
            return get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin ||
            get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin;
        }

        allow read;
        allow create, update, delete: if isAdmin();
    }
  }
}
like image 42
Hareesh Avatar answered Oct 26 '22 07:10

Hareesh