Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use firebase rule to check is user group array and record group array intersect

I have a list of records in firebase which will have a group property with zero or more groups on it. I also have the firebase auth object which will also have zero or more groups on it as well. I would like to set up a .read firebase rule for my records that will check if the two have at lease one group that exists in both lists.

Put another way I have a user that has an array of groups that have been assigned to it. I have some records that also has some list of groups on them that specify what groups the user must have to access them. If the logged in user tries to access the record, I want to make sure that the user has at least one group that the record requires.

On the client I would do something like _.intersect(userGroups, recordGroups).length > 0

I'm not sure how I would do this in a firebase rule expression. It would be cool if it worked something like this.

Record:

{
  someData: "test"
  groups: ['foo', 'bar']
}

Firebase Auth Object:

{
  userName: "Bob",
  groups: ['foo', 'bar']
}

Rule Data:

{
  "rules": {
    "records": {
      "$recordId": {
        ".read": "data.child('groups').intersectsWith(auth.groups)"
      }
    }
  }
}

Thanks.

Update: I think that if hasChildren() used || instead of && I could put the group names in they key position and check for their existence this way. Something like "data.child('groups').hasChildren(auth.groups, 'or')"

Where Record:

{
  someData: "test"
  groups: {
    'foo': '',
    'bar': ''
  }
}

Update2: Based off Kato's comment & link I realize that even if hasChildren could do OR it still wouldn't work quite right. Requests for individual records would work but requests for all records would error if the current user didn't have access to every record.

It is still not clear how you would structure data to make this work. If a record could belong to many groups how would that work? This is a very common scenario(basically how linux group permissions work) so I can't be the only one trying to do this. Anyone have any ideas/examples of how to accomplish this in firebase?

like image 593
yodaisgreen Avatar asked May 11 '15 05:05

yodaisgreen


2 Answers

At the current moment, I believe it's impossible. There's a limited number of variables, methods, and operators allowed, listed here:

Firebase Security Rules API

Since function definitions are not allowed in the rules, you can't do anything fancy like call array.some(callback) on an array to do the matching yourself.

You have three options that I know of:

1) Copy data so you don't need to do the check. This is what I did in my project: I wanted some user data (names) available to users that shared a network in their network lists. Originally I wanted to check both member's network lists to see if there was at least one match. Eventually I realized it would be easier to just save each user's name as part of the network data so there wouldn't have to be a user look up requiring this odd permissions. I don't know enough about your data to suggest what you need to copy.

2) Use strings instead of arrays. You can turn one string into a regex (or just save it in regex format) and use it to search the other string for a match.Firebase DB Regex Docs

3) If you have enough weird cases like this, actually run a server that validates the request in a custom fashion. In the DB, just allow permissions to your server. You could use Firebase Cloud Functions or roll your own server that uses the Firebase Admin SDK

like image 63
ansorensen Avatar answered Oct 05 '22 22:10

ansorensen


Nowadays, there's another possibility: to use Firestore to deliver your content, possibly in sync with the Realtime Database.

In Firestore, you can create rules like this:

function hasAccessTo(permissionList) {
  return get(/databases/$(database)/documents/permissions/$(request.auth.uid))
      .data.userPermissions.keys().hasAny(permissionList)
}

match /content/{itemId} {
  allow read: if hasAccessTo(resource.data.permissions.keys());
}

The following data would allow a read of $CONTENTID by $UID, because the user permissions set intersects with the possible permissions required to access the content (with access123). My scenario is that a piece of content can be unlocked by multiple In-App Purchases.

{
  permissions: { 
    $UID: { userPermissions: { access123:true, access456:true } },
    ...
  },
  content: { 
    $CONTENTID: { ..., permissions: { access123, access789 } },
    ...
  }
}

For a progressive migration, you can keep data in sync between the Realtime Database and Firestore by using a one-way Cloud Function like this for example:

exports.fsyncContent = functions.database
  .ref("/content/{itemId}")
  .onWrite((snapshot, context) => {
    const item = snapshot.after.val();
    return admin
      .firestore()
      .collection("content")
      .doc(context.params.itemId)
      .set(item);
  });
like image 28
now Avatar answered Oct 05 '22 23:10

now