Firebase Security Rules - Checking if data.hasChild and keeps failing - firebase

I have a security rule that looks like this:
"user_feeds": {
"$userId": {
".indexOn": ["timestamp"],
".read": "auth != null",
"$postId" : {
".write": "auth != null && newData.parent().hasChild(newData.child('band_id').val())"
}
}
}
in my $postId sub-tree, I'm trying to check to see if the parent has a node that corresponds to the band_id value of the data that is being inserted.
The data looks like:
{
post_id: 1,
band_id: '-L-tyesfsdf13434',
timestamp: 34234234234
}
And an actual tree that these rules would correspond to look like:
{
"user_feeds": {
"userId1": {
"-L-tyesfsdf13434": true
}
}
}
I am using Ionic/Angular and here is my function that's actually trying to do the write operation:
addPostToFollowers(bandId, postId) {
// 1. Get the user's connections
this._bands.getBandFollowers(bandId).take(1).subscribe(connections => {
if(connections.$value !== null) {
delete connections['$key'];
delete connections['$exists'];
let connectionKeys = Object.keys(connections);
// 2. Go through those connections and add the post
let bandFeeds = {};
connectionKeys.forEach(connectionKey => {
bandFeeds[`/user_feeds/${connectionKey}/${postId}`] = {
post_id: postId,
band_id: bandId,
timestamp: -(+(new Date()))
}
})
// 3. Update the social feeds
this.db.object(`social`).update(bandFeeds);
}
})
}
However, when I try to run this, I get a permission denied and I have no idea why. Am I doing something wrong here?

Try using root.child('user_feeds').child($userId) instead of newData.parent():
"user_feeds": {
"$userId": {
".indexOn": ["timestamp"],
".read": "auth != null",
"$postId" : {
".write": "auth != null && root.child('user_feeds').child($userId)
.hasChild(newData.child('band_id').val())"
}
}
}
I suspect the newData snapshot is limited to the data at that location and parent() cannot be used to fetch data at a location higher in the database tree.

So here are my updated rules. I needed to reference another path correctly:
"user_feeds": {
"$userId": {
".indexOn": ["timestamp"],
"$postId" : {
".write": "auth != null && (root.child('social').child('user_band_follows').child($userId).hasChild(newData.child('band_id').val()) || root.child('social').child('user_band_follows').child($userId).hasChild(data.child('band_id').val()) || root.child('social').child('user_connections').child($userId).hasChild(auth.uid))"
}
}
}

Related

Using field in database security rule that is stored in the data

I have a firebase database with the following structure
I want to define a database rule where authenticated users can read the data only where authenticated user's id is equal to userId defined in the data.
How can i define this rule?
I have tried defining a rule as shown below but id didn't work. It doesn't allows any read operation
{
"rules": {
".read": "auth.uid == root.child('orders').child('userId').val()",
".write": "auth != null"
}
}
This is how i am making a GET request to firebase's REST api
axios.get(`/orders.json?auth=${firebaseAuthToken}`)
.then(response => {
if (response.status === 200) {
dispatch(fetchOrdersSuccess(Object.values(response.data)));
}
})
.catch(error => {
dispatch(fetchOrdersError(error.message));
});
try the following rule for reading orders
{
"rules": {
"orders": {
"$orderId": {
".read": "data.child('userId').val() == auth.uid",
".write": "auth != null"
}
}
}
}

Read only children of a list that have a field with the value of auth.uid

This is my DB structure
"tasks"
"$taskId"
...
"user": "firebase user id"
I have already written a rule ".read": data.child('user').val() === auth.uid" under $taskId. When I try to access a single task, this rule is taking effect.
Will this also guarantee that if I write a query like firebase.database().ref('/tasks').orderByChild('status').limitToFirst(1) I'll only get tasks that have user id field as auth.uid. Or should I also write a .read clause under tasks
There are several aspects to be answered in your question:
1/ At which level should you write the security rules?
If you write only at the task level like just follows, you will not be able to query the entire set of tasks.
You can test it by doing the following:
Rules:
{
"rules": {
"tasks": {
"$taskID": {
".read": "auth != null",
".write": "auth != null"
}
}
}
}
JS:
var db = firebase.database();
var ref = db.ref('tasks');
firebase.auth().signInWithEmailAndPassword("....", "....")
.then(function(userCredential) {
ref.once('value').then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
console.log(childSnapshot.val());
});
});
});
This will fail with "Error: permission_denied at /tasks: Client doesn't have permission to access the desired data."
If you change var ref = db.ref('tasks'); to var ref = db.ref('tasks/123456'); (123456 being an existing task id) you will get a result.
If you change your rules to the following, the two previous queries will work.
{
"rules": {
"tasks": {
".read": "auth != null",
".write": "auth != null"
}
}
}
2/ How should you do to only get tasks that have user id field as auth.uid?
The first point to note is that "Rules are not Filters", as detailed here: https://firebase.google.com/docs/database/security/securing-data#rules_are_not_filters
So if you implement security rules as follows:
{
"rules": {
"tasks": {
"$taskId": {
".read": "auth != null && data.child('user').val() === auth.uid",
".write": "auth != null"
}
}
}
}
You will need to write a query that includes the same restriction on the user uid, like the following:
var db = firebase.database();
firebase.auth().signInWithEmailAndPassword("....", "....")
.then(function(userCredential) {
var ref = db.ref('tasks').orderByChild('user').equalTo(userCredential.user.uid);
ref.once('value').then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
console.log(childSnapshot.val());
});
});
});
But this query will not work, again, because "Error: permission_denied at /tasks: Client doesn't have permission to access the desired data."
You cannot do the following neither, since "Shallower security rules override rules at deeper paths.":
{
"rules": {
"tasks": {
".read": "auth != null",
".write": "auth != null"
"$taskId": {
".read": "auth != null && data.child('user').val() === auth.uid",
".write": "auth != null"
}
}
}
}
One solution is to use Query-based Rules (see the doc here) and write your rules as follows:
{
"rules": {
"tasks": {
".read": "auth != null &&
query.orderByChild == 'user' &&
query.equalTo == auth.uid",
".write": "auth != null"
}
}
}
However, as you have probably noticed, this will prevent you to order your query (and filter it) by something else than the user (e.g. by status), since "You can only use one order-by method at a time."
The solution would therefore be to create a second data structure in parallel to your existing structure, where you add the user as a top node, like
"tasks"
"$taskId"
...
"user": "firebase user id"
"tasksByUser"
"$userId"
"$taskId"
...
You would use the update() method to write to the two data structures simultaneously. See the doc here.
I gave .read: true under tasks and it is considering the rules written under the individual task objects before returning the results.

