Acessing adjacent data in firebase - firebase

Let's imagine a simple scenario: A simple mail system in firebase in a tree like this:
{
"users":{
"user-0001":{
"mailbox":{
"user-0002":{
"DSdljkdkd333klll":{
"message":"Hi ! I am user-0002 and I sent to user-0001 a message"
},
"JjkJHHH8888Gggg2":{
"message":"Hi ! It's Me, user-0002 again !"
}
}
},
"secretStuff":"Something Private - nobody can write anything here",
"myContacts":"This is my contact list. Obviously just me can access"
},
"user-0002":{
"mailbox":{
"user-0056":{
"DSdljkdkd333klll":{
"message":"Party tonight ! Don't forget !"
}
},
"user-0282":{
"3893NJJj33333eddf":{
"message":"How are you ?"
}
}
},
"secretStuff":"Something Private - nobody can write anything here",
"myContacts":"This is my contact list. Obviously just me can access"
}
}
}
Me as user-0002 , I can write on my own tree.
Ok, but I should be capable to write at user-*/mailbox/user-0002 because I can send a message to any user that I want. And of course: I can't have access to any other key.
So, how to archive this : A rule where I can write on my tree and in an adjacent tree like the example above ?

You should keep public data and private data in separate top-level lists. So:
mailboxes
user-0001
user-0002:{
"DSdljkdkd333klll":{
"message":"Hi ! I am user-0002 and I sent to user-0001 a message"
},
"JjkJHHH8888Gggg2":{
"message":"Hi ! It's Me, user-0002 again !"
}
}
user-0002
user-0056:{
"DSdljkdkd333klll":{
"message":"Party tonight ! Don't forget !"
}
},
user-0282:{
"3893NJJj33333eddf":{
"message":"How are you ?"
}
}
contacts
user-0001: "This is my contact list. Obviously just me can access"
user-0002: "This is my contact list. Obviously just me can access"
secrets
user-0001: "Something Private - nobody can write anything here"
user-0002: "Something Private - nobody can write anything here"
Now you can secure access based on the type of information, and load specific types of information.
If you need all information for a specific user, you will need to read from all three places. But given that you'll usually do this for a specific user, those reads are not a scalability concern. Even if you need all info for multiple users, it's usually quite fast since Firebase pipelines the requests over a single connection.
Also see:
Firebase: How to structure public/private user data
How to create public/private user profile with Firebase security rules?
Make a public profile and a private profile in angular 4+
Firebase: Allow Read of property names, but not content
How to store public/private data under a node, and still query the entire node

After researching more and testing I conclude that this should work...
{
"rules":{
"users":{
"$uid":{
".write":"$uid === auth.uid",
".read":"$uid === auth.uid",
"mailbox":{
"$uid":{
".write":" $uid === auth.uid"
}
}
}
}
}
}
This means:
If I'm user-0001, I can read/write in anything at /users/user-0001, however I just can write in elsewhere if the path is /users/*/mailbox/user-0001

Related

Firebase authentication - get data from other user's node but rules are set to ".read": "$uid === auth.uid"

