(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 });
Related
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.
I have a data structure as following:
{
"users" : {
"31J59dq1clZ3inHMzoopiigsEg63" : [ {
"name" : "Lanchana",
"timestamp" : 1516916245423,
"uid" : "31J59dq1clZ3inHMzoopiigsEg63",
"userEmail" : "*****#gmail.com",
"total-stars" : 123
} ]
}
}
and fire rules as following:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid && auth != null",
".write": "!data.exists() || data.child('uid').val() ==
auth.uid && auth != null",
}
}
}
}
I can add new data successfully but I cannot make any updates to the existing one. for ex: If I want to update only total-stars of a particular user based on the uid, how can I specify write and validation rules?
Your data structure seems to have a layer 0 that is missing from the rules. If that layer indeed exists in your actual JSON, you'll want to have it in your rules too:
{
"rules": {
"users": {
"$uid": {
"$postid": {
".read": "$uid === auth.uid && auth != null",
".write": "!data.exists() || data.child('uid').val() ==
auth.uid && auth != null",
}
}
}
}
I called the layer $postid here. You may want to use a name that better reflects what the layer represents.
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.
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))"
}
}
}
Im having trouble with the security rules for firebase and Im not 100% where I am going wrong. I am thinking that maybe I have my data structure wrong:
{
"users": {
"uid": {
"displayName": "Name";
}
},
"modules": {
"id": {
"title": "buttons",
"uid": "(user id string)"
},
"id": {
"title": "navbars",
"uid": "(user id string)"
}
},
"snippets": {
"id = moduleID": {
"id (of snippet)": "(id string)" {
"uid (user ID)": "(string)",
"body": {
"css": "(some code)",
"html": "(Some code)",
"name": "(string)",
"description": "(string)"
}
}
}
}
Everything in the app works fine, but when I started to add security rules I got access denied errors. Im just wondering if I have the data structure correct in the first place or is the security rules completely wrong?
Security rules:
{
"rules": {
"users": {
"$uid": {
// grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth != null && auth.uid == $uid",
".read": "auth != null && auth.uid == $uid"
}
},
"snippets": {
"$uid": {
// grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth != null && auth.uid == $uid",
".read": "auth != null && auth.uid == $uid"
}
},
"modules": {
"$uid": {
// grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth != null && auth.uid == $uid",
".read": "auth != null && auth.uid == $uid"
}
}
}
Any advice would be greatly appreciated.
It seems the rules are malformed based on the data structure.
The rules have $uid's in each node but your data doesn't match that. Users has uid but modules has id and snippets has id = moduleID.
$uid is a variable that holds the node name so it can be referenced inside { } so you should (for readability) rename that variable in the other two nodes to something that makes more sense inside each {}. Like in modules, have it $module_id.
However. I think the jest of this is you want to limit reading snippets and modules to authenticated users. To do that, you can reference the users node.
a .read rule would be something like this
"modules": {
"$module_id": {
".read": "auth != null && root.child('users/' + auth.id).exists()
}
So your modules node can be read by a user that is auth'd and their uid also appears in the users/ node
Are you using the Firebase Bolt compiler for rules? I had to write some complex rules and doing it by hand gets confusing very quickly.
Below is what it would looks like. Very easy to make changes, compile and try them out.
//current logged in user
isUser(uid) = auth != null && auth.uid == uid;
//does this module id exist
hasValidModule(module_id) = root['modules'][module_id] != null;
//dont let anyone read or write to top node
path / {
read() = false;
write() = false;
}
path /users/$user_id
{
write() = isUser($user_id);
read() = isUser($user_id);
}
path /snippets/$module_id/$snipit_id/$user_id
{
write() = isUser($user_id) && hasValidModule($module_id);
read() = isUser($user_id);
}
path /modules/$user_id
{
write() = isUser($user_id);
read() = isUser($user_id);
}
Here's the json it spits out:
{
"rules": {
"users": {
"$user_id": {
".read": "auth != null && auth.uid == $user_id",
".write": "auth != null && auth.uid == $user_id"
}
},
"snippets": {
"$module_id": {
"$snipit_id": {
"$user_id": {
".read": "auth != null && auth.uid == $user_id",
".write": "auth != null && auth.uid == $user_id && newData.parent().parent().parent().parent().child('modules').child($module_id).val() != null"
}
}
}
},
"modules": {
"$user_id": {
".read": "auth != null && auth.uid == $user_id",
".write": "auth != null && auth.uid == $user_id"
}
}
}
}
There's some info on the Firebase blog but the doc that really helped me is this
https://github.com/firebase/bolt/blob/master/docs/language.md