Deny creation of child node, if the node key is not the same as user id. Firebase Rules

Suppose I have a database structure as follows:
/
favorites
ownerid_1
favorite1_id
title
link
favorite2_id
title
link
ownerid_2
etc
I can easily put Firebase permissions around these as follows:
{
"rules": {
"favorites": {
"$owner_id": {
".read": "root.child('favorites').child($owner_id) == auth.uid",
".write": "root.child('favorites').child($owner_id) == auth.uid",
}
}
}
}
However favorites are optional and the user can decide when to create their first favorite. The user's "$user_id" data structure does not exist until they do.
I'm struggling to work out how I can put in a validation rule at /favorites that will allow a new child node to be created by the user only when the key is the same as their user id.
i.e. ownerid_1 cannot create an ownerid_2 node.
I've tried the following but the simulator fails without specifying a specific line:
{
"rules": {
"favorites": {
".validate": "newData.hasChild(auth.uid)",
"$owner_id": {
".read": "root.child('favorites').child($owner_id) == auth.uid",
".write": "root.child('favorites').child($owner_id) == auth.uid",
}
}
}
}
Update #1
Here's the logs without specifying any line in the Rules:
Type write
Location /favorites/
Data { "JhDa8owfAkTRR5qMbuvAgEyUHYL2": { "favid1": { "title": "google", "link": "www.google.com" } } }
Auth { "provider": "google", "uid": "JhDa8owfAkTRR5qMbuvAgEyUHYL2" }
Admin false
Update #2
I have tried the following:
{
"rules": {
"favorites": {
".write": "newData.hasChild(auth.uid)",
// This too: ".write": "newData.hasChildren([auth.uid])",
"$owner_id": {
".read": "root.child('favorites').child($owner_id) == auth.uid",
".write": "root.child('favorites').child($owner_id) == auth.uid",
}
}
}
}
Which works, except it also allows the following to be successfully written:
"favorites": {
"bad_data": "oops",
"$owner_id": {
"favoriteid_1": {
"title": "google.com",
"link": "www.google.com"
}
}
}
It feels like we need an option like "onlyChildren" so that use cases of arbitrary data being written can be prevented.
Any thoughts?
The problem might be in you trying to get the child of a node that does not exist when you do:
"root.child('favorites').child($owner_id) == auth.uid"
Change your rules to:
{
"rules": {
"favorites": {
".validate": "newData.hasChild(auth.uid)",
"$owner_id": {
".read": "$owner_id === auth.uid",
".write": "$owner_id === auth.uid"
}
}
}
}
I changed my approach and it seems to have done the job. This basically says, you can write here as long as you're authenticated and you're not writing null... but if you do, then within the $owner_id tree...
The $owner_id must match your auth.uid, ensuring you only write to your location.
Both a 'title' and 'link' child is required, ensuring you do not write arbitrary data within /favorites.
The data type must be correct, ensuring good house keeping.
Here's the validation rules:
{
"rules": {
"favorites": {
".write": "auth.uid != null && newData.val() != null",
"$owner_id": {
".read": "$owner_id == auth.uid",
".validate": "$owner_id == auth.uid &&
newData.hasChildren(['title', 'link']) &&
newData.child('title').isString() &&
newData.child('link').isString()"
}
}
}
}
Update
Added check for null value.

Firebase Database Rules - How to emulate if a key within a tree exists, such as hasOwnProperty

