I have a firebase database where users can submit posts. If the post is submitted privately, other users can not read it.
See the database and the rules below:
// FIREBASE DATABASE:
{
"posts" : {
"-PaNtmNIFp9sTT549-Kn" : {
"author" : "XXXYYY",
"content" : "New post",
"public" : true
},
"-PaNw0ak27MfcU5Vff1t" : {
"author" : "XXXYYY",
"content" : "Secret post",
"public" : false
},
"-PaOPhX3SwRe2ThEFWJo" : {
"author" : "XXXYYY",
"content" : "another post",
"public" : true
}
},
"user-posts" : {
"XXXYYY" : {
"-PaNtmNIFp9sTT549-Kn" : true,
"-PaNw0ak27MfcU5Vff1t" : true,
"-PaOPhX3SwRe2ThEFWJo" : true
}
}
}
// RULES:
{
"rules": {
"posts": {
"$post": {
".read": "auth !== null && data.child('public').val() === true || data.child('author').val() === auth.uid"
}
}
}
}
This works fine, but only when I retrieve the items post by post:
// DOES NOT WORK
firebase.database().ref().child('posts').on('value', snap => {
console.log(snap.val())
});
// DOES WORK
var children = ["-PaNtmNIFp9sTT549-Kn", "-PaNw0ak27MfcU5Vff1t", "-PaOPhX3SwRe2ThEFWJo"];
children.forEach(function(child) {
rootRef.child(child).on('value', snap => {
console.log(snap.val())
})
})
I guess that Rules are not Filters, right? Then my question becomes:
How do I hint the user with the right entities to look for? Should I make a separate node with all public posts keys, ie called publicPosts?
If I still want to be able to sort queries on public posts by, say, their content, how do I go about doing this? Simply by duplicating all data that could be relevant for a query in the new node publicPosts?
Thanks!
A list with the keys of public posts is indeed one option. The other options is to create a separate top-level node with the complete public posts.
And indeed: to be able to sort on data, that data must be readable to the user. If you just want to order them by a timestamp, you could put that timestamp as the value (instead of the true you have now). But for more elaborate queries you'll need to duplicate more data. At some point you'll like be better off duplicating the entire post.
Related
I Have a Google Firestore realtime database, which contains a flag isPublic. If set to true, the child node details can be read by unauthenticated users. If set to false, the record can only be accessed by one (for now) authenticated & authorized user.
The authorized user is the only one with write permission.
Further, the child node private always only grants access to the one authorized user.
The solution I have looked into is based on query based rules https://firebase.google.com/docs/database/security/securing-data#query-based_rules
Unfortunately, I am getting a permission_denied response, even as the authorized user.
Info aside: this is in a Vue JS project using the Firebase SDK.
Here is my database sample.
The first product should have read access to details for unauthenticated users, but not to private.
The second one should not be shown to unauthenticated users at all, by forcing them to run the query dbRef.orderByChild('isPublic').equalTo(true)
"products": {
"-pushIdxyz1" : {
"isPublic" : true,
"private" : {
"notes" : "lorem ipsum always private",
"soldDate" : ""
},
"details" : {
"imageURL" : "https://firebasestorage.imagexyz",
"keywords" : "this, that",
"title" : "Public product title",
"workCategory" : "hardware",
},
},
"-pushIdxyz2" : {
"isPublic" : false,
"private" : {
"notes" : "lorem ipsum always private",
"soldDate" : ""
},
"details" : {
"imageURL" : "https://firebasestorage.imagexyz",
"keywords" : "this, that",
"title" : "Secret product title",
"workCategory" : "hardware",
},
}
}
And my not-working rules
"rules": {
"products": {
"$product_id": {
".indexOn": "isPublic",
".read": "query.equalTo == true || auth.uid == '[admin user id]'"
// alternative attempt
// ".read": "(query.orderByChild == 'isPublic' && query.equalTo == true) || auth.uid == '[admin user id]'",
,
".write": "auth.uid == '[admin user id]'",
"details": {
".read": true,
},
"private": {
".read": "auth.uid == '[admin user id]'",
}
}
}
}
This query, for unauthorized users, should give read access to the details child node:
firebase.database().ref('products').orderByChild('isPublic').equalTo(true).once('value')
.then(...)
This query, for the authorized user only, should give read access to all data
firebase.database().ref('products').once('value')
.then(...)
With the current setting I am getting "permission denied" for either query, logged in as the authorized user or not.
If you want a user to be able to read from the products node, you will need to have a .read rule on that node. And since you only want to allow the read if it's a query for public products, you should validate that specific query.
Something like this:
"rules": {
"products": {
".indexOn": "isPublic",
".read": "(query.orderBy == 'isPublic' && query.equalTo == true)
|| auth.uid == '[admin user id]'"
}
}
So:
You defined the rule one level too low, which means all reads on /products were rejected.
Your rule wasn't checking the orderBy field.
Note that the additional .read rule you've declared on private is meaningless here, since the same user already got read permission on the entire `products node anyway.
Franks answer pointed me into the right direction (thank you!), but I decided to post a more complete answer myself.
Here's the data structure. Note that I changed 'isPublic' to 'isPublished'.
'products' now contains two child nodes: 'private' and 'public'. Both use the same key.
// Firebase Data
"products": {
"private" : {
"-pushId_1" : {
"notes" : "Private notes product 1",
"soldDate" : ""
},
"-pushId_2" : {
"notes" : "Private notes product 2",
"soldDate" : ""
}
},
"public" : {
"-pushId_1" : {
"imageURL" : "https://firebasestorage.imagexyz",
"isPublished" : true,
"title" : "Published product title",
},
"-pushId_2" : {
"imageURL" : "https://firebasestorage.imagexyz",
"isPublished" : false,
"title" : "Unpublished product title, only accessible if authorized",
}
}
}
The rules allow admin everything and allow unauthenticated access to the public child node if the query looks for published products
// Firebase Rules
{
"rules": {
"products" : {
".read": "auth.uid == '[admin user id]'",
".write": "auth.uid == '[admin user id]'",
"public" : {
".indexOn": "isPublished",
".read": "(query.orderByChild == 'isPublished' && query.equalTo == true)"
}
}
}
}
To create new products, I am first pushing to 'public', then use the returned key to create the private record
// Add new product
db.ref('products/public').push(publicData)
.then((data) => {
key = data.key
return key
})
.then((key) => {
db.ref('products/private').child(key).update(privateData)
return key
})
.then( ... )
To get the products, public/unauthenticated users have to look for isPublished in public.
The admin can loop through public and private child nodes
// Retrieve products - Public
db.ref('products/public').orderByChild('isPublished').equalTo(true).once('value')
.then((data) => {
let publicData = data.val()
// Do something with the data
})
// Retrieve products - Admin
db.ref('products').once('value')
.then((data) => {
let publicData = data.child('public').val()
let privateData = data.child('private').val()
// Do something with the data
})
i have following security rules:
{
"rules": {
"users": {
".read" : true,
"$user_id": {
".read": true,
".write": "auth != null && $user_id === auth.uid"
}
}
}
}
My data looks like:
{
"users" : {
"JIZW3KswDDMggo9U1WwCoIamHaU2" : {
"-L8s34CZsCgodNxgF09G" : {
"email" : "lmn#yahoo.co.in",
"isAdmin" : true,
"name" : "Mannu Sharma",
},
"UKtLdQzPdWa2KJ10iXrjuV80JSd2" : {
"-L8LTf95dxqQdjYHhFDB" : {
"email" : "pqr#gmail.com",
"name" : "Neeti Singhal"
}
},
"YQCXFjnU8jaXR9xUXIgknp18Z3A3" : {
"-L8TQTFiGLEuxCTbbrQ7" : {
"email" : "abcd#gmail.com",
"name" : "John Teslie",
}
}
}
My angularfire2 code to query data is:
getUserByEmail(email:string){
console.log("start of getUserByEmail with email:" + email)
return new Promise((resolve, reject) =>
{
this.db.list("/users",
ref => ref.orderByChild('email').equalTo(email)
).valueChanges().subscribe(
res => {
console.log('response:' + JSON.stringify(res))
resolve(res)
},
err => {
console.log(err)
reject(err)
}
)
})
}
I have login with facebook implemented. So when i login with my email abcd#gmail.com and do the search it returns me my record. But any other search does not work.
To my understanding my security rule let anyone query the users data. So what I am missing?
Your data is double-nested: you have /users/$uid/$pushId. That last level is not needed, and means that you can't query for a user by their email. For more on this general case see Firebase Query Double Nested.
But in this case the fix is quite simple: I don't think you need the level with the push IDs (-L....) in your JSON. You're likely adding the user data by calling push(), which isn't needed. Remove the call to push(), probably use set() it its stead, and you should lose that extra level in the JSON tree. With that level gone, you can query /users by the email property of each user.
This code uses a loop to $unset the "checked" property of all embedded documents in the "value" array, then $set the one when a condition evaluates to true.
But when the condition is true, the update block failed to update the embedded document by setting a checked: "checked", I know that because meteor:PRIMARY> db.radioOptions.find({}).pretty(); gives the same results before and after.
What am I doing wrong? and how to fix it? Thanks
meteor:PRIMARY> db.radioOptions.find({}).pretty();
{
"_id" : "jXQcsXtedQYotKQXG",
"name" : "optionType",
"value" : [
{
"name" : "1stOption",
"caption" : "1st Option"
},
{
"name" : "2ndOption",
"caption" : "2nd Option"
}
]
}
var doc = RadioOptions.findOne({name: obj.name});
if (typeof doc != 'undefined') {
doc.value.forEach(function (embdoc) {
console.log(embdoc);
RadioOptions.update(
{name: obj.name, 'value.name': obj.value},
{$unset: {'value.$.checked': ""}}
);
if (embdoc.name == obj.value) {
console.log(obj.name + " " + obj.value); //obj.value = 1stOption for example
RadioOptions.update(
{name: obj.name, 'value.name': obj.value}, //obj.name = "optionType"
{$set: {'value.$.checked': "checked"}}
);
}
})
}
Let's say that is was your objective to set the array element with the name "2ndOption" to "checked" and $unset all other array elements. You would then instead do:
var doc = RadioOptions.findOne({name: obj.name});
if (typeof doc != 'undefined') {
// You have to update every element
doc.value.forEach(function (embdoc) {
RadioOptions.update(
{ "_id": doc._id, "value.name": embdoc.name },
{ "$unset": { "value.$.checked": "" } }
)
});
// Then just set the one you want
RadioOptions.update(
{ "_id": doc._id, "value.name": "2ndOption" }, // <-- actually using a variable of course
{ "$set": { "value.$.checked": "checked" } }
)
}
Now if you had actually read all the responses on the duplicate question you were given for your original question:
How to Update Multiple Array Elements in mongodb
Then you would have seen the response there that mentioned the best way to handle all these mutliple updates was using the "Bulk" API methods in the underlying driver. And also has some useful tips on the general process of updating multiple elements.
On the "server" (not in minimongo) is the best place to to this, and all meteor collections have a .rawCollection() method which returns the collection object from the underlying node native driver.
So the general logic is:
Loop all array elements and update to "turn off"
Match the element you want to "turn on"
And best done in Bulk rather than responding back and forth for each update.
I am trying to mock out a user for testing out my application, and I have gotten to the point where I can create a test user and log them into the mirror instance of my app.
I need to compare the gmail addresses for peoples accounts, and to test this functionality, I want to add a test email address under user.services.google.email within the Meteor users account database (which is where the accounts-google package stores it, I don't need to mock out an entire user account yet).
What I can't figure out is how to append this information, instead of just overwriting what is already there, this is what my code looks like:
if (Meteor.users.find().count() === 0) {
var testUserDetails = {
email: 'testEmail#gmail.com',
password: 'testPassword'
};
console.log("Creating the Test User");
var newUserId = Accounts.createUser(testUserDetails);
Meteor.users.update({
_id: newUserId
}, {
$set: {
services: {
google: {
email: "testEmail#gmail.com"
}
}
}
});
} else {
console.log("There are already users in the Test database");
}
console.log('***** Finished loading default fixtures *****');
},
And this is what a user looks like:
{
"_id" : "Dw2xQPDwKp58RozC4",
"createdAt" : ISODate("2015-07-30T04:02:03.261Z"),
"services" : {
"password" : {
"bcrypt" : "asdfasdfasdfdsafsadfasdsdsawf"
},
"resume" : {
"loginTokens" : [ ]
}
},
"emails" : [
{
"address" : "testEmail#gmail.com",
"verified" : false
}
]
}
Now $set just rewrites everything within services, and there is no $push operation for mongo or for js, so how should I go about doing this? Should I consume the object and parse it manually?
*Note I have also tried using Meteor's Accounts.onCreateUser(function(options, user) but facing the same issue.
[...] there is no $push operation for mongo [...]
Sure, there is a $push operator, which appends a specified value to an array.
However, I think what you are trying to do is to update a document and keep all values which are already set.
Here is how you can do that:
Query the document first to get the object you want to set.
Update the respective object.
Run the MongoDB update operation to set the new object.
For instance:
var user = Meteor.users.findOne({
_id: newUserId
});
var servicesUserData = user.services;
servicesUserData.google.email = "your_new_email#gmail.com";
Meteor.users.update({
_id: newUserId
}, {
$set: {
"services": {
servicesUserData
}
}
});
Here I am fetching the data using angularFire:
angular.module('FireApp', ['firebase'])
.controller('Document', function($scope, $routeParams, angularFire){
var url = "https://my-account.firebaseio.com/test" + "/" + $routeParams.data;
angularFire(url, $scope, data);
});
The problem is that this will load the 'https://my-account.firebaseio.com/test/data' and all of its children and grandchildren etc..
The '/data' is an array of objects, which can (but doesn't have to - it could very well be an array of Strings) look similar to the parent of "/data":
data: ["0" : {
"data" : [...],
"meta" : {
"active" : false
},
"sign" : [...]
},
"1" : {
"data" : [...],
"meta" : {
"active" : true
},
"sign" : {...}
},
"2" : {
"data" : [...],
"meta" : {
"active" : false
},
"sign" : {...}
}]
Basically, I want all unauthenticated angularFire clients to receive ONLY those elements of the data array, where the boolean flag meta.active = true, i.e. the active flag need to decide whereas its grandparent will be sent to the client upon the angularFire request. Also unauthenticated clients can't write/edit any of the data. The rule I want to set should say: "whenever the 'active = false' flag is set do not send the flag's grandparent to the angularFire client, unless the client is authenticated.". So yes the rule need to be relative rather than absolute.
Only authenticated clients should be able to receive/edit all the data.
Can I make such rules with Firebase Simple Login and Security Rules?
If this can't be done for a grandparent, the 'active' flag can be moved one level up to affect its parent instead.
Thank you for your time,
Jared
You'll need to restructure your data in order to achieve this with Firebase. See this blog post for some background on this: https://www.firebase.com/blog/2013-04-12-denormalizing-is-normal.html
One way to structure the data would be to create two separate lists, one for active items and another for inactive items. When an item becomes active or inactive, be sure to remove it from one list and put it in another.
{
"active": {
0: {
"data": {...},
"sign": {...}
}
},
"inactive": {
0: {
"data": {...},
"sign": {...}
}
}
}
In your security rules, you can simply prevent access to inactive items to unauthenticated users:
{
"rules": {
"active": {
".read": true
},
"inactive": {
".read": "auth != null"
}
}
}
Hope this helps!