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.
Related
I have the following Firebase rule:
match /PendingInvites/{inviteID} {
allow read: if request.auth != null &&
isInviteForUser(database, inviteID);
}
and the following functions:
function isInviteForUser(database, inviteID) {
let dataItem = get(/databases/$(database)/documents/PendingInvites/$(inviteID)).data;
return (dataItem.userPhone == request.auth.token.phone_number) ||
(dataItem.userEmail == request.auth.token.email);
}
The testing against the collection using the online role test works, I've verifies using real documents and with both userPhone and userEmail values (both matching and non-matching). All work as expected, denying mismatch values and allowing matched values.
This is where is gets strange. When I run this (Android) query:
val companyMemberDocuments = Firebase.firestore.collection("PendingInvites").whereEqualTo("userPhone", firebaseAuth.currentUser!!.phoneNumber).whereEqualTo("userEmail", firebaseAuth.currentUser!!.email).get().await()
I get "PERMISSION_DENIED: Missing or insufficient permissions". As near as I understand rules, I think it should work. It works in the online console, I'm specifically querying for userPhone (or userEmail) but it doesn't work.
I've tried removing the userEmail and only testing for userPhone, but that also appears to not work.
Any ideas how I can correct the rules (or query)?
Thanks
Document:
Auth token used in testing:
{
"uid": "",
"token": {
"sub": "",
"aud": "certifly-global",
"phone_number": "+16505551234",
"firebase": {
"sign_in_provider": "phone"
}
}
}
Note how the document includes BOTH, userPhone and userEmail
Your rules are rejecting the query every time because Firestore security rules are not filters. Be sure to read that documentation and this post.
Your query is asking for all documents in PendingInvites where userPhone == +16505551234. However, your rules do not allow this for two reasons:
Your query doesn't match the constraints of rule (that both userPhone and userEmail are set to specific values). The query is just specifying userPhone.
The system will not perform a get() for every single document that could match. While this will work for individual document gets from the client, it will not work for queries that could return any number of documents.
So you will have to resolve both problems.
Your query will need to filter using both userPhone and userEmail, as required by your rules. This means you will have to add another whereEqualTo on userEmail that matches the requirement of the rules. In other words, the client app needs to pass the user's email in that filter.
You don't need to use a get() at all in the rules. You can refer to fields in documents in the current collection via resource.data.
The rules will need too something more like this:
match /PendingInvites/{inviteID} {
allow read: if
request.auth != null &&
resource.data.userPhone == request.auth.token.phone_number &&
resource.data.userEmail == request.auth.token.email;
}
We have a firebase structure as an attached image.
My current rules are
"Games":{
".read":"query.orderByChild == 'email' ",
"$gid":{
".read":true,
".write":true
},
".write":false,
}
This helps to disable access to parent but when there is blank email id is passed it was fetching the whole database so is it possible to check the length of email using rules?
It is hard for me to validate this answer blindly (so take it with a grain of salt), but I hope this will give you a directional help.
So the "download" in your case means the ability to read db.ref('/Games') should not be available, but a user (or certain users group?) should be able to read db.ref('Games/$gameId').
According to this article: https://firebase.google.com/docs/database/security/securing-data#rules_are_not_filters
you can not achieve this with security rules restricting ".read": false at "/Games" level, cause this will prevent reads for any of the gameIds, since the security rule will cascade.
what you can do is to try using queries as way to "filter" as described in the article:
"Games":{
".read":"query.orderByChild == 'email' ",
"$gid":{
".read": "query.orderBy("email") && query.equalTo === $gid",
".write":true
},
".write":false,
}
So your DB query should be something like:
gamesRef = database.ref('Games').orderByChild('gameId').equalTo(20204315123456)
The above assumes that the gameId is also a property of each $gameId item inside Games list. The query above will check Games list for children whose property is "gameId" and that are equal to the value provided by the client (same as the key).
Hope that will help you.
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()",
In my Firebase database I have a node eventdata contains a list of data elements related to a certain event.
Each event item has an event_log node where all the logging of that certain event is done.
The goal is to allow users only access to part of the event_log items. (e.g. the items they created themselves)
When the respective part in the Security Rules looks like this, everything works fine, but no access control on individual items is possible.
"eventdata":{
"$event":{
"event_log":{
".read": " auth != null",
".write":" auth != null"
}
}
}
Changing the code such that it uses another $ variable (but still should allow access from all authenticated users) results in not being able to access the event_log.
"eventdata":{
"$event":{
"event_log":{
"$logitem":{
".read": " auth != null",
".write":" auth != null"
}
}
}
}
Isn't it possible to 'nest' $ variables in the Security API? I couldn't find an answer to this in the Firebase Docs. Is there any other solution other than having to restructure my code?
Thanks in advance
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)",
...
}