Sorry if the title was confusing, I'll do a better job explaining here.
I was reading up on the Firebase security documentation and came across this page: https://firebase.google.com/docs/database/security/quickstart?authuser=0. Specifically I was looking at the "Sample Rules" -> "User" section which contained the following code:
// These rules grant access to a node matching the authenticated
// user's ID from the Firebase auth token
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
Assuming I understand this correctly, you will only be able to read YOUR data under the "users" node, and nobody else's.
So in my app I need to retrieve some data about a "friend" (friend count, username, posts they've made, etc) which is stored under my "friend's" user id in the "users" node. So it looks like this:
{
"users": {
"friend_uid": {
"friend_count": 10,
"username": "john",
"posts": [
"message": "post1"
],
"age": 18,
...
}
}
}
How can I do this based on the permissions above? If this is such a common task (getting some basic information about your friend), then why do a lot of tutorials show that you should set the rules to what I pasted above?
How do developers get the data they need? Do they have 2 nodes, "users" and "private_users", where in "users" they store all of the public information for retrieval from anyone, and "private_users" is where they store all the private info about the user and set stricter read/write rules?
Or do they set database rules on the individual attributes like age, name, etc?
You've basically given the answer yourself... You could either separate the data nodes and add an object with public data and one with private data, where both would have different rules for access.
For the public data node of a user, you of course wouldn't set the rules to allow complete public access when reading, but also limit it to users that are in the friend list.
Another option is to set rules on the individual properties, but this makes the rules quite big and harder to maintain, as every additional parameter needs to be added to the rules manually.
An additional thing I see when looking at your data is the missing data denormalisation. Each user object contains a list of messages/posts done by that user. So if you query a user in users/UID, you'd fetch the whole object including the possibly millions of posts/messages. So you should definitely exclude those from the user objects.

Restrict Firebase users by email

I have a client that would like to be able to make a list of restricted emails that can access the data. So anyone else coming to the app can't read/write any data at all ( ideally can't even log in but I don't think that's possible with Firebase? ). Any ideas on how to go about this? I had thought of having an array of accepted emails and checking whether their email existed in the security rules but that didn't seem to work. I had the following in the database:
"validEmails": ["test#test.com"]
and then in the security rules:
".read": "root.child('validEmails').val().indexOf(auth.token.email) > -1"
But it looks like you can't use indexOf in those security rules.
Maybe I need to have a list of acceptable emails, and then when a user signs up it checks whether they're in that list and adds their UID to an accepted list? I guess I could do this through a cloud function or something?
Any help would be much appreciated.
Cheers
Have the list of allowed user's emails in the database:
"whitelist": {
"fred#gmail%2Ecom": true,
"barney#aol%2Ecom": true
}
Since periods are not allowed in keys, you need to escape strings with periods before storing them.
Then in the database rules:
{
"rules": {
"whitelist": {
".read": false,
".write": false
},
".read": "root.child('whitelist').child(auth.token.email.replace('.', '%2E')).exists()",
".write": "root.child('whitelist').child(auth.token.email.replace('.', '%2E')).exists()"
}
}
User's email is accessible via auth.token.email. You need to escape the dots (. -> %2E) and check if the key exists on the whitelist.
These rules don't allow anyone read or write access to the /whitelist section of the database. Modification is only possible via firebase console.
Thanks guys, what I ended up doing was having a list of acceptable emails:
{
"validEmails": ["test#test.com"],
"validUsers": {}
}
and then have a cloud function run to check when a user signed up if their email was in the valid email list. If it was then it added them to the valid users list and if not it deleted the newly created user. I also set up data rules so that only users within validUsers could access the data.
The front-end then handled the redirection etc for invalid users.
Once you enable the authentication module of Firebase I believe you can't restrict it to email addresses or domains. However you could secure your database another way. If your users are already registered and you know their uid, then you can restrict read and write access based on these.
Lets pretend you have an acl object in the database, you can list the users and their uid with their read/write permissions.
These rules will check each request and only allow authorised users to access the data.
{
"acl": {
[
{
"uid: "abc123"
"canRead": true,
"canWrite": true
},
{
"uid": "def456",
"canRead": true,
"canWrite": false
}
},
"secure": {
".read": { root.child('acl').child(auth.uid).child('canRead').val() == true }
".write": { root.child('acl').child(auth.uid).child('canWrite').val() == true }
}
}

Restrict querying by a certain child value in security rules

I'm struggling to come up with the best way to structure part of my database and its associated security rules.
I have chat groups, and users can be added to those groups at any point. When users are added to a group, they should be able to retrieve only the messages sent after that. It shouldn't be possible for them to retrieve any messages that were sent before they (the users) were added to the group.
My first approach wrongly assumed that security rules would apply only to the data being queried.
Simplifying it for this question, I had the following structure:
{
"groups": {
"-Kb9fw20GqapLm_b8JNE": {
"name": "Cool people"
}
},
"groupUsers": {
"-Kb9fw20GqapLm_b8JNE": {
"3JzxHLv4b6TcUBvFL64Tyt8dTXJ2": {
"timeAdded": 1230779183745
},
"S2GMKFPOhVhzZL7q4xAVFIHTmRC3": {
"timeAdded": 1480113719485
}
}
},
"groupMessages": {
"-Kb9fw20GqapLm_b8JNE": {
"-KbKWHv4J4XN22aLMzVa": {
"from": "3JzxHLv4b6TcUBvFL64Tyt8dTXJ2",
"text": "Hello",
"timeSent": "1358491277463"
},
"-KfHxtwef6_S9C5huGLI": {
"from": "S2GMKFPOhVhzZL7q4xAVFIHTmRC3",
"text": "Goodbye",
"timeSent": "1493948817230"
}
}
}
}
And these security rules:
{
"rules": {
"groupMessages": {
".indexOn": "timeSent",
"$groupKey": {
".read": "root.child('groupUsers').child(auth.uid).child($groupKey).child('timeAdded').val() <= data.child('timeSent').val()"
".write": "!data.exists() && root.child('groupUsers').child(auth.uid).child($groupKey).exists() && newData.child('from').val() === auth.uid",
}
}
}
}
With that, I figured I could retrieve the messages for a particular group like so:
var myTimeAdded = /* already retrieved from the database */;
firebase.database()
.ref('groupMessages/-Kb9fw20GqapLm_b8JNE')
.orderByChild('timeSent')
.startAt(myTimeAdded)
.on('child_added', /* ... */);
But like I said, that was a wrong assumption. Any suggestion on how I could achieve this?
Read rules are enforced at the location where you attach a listener.
So in your case that is groupMessages/-Kb9fw20GqapLm_b8JNE. If your user has read permission there the listener is allowed. If the user does not have read permission, the listener is rejected/cancelled.
This means that rules cannot be used to filter data. We often refer to this as "rules are not filters" and it's one of the most common pitfalls for developers who are new to Firebase's security model. See:
the section rules are not filters in the Firebase documentation
previous questions about Firebase that mention "rules are not filters"
By themselves your rules are not wrong: they only allow access to each specific child if it's not too old. They just don't allow you to run a query on groupMessages/-Kb9fw20GqapLm_b8JNE anymore.
The common way to work around this is to have a separate structure (commonly called an "index") with the keys of the items that your query would otherwise return. In your case it looks like that might turn into a index for each user with the keys of all messages after they joined.
But I'll be honest, it sounds like you're trying to use security rules in a SQL way here. It seems unlikely that the user isn't allowed to see older messages. More likely is that you don't want the user to be bother by the older messages. In that case, I'd just solve it with a query (as you already have) and remove the ".read" rule.

Workaround for Firebase's "Rules Are Not Filters" constraint

I’d like a security rule that lets anyone get a list of users and read their names, but only allows logged in users to view their own email.
Here’s an example data structure:
"User" : {
"abc123" : {
"name" : "Bob",
"email" : "bob#hotmail.com"
}
}
A naive approach to a security rule might be to do the following:
"User" : {
"$user" : {
"name" : {
".read" : true
},
"email" : {
".read” : "auth.uid === $user"
}
}
}
However because there is no read rule at the User level, requests to read the list will be denied. But adding a read rule at the User level will override the email rule, and make every child node readable (see Rules Cascade in Firebase's Security Guide).
The Security guide does point out that Rules Are Not Filters, but doesn’t offer much guidance as to what to do about it.
Should I just split my User entity up into PrivateUser and PublicUser?
To let anyone get a list of users and read their names. AND to allow logged in users to view their own email.
Zac says: first think about access, and then to model objects that are either completely public or completely private.
Type 1:
{"rules":{
"user_publicly":{"$user:{
".read":true,
"name":{}
}},
"user_privately":{"$user:{
".read":"auth != null && $user == auth.uid",
"email":{}
}}
}}
Type 2:
{"rules":{
"user":{"$user:{
"public":{
".read":true,
"name":{}
},
"private":{
".read":"auth != null && $user == auth.uid",
"email":{}
}
}}
}}
A "workaround" would be to use Firestore (has a lot of the good things from Firebase Realtime Database, but adds more querying options, etc).
There is no "rules are not filters" restriction in Firestore!
EDIT:
Thanks to #DougStevenson for making me be more specific.
In Firestore, rules are still not filters, but they are compatible with filtering, unlike in Firebase Realtime DB.
Though, you have to construct your query in such a way, as to only return objects for which you have read permission (otherwise you get a security exception).
Here are some starting point docs:
Security rules:
https://firebase.google.com/docs/firestore/reference/security/
Queries: https://firebase.google.com/docs/firestore/query-data/queries

