How can I grant access to array of admins on Firebase database rules - firebase

I am stuck trying to allow an an array of admins access to their data.
I have a database structure like this:
{
"Respondents": {
"Acme Corp": {
"admins": ["mMK7eTrRL4UgVDh284HntNRETmx1", ""mx1TERNmMK7eTrRL4UgVDh284Hnt"],
"data": {data goes here...}
},
"Another Inc": {
"admins": ["Dh284HmMK7eTrRL4UgVDh284HntN", ""x1TERNmx1TERNmMK7eTrRL4UgVDh"],
"data": {their data goes here...}
}
}
}
And then I tried to set my rules like this
{
"rules": {
"Respondents": {
"$organisation" : {
".read": "root.child('Respondents').child($organisation).child('admins').val().includes(auth.id)",
".read": "root.child('Respondents').child($organisation).child('admins').val().includes(auth.id)"
}
}
}
}
..but that won't parse in the Firebase Database Rules editor
I get "Error saving rules - Line 7: No such method/property 'includes'", but I need something to match the user id with the array of admins.
Any experience or suggestions?

As you've found, there is no includes() operation in Firebase's security rules. This is because Firebase doesn't actually store the data as an array. If you look in the Firebase Database console or read this blog post you will see that Firebase stores it as a regular object:
"admins": {
"0": "mMK7eTrRL4UgVDh284HntNRETmx1",
"1": "mx1TERNmMK7eTrRL4UgVDh284Hnt"
}
And since that is a regular JavaScript object, there is no contains() method on it.
In general creating arrays are an anti-pattern in the Firebase Database. They're often the wrong data structure and when used are regularly the main cause of scalability problems.
In this case: you're not really looking to store a sequence of UIDs. In fact: the order of the UIDs doesn't matter, and each UID can be meaningfully present in the collection at most once. So instead of an array, you're looking to store set of uids.
To implement a set in Firebase, you use this structure:
"admins": {
"mMK7eTrRL4UgVDh284HntNRETmx1": true,
"mx1TERNmMK7eTrRL4UgVDh284Hnt": true
}
The value doesn't matter much. But since you must have a value to store a key, it is idiomatic to use true.
Now you can test whether a key with the relevant UID exists under admins (instead of checking whether it contains a value):
"root.child('Respondents').child($organisation).child('admins').child(auth.uid).exists()",

Related

How do you get the top value of newData in Firebase Realtime Database rules?

New to firebase rules. I may be thinking about this all wrong, but anyway, I am trying to create a rule that makes sure that the newData being written to the database does not exist under the node it is being written. To write to the database I am using this code:
self.reference?.child("users").child(userid).setValue(data) {
(error, result) in
if (error != nil) {
completion(true, error!.localizedDescription)
} else {
completion(false, "")
}
}
Here is where I'd like to create the rule:
{
"rules": {
".read": false,
"users": {
".write": //allow write if the newData's keyBasedOnAuthID is not under students already
"$student_id": {
//more rules...
}
},
My incoming newData looks something like this:
{ keyBasedOnAuthID {
uid: auth.uid
name: xxxx
other:{
key: xxx
key: xxx
}
} }
I am writing this data to a node named 'users,' so the final path would be root.child('users').child(newData). The rule I would like to make would prevent users from writing duplicate data to the 'users' node. So I want to get the keyBasedOnAuthID value from the newData and check that it does not already exist.
Is this possible? I've tried many different ways to retrieve the keyBasedOnAuthID from the newData snapshot, but none have worked. Maybe I need a different approach? Any help would be appreciated!
Unfortunately, I'm not sure which language your using.
However, setValue doesn't create duplicates: either creates or updates data for the given path.
So, if you don't want to update existing data, you need to check for existence before calling your setValue method in your application code.

Firestore Match Rules for looking up data in a document that is in another collection

I am having an issue with Firestore rules when the permission is stored in another document in another collection. I haven't been able to find any examples of this, but I have read that it can be done.
I want to do it this way to avoid having to do a lot of writes when a student shares his homework list with many other students. Yes, I know this counts as another read.
I have 3 collections, users, permissions, and homework along with some sample data.
users
{
id: fK3ddutEpD2qQqRMXNW,
name: "Steven Smith"
},
{
id: YI2Fx656kkkk25,
name: "Becky Kinsley"
},
{
id: CAFDSDFR244bb,
name: "Tonya Benz"
}
permissions
{
id: fK3ddutEpD2qQqRMXNW,
followers: [YI2Fx656kkkk25,CAFDSDFR244bb]
}
homework
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false
}
The start of my firestore rules:
service cloud.firestore {
//lock down the entire firestore then open rules up.
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /homework/{ } {
allow get: if isSignedIn()
}
// helper functions
function isSignedIn() {
return request.auth != null;
}
function isUser(userId) {
return request.auth.uid == userId;
}
function isOwner(userId) {
return request.auth.uid == resource.data.uid;
}
}
}
Use case:
Steven Smith Shared his homework list with Tonya Benz.
Tonya Benz is logged into the app to view her friend Steven's homework. The app runs this query to get the homework list of Steven Smith.
var homeworkRef = db.collection("homework");
var query = homeworkRef.where("owner", "==", "fK3ddutEpD2qQqRMXNW");
Question:
What is the proper Firestore match rule that takes the "owner" field from the homework collection to look up it up as the id in the permissions collection when the user Tonya Benz is signed in so this query can run.
With your current query and database structure, you won't be able to achieve your goal using security rules.
Firstly, it sounds like you're expecting to be able to filter the results of the query based on the contents of another document. Security rules can't act as query filters. All the documents matched by the query must be granted read access by security rules, or the entire query is denied. You will need to come up with a query that is specific about which documents should be allowed access. Unfortunately, there is no single query that can do this with your current structure, because that would require a sort of "join" between permissions and homework. But Firestore (like all NoSQL databases), do not support joins.
You will need to model your data in such a way that is compatible with rules. You have one option that I can think of.
You could store the list users who should have read have access to a particular document in homework, within that same document, represented as a list field. The query could specify a filter based on the user's uid presence in that list field. And the rule could specify that read access only be granted to users whose IDs are present in that list.
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false,
readers: [ 'list', 'of', 'userids' ] // filter against this list field
}
The bottom line here is that you'll need to satisfy these two requirements:
Your query needs to be specific about exactly which documents that it expects to be readable. You can't use a rule to filter the results.
Your rule needs a way to determine, using nothing more complicated than the fields of the document itself, or a get() on other known documents, what the access should be for the current uid.

Firebase Rules: Read restriction for dynamic child nodes

I'm trying to implement a Firebase rules read restriction in a data model that has a few nested dynamic child nodes.
I have the following data model:
/groupMessages/<groupId>/<messageId>/
{
"senderId": "<senderId>",
"recipientId": "<recipientId>",
"body": "..."
}
groupId, messageId, senderId and recipientId are dynamic ids. I would like to attach a listener to the /groudId node to listen to new messages. At the same time I only want users to read the message where the senderId or recipientId matches a corresponding auth.token value.
Due to Firebase cascading rules, if I allow the read at the groupId level without restrictions, I can't deny them on the message level.
{
"rules": {
"groupMessages"
"$groupId": {
".read": "auth != null"
}
}
}
}
I also haven't found a way to restrict the read rule on the groupId level to check for sender/recipientId of a message.
Any suggestions greatly appreciated.
As you've found, security rules cannot be used to filter data. But they can be used to restrict what queries can be performed on the data.
For example, you can query for all messages where the current user is the sender with:
var query = ref.child("groupMessages").child(groupId).orderByChild("senderId").equalTo(uid);
And you can secure access to the group's messages to only allow this query with:
{
"rules": {
"groupMessages": {
"$groupId": {
".read": "auth.uid != null &&
query.orderByChild == 'senderId' &&
query.equalTo == auth.uid"
}
}
}
}
The query and rules now exactly match, so the security rules will allow the query, while they'd reject a broader read operation. For more on this, see query based rules in the Firebase documentation
You'll note that this only works for a single field. Firebase Database queries can only filter on a single field. While there are workarounds by combining multiple values into a single property, I don't think those apply to your scenario, since they only work for AND queries, where you seem to want an OR.
You also seem to want to query on /groupMessages instead of on messages for a specific group. That also isn't possible: Firebase Database orders/filters on a property that is at a fixed path under each child of the node where you run the query. You cannot query across two dynamic levels, as you seem to be trying. For more on this see: Firebase Query Double Nested and Firebase query if child of child contains a value.
The common solution for your problem is to create a list of IDs for each user, which contains just the IDs of all messages (and/or the groups) they have access to.
userGroups: {
uid1: {
groupId1: true,
groupId2: true
},
uid2: {
groupId2: true,
groupId3: true
}
}
With this additional data structure (which you can much more easily secure), each user can simply read the groups they have access to, and your code then reads/queries the messages in each group. If necessary you can add a similar structure for the messages themselves too.
Finally: this type of recursive loading is not nearly as inefficient as many developers initially think, since Firebase pipelines the requests over an existing connection.

