Aron
Aron

Reputation: 92

How do I achieve this security logic in Firebase FireStore?

The illustration below shows the logic of security rules that I need to apply for my firebase firestore.

Description My app allows user authentication, and I manually activate client accounts. Upon activation, they can add users to their database collection, as well as perform operations on their database. I have illustrated each client as 'Organization' below, and each has multiple users which should only be able to access specific parts of the database collections/documents. Each organization has an admin user who has full access over the database collection of that particular organization. Each user (from each organization) can access the node 'Elements', as well as their own UID-generated docs and collections only.

It seems like I need custom claim auths to achieve that. I wonder if some alternatives exist, like adding some specific security rules in my fireStore to make this work, or any other alternatives besides the firebase admin sdk tools, as it's time consuming and I'm not an expert backend developer.

Other Details I use flutter. My app allows clients to authenticate and create their database collection. Clients add users (as team members) with different roles (which affect what collection/document they can access) This security rules logic is the main thing I'm stuck on right now.

I highly appreciate suggestions and examples that might shed light on my way of achieving that.

illustration enter image description here

My FireStore security rules right now

enter image description here

Upvotes: 1

Views: 181

Answers (1)

Peter Obiechina
Peter Obiechina

Reputation: 2835

One possible solution: Each organisation should contain a list of strings (userIds), and only users with userId in this list can access the Organisation collection and docs.

Database structure:

organisation_1:
  userIds (field containing list of user ids - []<String>): 
  adminId (field containing admin id - String):
  admin (collection):
  users (collection):
  elements (collection):
  premium (collection):

organisation_2:

Security rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function isLoggedIn() {
      // only true if user is logged in
      return request.auth != null;
    }

    match /organisation/{organisationId} {
      function prefix() {
        return /databases/$(database)/documents/organisation/$(organisationId);
      }
      function isAdmin() {
        // only true if admin
        return isLoggedIn() && request.auth.uid == get(/$(prefix())).data.adminId;
      }
      function isUser() {
        // only true if user
        return isLoggedIn() && request.auth.uid in get(/$(prefix())).data.usersId;
      }
      function isDataOwner(dataId) {
        // only true if user is admin or userId is the document id.
        // this rule should allow each user access to their own UID-
        // generated docs and collections only
        return isLoggedIn() && (isAdmin() || dataId == request.auth.uid);
      }

      // since userIds list is organisation data, we should prevent any 
      // user from editing it (or only allow admin to edit it).
      // if you are using cloud function to update userIds list, set this 
      // to false. Cloud function does not need access.
      allow write: if isAdmin();
      allow read: if true;

      match /Elements/{elementsId=**} {
        // allow access to the entire Elements collection and 
        // subcollections if isAdmin or isUser.
        allow read, write: if isAdmin() || isUser();
      }
      match /settings/{userId} {
        // allow access only if document id is your userId
        allow read, write: if isDataOwner(userId);
      } 
      match /adminDocs/{docId} {
        // only allow admin
        allow read, write: if isAdmin();
      }
    }
  }
}

Then you can use a cloud function to keep your userIds list up to date. Example:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const db = admin.firestore();

exports.onCreate = functions.firestore
  .document("/organisation/{organisationId}/users/{userId}")
  .onCreate((_, context) => {
    const params = context.params;
    const organisationId = params.organisationId;
    const userId = params.userId;

    const data = {
      userIds: admin.firestore.FieldValue.arrayUnion(userId),
    };
    return db.doc(`/organisation/${organisationId}`)
      .set(data, { merge: true });
  });

exports.onDelete = functions.firestore
  .document("/organisation/{organisationId}/users/{userId}")
  onDelete((_, context) => {
    const params = context.params;
    const organisationId = params.organisationId;
    const userId = params.userId;

    const data = {
      userIds: admin.firestore.FieldValue.arrayRemove(userId),
    };
    return db.doc(`/organisation/${organisationId}`)
      .set(data, { merge: true });
  });

You can avoid this cloud function by simply adding userid to userId list when admin creates new user. But cloud function is cleaner (Use it).

UPDATE

$(database) is the name of your firestore database.

{database} (line 3 in my security rules) tells rules to save the actual name of database into database variable.

prefix() returns the path to the organisation document.

If a user tries to access his document in this path organisation/12345/users/67890, then $(database) is default and prefix() returns /databases/default/documents/organisation/12345/

You can go to firestore docs to see how $(database) and path (prefix()) is being used.

Upvotes: 2

Related Questions