Basic user authentication with records in AngularFire

Having spent literally days trying the different, various recommended ways to do this, I've landed on what I think is the most simple and promising. Also thanks to the kind gents from this SO question: Get the index ID of an item in Firebase AngularFire
Curent setup
Users can log in with email and social networks, so when they create a record, it saves the userId as a sort of foreign key.
Good so far. But I want to create a rule so twitter2934392 cannot read facebook63203497's records.
Off to the security panel
Match the IDs on the backend
Unfortunately, the docs are inconsistent with the method from is firebase user id unique per provider (facebook, twitter, password) which suggest appending the social network to the ID. The docs expect you to create a different rule for each of the login method's ids. Why anyone using >1 login method would want to do that is beyond me.
(From: https://www.firebase.com/docs/security/rule-expressions/auth.html)
So I'll try to match the concatenated auth.provider with auth.id to the record in userId for the respective registry item.
According to the API, this should be as easy as
In my case using $registry instead of $user of course.
{
"rules": {
".read": true,
".write": true,
"registry": {
"$registry": {
".read": "$registry == auth.id"
}
}
}
}
But that won't work, because (see the first image above), AngularFire sets each record under an index value. In the image above, it's 0. Here's where things get complicated.
Also, I can't test anything in the simulator, as I cannot edit {some: 'json'} To even authenticate. The input box rejects any input.
My best guess is the following.
{
"rules": {
".write": true,
"registry": {
"$registry": {
".read": "data.child('userId').val() == (auth.provider + auth.id)"
}
}
}
}
Which both throws authentication errors and simultaneously grants full read access to all users. I'm losing my mind. What am I supposed to do here?
I don't think you want to store user-specific data under a non-user-specific index. Instead of push()ing to your firebase reference, store the user data behind a meaningful key.
e.g.
auth.createUser(email, password, function(error, user) {
if (!error) {
usersRef.child(user.id).set(stuff);
}
});
Now you can actually fetch user data based on who is authenticated.
The custom Auth in the forge's simulator isn't the greatest but if you hit the tab key after selecting the input, it lets you paste or edit the field. At which point you can add {"provider":"facebook","id":"63203497"} or {"provider":"twitter","id":"2934392"} and hopefully get some useful debug out of it.
Assuming your firebase is something like:
{"registry":{
"0":{
"id":"abbacadaba123",
"index":"0",
"name":"New Device",
"userId":"facebook63203497"},
"1":{
"id":"adaba123",
"index":"1",
"name":"Other Device",
"userId":"twitter2934392"}
}
}
This may work for security rules:
{
"rules": {
"registry":{
"$registryId":{
".read":"data.child('userId').val() === (auth.provider + auth.id)",
".write":"(data.child('userId').val() === (auth.provider + auth.id))||(auth != null && !data.exists())",
".validate": "newData.hasChildren(['id', 'index', 'name', 'userId'])",
"id": {
".validate":"newData.isString()"
},
"index": {
".validate":"newData.isNumber()"
},
"name": {
".validate":"newData.isString() && newData.val().length >= 1"
},
"userId": {
".validate":"newData.val() === (auth.provider + auth.id)"
}
}
}
}
}
Your read rule tested as expected. The facebook user read-tests true on registry 0 and false on 1. The twitter user is false on 0 and true on 1.
I did a couple quick tests on the .write and .validate rules and they seem to work.
Hope this helps at least rule out the firebase security rules portion of things, so you can focus on the AngularFire binding part.

Resources