Firebase .indexOn with complex DB structure

The current query you see below is not efficient because I have not setup the proper indexing. I get the suggestion Consider adding ".indexOn": "users/kxSWLGDxpYgNQNFd3Q5WdoC9XFk2" at /conversations in the console in Xcode. I have tried it an it works.
However, I need the user id after users/ to be dynamic. I've added a link to another post below that has tried a similar thing, but I just can't seem to get it. All help would be much appreciated!
Note: The console output user id above does not match the screenshot below, but does not matter to solve the problem I believe. Correct me if I'm wrong. Thanks!
Here is the structure of my DB in Firebase:
{
"conversationsMessagesID" : "-KS3Y9dMLXfs3FE4nlm7",
"date" : "2016-10-19 15:45:32 PDT",
"dateAsDouble" : 4.6601793282986E8,
"displayNames" : [ “Tester 1”, “Tester 2” ],
"hideForUsers" : [ "SjZLsTGckoc7ZsyGV3mmwc022J93" ],
"readByUsers" : [ "mcOK5wVZoZYlFZZICXWYr3H81az2", "SjZLsTGckoc7ZsyGV3mmwc022J93" ],
"users" : {
"SjZLsTGckoc7ZsyGV3mmwc022J93" : true,
"mcOK5wVZoZYlFZZICXWYr3H81az2" : true
}
}
and the Swift query:
FIRDatabase.database().reference().child("conversations")
.queryOrderedByChild("users/\(AppState.sharedInstance.uid!)").queryEqualToValue(true)
Links to other post:
How to write .indexOn for dynamic keys in firebase?
It seems fairly simple to add the requested index:
{
"rules": {
"users": {
".indexOn": ["kxSWLGDxpYgNQNFd3Q5WdoC9XFk2", "SjZLsTGckoc7ZsyGV3mmwc022J93", "mcOK5wVZoZYlFZZICXWYr3H81az2"]
}
}
}
More likely your concern is that it's not feasible to add these indexes manually, since you're generating the user IDs in your code.
Unfortunately there is no API to generate indexes.
Instead you'll need to model your data differently to allow the query that you want to do. In this case, you want to retrieve the conversations for a specific user. So you'll need to store the conversations for each specific user:
conversationsByUser {
"SjZLsTGckoc7ZsyGV3mmwc022J93": {
"-KS3Y9dMLXfs3FE4nlm7": true
},
"mcOK5wVZoZYlFZZICXWYr3H81az2": {
"-KS3Y9dMLXfs3FE4nlm7": true
}
}
It may at first seem inefficient to store this data multiple times, but it is very common when using NoSQL databases. And is really no different than if the database would auto-generate the indexes for you, except that you have to write the code to update the indexes yourself.

