Matthew Rideout
Matthew Rideout

Reputation: 8538

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;
}

Upvotes: 9

Views: 5176

Answers (3)

Matthew Rideout
Matthew Rideout

Reputation: 8538

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.

Upvotes: 16

Flo
Flo

Reputation: 21

The solution i came up with. My tests showed it's not possible to create other user-docs than the own uid and it prevents normal users to change any admin state.



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

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

        match /users/{user} {

          // allow updates to own user-doc
          allow read, update, delete: if request.auth.uid == user &&

            // allow updates to own user-doc if "isAdmin" field is the same as before the update (in case user was already admin)
            (request.resource.data.isAdmin == resource.data.isAdmin ||

                // or allow updates if "isAdmin" will be set to false
                request.resource.data.isAdmin == false ||

                // or allow updates if no "isAdmin" field exists after the update
                !("isAdmin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data)
            );

          // allow creation of user-doc with own uid and no others       
          allow create: if request.auth.uid == user &&

            // if no "isAdmin" field is set
            !("isAdmin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);

          // give full access to admins
          allow read, write: if isAdmin();
        }
      }
    }

Upvotes: 0

Hetote
Hetote

Reputation: 340

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...

Upvotes: 1

Related Questions