Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firestore Security Rules: Allow User To Create Doc Only If New Doc ID is same as User ID

When users log in for the first time, I need to also call a function that creates a document in my firestore users collection to store their profile data. Using Web SDK.

(I was previously using a new user triggered event with firebase functions, but it was too slow to wait for a cold function to spin up).

Security Rule Requirements

Needs to ensure that the user can only create a document if the document id is the same as their user id (to prevent the user from creating other docs). Needs to ensure that this doc doesn't already exist.

Attempt - Works In Simulator, Not IRL

These tests pass in the simulator, but not IRL.

// Allow users to create a doc if the doc ID == their user id
allow create: if path("/databases/" + database + "/documents/users/" + request.auth.uid) == request.path;

OR

allow create: if /databases/$(database)/documents/users/$(request.auth.uid) == request.resource['__name__']

Have also tried this (again, works in simulator, but not IRL)

match /users/{userId} {
    // Allow users to read their own profile
    allow create: if request.auth.uid == userId;
}
like image 486
Matthew Rideout Avatar asked Oct 25 '18 15:10

Matthew Rideout


2 Answers

Update

I recently had to update my rule set because of some changes to the way firestore rules worked, and changes in how the "getAfter" function works. Specifically, I am now able to use request.resource for data comarisons. Anyways, it appears that I can accomplish my goals with simpler rules now so I thought I'd update this answer and share.

Goals

  • User can create a document, only if the new document ID matches their user ID.
  • User cannot declare themselves an "admin", block create / update / write requests if "admin" is a field (unless they are already an admin)
service cloud.firestore {
  match /databases/{database}/documents {
  
    // Allow users to create a document for themselves in the users collection
    match /users/{document=**} {
      allow create: if request.resource.id == request.auth.uid &&
        !("admin" in request.resource.data);
    }
    
    // Allow users to read, write, update documents that have the same ID as their user id
    match /users/{userId} {   
        // Allow users to read their own profile (doc id same as user id)
      allow read: if request.auth.uid == userId;
      
      // Allow users to write / update their own profile as long as no "admin"
      // field is trying to be added or created - unless they are already an admin
      allow write, update: if request.auth.uid == userId &&
        (
          !("admin" in request.resource.data) ||
          get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true // allow admin to update their own profile
        )

      // Allow users to read their own feeds
      match /feeds/{document=**} {
        allow read: if request.auth.uid == userId;
      } 
    }
  }
}

Old Answer

So I figured out how to do this in a workaround way. I also had some additional write / update conditions that prevent the user from changing their permission level. This was for some reason, preventing any "creates" from happening. So I had to mirror the same conditions in create, and the write / update rules. For some reason this was necessary.

This new rule structure accomplishes the following

First Section, for create rule

  • allows the only authenticated users to create documents only in the "users" collection (during the user setup process, a document is created automatically with the same ID as their user id).
  • does not allow creation of a document containing the "admin" field, which would suggest they are trying to gain admin access.
  • it seems that validating the id of the document during creation is not possible, hence additional write / update rules below

Second Section - read, update, write

  • allows users to read / write / update only documents that have the same ID as their user id (user trying to create a document with an ID other than their user id will fail, also prevents the user from spamming creation of tons of docs by manipulating the client-side JS request.)
  • does not allow users to write / update their profile to include the "admin" field

Rules

service cloud.firestore {

      match /databases/{database}/documents {
      
        // Allow users to create documents in the user's collection
        match /users/{document=**} {
          allow create: if request.auth.uid != null &&
            !("admin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
        }
        
        // Allow users to read, write, update documents that have the same ID as their user id
        match /users/{userId} {   
            // Allow users to read their own profile (doc id same as user id)
          allow read: if request.auth.uid == userId;
          
          // Allow users to write / update their own profile as long as no "admin" field is trying to be added or created
          allow write, update: if request.auth.uid == userId &&
            !("admin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
        }
      }
    }

PS This was not intuitive at all, so if someone has a better workaround, please post it. Also, I'm really hoping that once firestore 1.0 is out, it will bring with it some huge improvements to rules and rule documentation.

like image 92
Matthew Rideout Avatar answered Nov 09 '22 07:11

Matthew Rideout


A little bit late, but I manage to tweak one of your possible solutions and make it work:

allow create: if path("/databases/(default)/documents/users/" + request.auth.uid) == request.path;

Just had to replace the database variable with (default). Yes, not fancy...

like image 1
Hetote Avatar answered Nov 09 '22 07:11

Hetote