firebase security api read permission denied

I found a neat little example for permission based chat rooms using firebase security api here
Notice the "chat": {
// the list of chats may not be listed (no .read permissions here)
I actually need to list the chats a user belongs to when I load their inbox, however I can't seem to get the .read rule correctly.
Ive tried using the following rule which makes total sense but doesn't work:
"convos": {
".read" : "auth != null && data.child('users').hasChild(auth.id)",
I suspect the problem is that there is still a level between convo and users.. aka would make more sense to do:
"convos": {
".read" : "auth != null && data.child($key + '/users').hasChild(auth.id)",
$key : { ... }
But that's not allowed is complains about $key not existing yet.
How can I allow a user to pull all the convos they belongs to using this setup?
You can't use security rules to filter data. Generally, your data structure will be fairly dependent on your specific use case--most directly on how the data will be read back.
A general solution is to list the chats your user belongs to separate from the bulk chat data, i.e. to heavily denormalize, and access the chats individually.
/messages/$chat_id/... (messages chronologically ordered using push() ids)
/chats/$chat_id/... (meta data)
/my_chats/$user_id/$chat_id/true (the value here is probably not important)
Now to access all of my chats, I could do something like the following:
var fb = new Firebase(URL);
fb.child('my_chats/'+myUserId).on('child_added', function(snap) {
var chatID = snap.name());
loadChat(chatID);
});
function loadChat(chatID) {
fb.child('messages/'+chatID).on('child_added', function(snap) {
console.log('new message', chatID, snap.val());
});
}
You would still want security rules to validate the structure of chat messages, and access to a users' chat list, et al. But the functionality of filtering would be done with an index like this, or by another creative data structure.
I'm not completely sure how you're structuring your Firebase, but this might work:
"convos": {
$key : {
".read" : "auth != null && data.child('users').hasChild(auth.id)",
...
}

Resources