I'm delving into Firebase Security rules for my Firebase realtime database, and I've got a tree that's designed like this:
the endpoint: /projects/${projectId}
{
"name" : "A Fun Project",
"users" : {
"zjy846p8q1TgdTLxjdFr9DVvxa53" : {
"role" : "admin"
}
}
}
Is there any way I could utilize my rules to do something like this?
{
"rules": {
"projects": {
"$projectId": {
".read": "auth !== null && data.child('users').child(auth.uid).key === auth.uid"
}
}
}
}
Thanks in advance!
I needed to use .hasChild(auth.id)
Here are my updated rules:
{
"rules": {
"projects": {
"$projectId": {
".read": "auth !== null && data.child('users').hasChild(auth.uid)"
}
}
}
}

How to access the key of the newData server variable?

(Please see revised question and comment if further clarity is necessary)
Given this data structure
{
"-KWz2G9JKtwqt5Kn-pL7":true
}
How can I access the first value ( "-KWz2G9JKtwqt5Kn-pL7" ) from within the Firebase Validation Rules when I try to validate the newData?
Below please find my original question.
Background
Samples online show that the best way to connect Firebase entities is using an index where one collects id's that relate one entity within a node of another entity.
for example;
"groups":{
...
"members":{
"userid-1":true,
"userid-2":true
}
}
I want to publish an index to a node below another entity (queues/queue/tasks) with the following data structure and to validate that data structure with the set of rules described below:
{
"-KWz2G9JKtwqt5Kn-pL7":true
}
The data structure is a reference to a task entity (/tasks/task/tid) that I want to associate with a node of a separate entity.
I'm planning on putting those values into a Dictionary and adding it via setValue. When I do this Firebase should apply a validation rule. I want to verify that the identifier is one that exists elsewhere in the database.
My Index Will Look Like This
(where the identifiers relate to a set of task id's stored in another location)
"queues":{
....
"K24395498054-p23"{
"tasks": {
"-KWz2G9JKtwqt5Kn-pL7":true,
"-KWjewrkstwqt7Ln-pL3":true,
"-KWjewgqjdsllfsn-pL5":true
}
}
}
But I'm unclear on how to unpack the first value of the newData server variable within the rules when in this case, I have no child identifier.
Instead of sending a singular value, I am sending a dictionary to Firebase.
newData in this case should equal:
{
"-KWz2G9JKtwqt5Kn-pL7":true
}
Nota Bene - An interesting side note on this; I'm trying to create an
index but reading the setValue documentation. It states that the value
would be overwritten. How then can I save the dictionary without
overwriting the existing values?
Rules:
{
"rules": {
".read": "auth != null",
"queues": {
".write":"auth.provider != 'anonymous'",
"$qid": {
"members" : {
//unsure how to access the first value of the newData object without it having a label but the following shows what I am trying to accomplish
".validate":"root.child('tasks').hasChild('-KWz2G9JKtwqt5Kn-pL7')",
}
}
}
}
}
In this case I am confirming that the newData being added is a valid unique identifier that already exists as a task. In this rule the task will be associated with the queue entity.
If it helps please see my entity map below:
Alternatively
How can I create an index similar to what I am seeing online in your samples?:
To represent a set of groups that I might associate with a user....
"user_0" : {
....
"groups" : {
"group_id0":true,
"group_id1":true,
"group_id2":true
}
}
Following that, how can I validate that a group_id# actually exists under my group node elsewhere?
If I can get that answer, I may be able to extrapolate for my needs.
Update
Based on the accepted answer, I have been able to successfully apply the following rules to achieve the desired outcomes:
{
"rules": {
".read": "false",
".write": "(auth.provider != 'anonymous') && (auth != null)",
"presence": {
".read": "(auth.provider != 'anonymous') && (auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
},
"queues": {
".read":"(auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
"$qid": {
"tasks": {
"$taskid": {
".validate": "root.child('tasks').child($taskid).exists()"
}
}
}
},
"tasks": {
".read": "(auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
"$taskid":{
"queues":{
"$qid": {
".validate":"root.child('queues').child($qid).exists()"
}
}
}
},
"users": {
".read": "(auth != null)",
".write": "(auth != null)",
"$userid":{
"groups":{
"$gid": {
".validate":"root.child('groups').child($gid).exists()"
}
},
"roles":{
"$rid": {
".validate":"root.child('roles').child($rid).exists()"
}
},
"metadata":{
".read": "(auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
}
}
},
"roles": {
".read": "(auth != null)",
".write": "(auth != null)",
},
"groups":{
".read": "(auth != null)",
".write": "(auth != null)",
},
} //eof-rules
}
You need to add another level to your rules so that you can use a $ variable for the task ID:
{
"rules": {
".read": "auth != null",
"queues": {
".write": "auth.provider != 'anonymous'",
"$qid": {
"members": {
"$taskid": {
".validate": "root.child('tasks').hasChild($taskid)"
}
}
}
}
}
}
You can then use either set or update to add the task ID to the index:
firebase
.database()
.ref("queues/K24395498054-p23/members/-KWz2G9JKtwqt5Kn-pL7")
.set(true);
firebase
.database()
.ref("queues/K24395498054-p23/members")
.update({ "-KWz2G9JKtwqt5Kn-pL7": true });

Resources