Issue with security rules getting data via auth.id - firebase

I'm having an issue setting my Security rules properly, specifically reading the post data.
The data hierarchy goes:
posts : {
0 : {
title: "Post One",
userId: 6
}
},
users : {
6 : {
name: "My Name"
}
}
And my rules are:
{
"rules": {
"posts" : {
"$post": {
".read":"data.child('userId').val() == auth.id",
".write":"newData.child('userId').val() == auth.id"
}
},
"users":{
"$user": {
".read":"auth.id == $user",
".write":"auth.id == $user"
}
}
}
}
I know that the "auth.id" is 6, because it's pulling the rules correctly for my user info. If I change the rules to pull the number statically, it works:
"$post": {
".read":"data.child('userId').val() == 6",
".write":"newData.child('userId').val() == auth.id"
}
but using auth.id does not. Is there something I'm missing?

One thing to keep in mind is that security rules are type-safe. In particular, In the rules, "6" != 6 (since one is a string and one is a number). So perhaps your auth.id is "6" (as a string), but your userId is 6 as a number?
If that's the case, one potential fix would be changing your rule expression to something like:
data.child('userId').val() + '' == auth.id
which will force userId to be a string. Alternatively, you could change your data to make sure userId is always stored as a string.

You haven't included the code you're using to look up this data--probably where the error is--or the error you are receiving; those would help quite a bit.
Your rules should work fine, assuming you are attempting to read a single post at a time, and assuming your authentication is set up correctly.
A quick guess would be that you're trying to read the entire "posts" path, and using security rules to filter your posts. But security rules are essentially atomic. If you try to read "posts", and one of the posts has a rule that prevents read, the entire operation is going to fail.
Instead, you need to segment the posts into paths where all the data can be read by the authenticated user, then you can apply security rules accordingly.
One thing that will help immensely is to test your security rules by going into your Forge and using the "simulator". You can log in as any user, then try a read/write, and see exactly which security rules is failing and why.

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.

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.

Setting a "publishing time" for Firebase database entries?

I am planning an app that would allow users to create posts which shouldn't be readable by other users until the date/time that the creator of the post has selected.
Is this possible using the Firebase Realtime Database? How would I implement something like this?
I guess that simply implementing it in client code would not be secure, since authenticated users could still GET all posts manually, even the "not yet published" ones? Can I use database rules to do it, even though each post would have their individual publish date/time?
Yes, it's possible with Firebase. All you need to do is to add a flag for each post with the default boolean value of false and a TIMESTAMP. This means that by default, the post cannot be readable by other users. Then you need to use a listener on that TIMESTAMP filed to see when the current date/time is equal with the date/time that the creator of the post has selected. If it's equal then just set the value of the flag to true. This means that the post can be readable by other users. That's it!
You can achieve this also using security rules like this:
//Ensure that data being read is less than or equal with the current date/time.
".read": "data.child('timestamp').val() <= now"
The only solution I can think of is creating an entirely new node scheduledPosts with it's own rules to only allow the creator to see/edit it before the publish date (if that's what you're aiming for).
{
"users": {
"uid_1": {
"someData": "data",
...
}
},
"scheduledPosts": {
"pid_1": {
"postData": "data",
"uid": "uid_1",
"publishDate": 1695840299, // must be in milliseconds since epoch
...
}
}
}
And your scheduledPosts's rules would look as follows:
{
"rules": {
"scheduledPosts": {
"$post_id": {
".read": "root.child('scheduledPosts').child($post_id).child("publishDate").val() < now || root.child('scheduledPosts').child($post_id).child("uid") === auth.uid",
".write": "root.child('scheduledPosts').child($post_id).child("publishDate").val() < now || root.child('scheduledPosts').child($post_id).child("uid") === auth.uid"
}
}
}
}
You can use read rule, with combination of now
The rule will look something like this:
".read": "(auth != null) && (data.child('publish_time').val() < now)"

Firebase Database Delete Security Rules

I am currently creating Firebase security rules to prevent a user from setting a node to null and deleting all the data in that node.
This is my schema
{
"folder" : {
"item1" : {
"dataset1" : {
"data1" : 123,
"data2" : 456,
"data3" : 789
}
}
}
}
These are my rules
{
"rules": {
"folder": {
// users cannot delete items in the node folder
".write": "newData.exists()",
// item is a variable
"$item": {
"dataset1": {
// dataset1 must have certain nodes and can't be deleted (set to null)
".validate": "data.hasChildren(['data1', 'data2', 'data3']) && newData.exists()",
"data1": {".validate": "newData.isNumber()"},
"data2": {".validate": "newData.isNumber()"},
"data3": {".validate": "newData.isNumber()"},
// using the variable $other means any node that isn't data1, data2, data3 is denied
"$other": {".validate": false}
}
}
}
}
}
Using the built in simulator I'm getting these results:
This works when location is set to "/folder/item1" and "/folder/item1/dataset1"
If I had deeper nodes in data1 they would all be deleted because the write was allowed.
Thanks for taking a read. Open to any answers, preferably I don't need to change the schema.
It's not really clear to me what you're asking. But there are a few problems with your rules, so I'll just point those out in hopes that they answer your question.
you grant write access on /folder, which you cannot take away at a lower level.
As long as any data is left under /folder any write is allowed. A thing to keep in mind is that newData is the data at the location as it will exist after the write operation; it is not just the new data that is written.
I have the impression that you're trying to prevent the deletion with .validate rules. Keep in mind that validation is not performed when deleting data, so you cannot use .validate rules to prevent deletion.
I suspect that #2 is causing your current problem.

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