Firebase private chat schema and rules - firebase

I am trying to setup private chat abilities in an app that I am working on and I'm having a bit of trouble wrapping my head around denormalizing the data/setting up the rules properly.
After doing some reading, I realize that rules are all or nothing so using rules to filter is not an option.
I've sketched out my basic idea on paper, and have pasted it below. Basically there would be two main routes, users and chats.
Users would just be a keyed list, which each key matching an authenticated user. Then inside each member of the list I would just have each chat that the said user is in listed as a key.
For the chats route I'd have a list of all of the chats.
Now for the rules.
Users would only be able to read their data in the list where the key matched their uid. For the write i'm less confident. I'm thinking I have to let anyone with authentication write, otherwise the user starting the chat could not notify others of the new chat by playing the chat id in their chat list in the users route.
For the chats rules both read and write would only be allowed if the user is authenticated and the chat key is located inside their data in the user route.
Does the seem like I'm going in the right direction?
users:{
user1:{
chat1: true,
chat2: true
...
},
user2:{
chat1: true,
chat3: true
....
}
}
chats:{
chat1:{
lastUpdate: timestamp,
messages:{
0:{
from: user1
to: user2,
message: some message
}
...
}
}
}
rules:{
.read: false,
.write: false,
users:{
$user_id:{
.read: auth != null && $user_id == auth.uid,
.write: auth != null //not sure here as other users need to write here if the start a new chat
}
},
chats:{
$chat_id: {
.read: auth != null && root.child('users').child($chat_id).contains(auth.id),
.write: auth != null && root.child('users').child($chat_id).contains(auth.id)
}
}
}

I've been playing with this more, so here is one option (by no means am I suggesting this is the best way to do it)
Rules:
{
"rules":{
".read": false,
".write": false,
"users":{
"$user_id":{
".read": "auth != null && $user_id == auth.uid",
".write": "auth != null" //not sure here as other users need to write here if the start a new chat
}
},
"chats":{
"$chat_id": {
".read": "auth != null && root.child('users').child(auth.uid).child('chats').hasChild($chat_id)",
".write": "auth != null && (root.child('users').child(auth.uid).child('chats').hasChild($chat_id) || !data.exists())"
}
}
}
}
then for users I have a structure like this:
users:{
someUserId:{
chats:{ //embedded a second level so I can save firebaseObj.someUserId to get the keys more easily
someChatId: true //and repeat for each chat
}
}
}
Chats are like this:
chats:{
someChatId:{
//chat data
}
//more chat objects
}
I wouldn't be surprised if there is a much better way to do this, but at least this may be a start for those who are stuck. I'll try to remember to update this if/when I get a better solution.

Related

firebase realtime database security rules for non user data

So, I have an app where users can order the cakes and do other profile management, the rules looks like below:
{
"rules": {
"cakes" : {
".read": true,
".write": false
},
"users": {
"$user_id": {
".read": "auth != null && $user_id === auth.uid",
".write": "auth != null && $user_id === auth.uid"
}
}
}
}
Simply, they mean any one can read the cakes node (but no one can write). And an authenticated user can see or write to his on node.
This is good so far.
Now, my requirement is: When someone places an order through the app then i need to store it to firebase db in a top level node (lets say it orders). The question is what kind of security would be placed on orders node?
In functional definition: The app should be able to create new orders as user checks out, no one except seller should be able to read those orders, only seller should be able to have update access to a order.
If you want everybody to be able to write orders, and nobody able to read, the rules are simply the inverse of the ones for cakes:
"rules": {
"orders" : {
".read": false,
"$orderId": {
".write": true
}
},
With this anyone can push new data under /orders. And as long as you use push() to generate the new keys, you're guaranteed that they'll never conflict.
With these rules only a system-level admin can read the data. That is: you can read it in the Firebase Console, or someone can read it if they use the Admin SDK.
You might want to open it up for reading a bit more, e.g. by having the concept of an application-level administrator. Say that your UID is uidOfVik, you could model a list of admins in your database:
admins: {
uidOfVik: true
}
Now you can allow only admins to read the orders with:
"rules": {
"orders" : {
".read": "root.child('admins').child(auth.uid).exists()",
"$orderId": {
".write": true
}
},

How to extract data from node variables to validate user roles on firebase

We are adding authentication roles to firebase.
our messaging structure is like this:
/users/<userId>/rooms/<roomId>/
roomId is custom string which concatenating both users(sender-reciver):
for example:
/users/4hkjheqrnv501eltbg1px/rooms/4hkjheqrnv501eltbg1px-638red8osipedem08j1a6/
i want to give write access to this room only for usersId's
4hkjheqrnv501eltbg1px or 638red8osipedem08j1a6 (which the roomId specifing)
How can I actually extract the userId's from the roomId variable and check if one of them is within the auth object.
I tried it this way but didnt work:
"rooms": {
"$roomId": {
".write": "auth != null && auth.uid.contains($roomId)"
}
so write access will be granted to this room only for auth users 638red8osipedem08j1a6 4hkjheqrnv501eltbg1px
Any ideas how to modify this?
I think you need to split your room address to array and find if the user id is in the array like:
"rooms": {
"$roomId": {
".write": "auth != null && $roomId.split('-').indexOf(auth.uid) > -1"
}
}

Firebase: Cannot add user entry (`set` permission denied)

I want to ensure that a new user can be created (from the client), but only an authenticated user can read or write an existing object.
I have a simple rule set:
{
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "!data.exists() || auth.uid === $uid"
}
}
}
}
I am calling createUser and then in the callback I'm trying to add an entry to my own users object:
const usersRef = ref.child('users');
const userEntry = {
[userData.uid]: {
created: new Date().getTime()
}
};
usersRef.set(userEntry)
I would have thought that even though the user is not yet logged in, they should have write permission because of !data.exists(). Yet I am getting a PERMISSION_DENIED error.
If I set ".write": true on the users level then it will cascade (and override?) my inner rules won't it?
Edit:
This fails even with:
"users": {
"$uid": {
".read": true,
".write": true
}
}
Thanks.
I think I initially misunderstood the problem. In your current callback, you are trying to overwrite the entire users level because of how set works.
You would really want to set only the thing that doesn't exist:
const userRef = ref.child('users').child(userData.uid);
const userEntry = {
created: new Date().getTime()
};
userRef.set(userEntry);
Then, I think that your existing rules would work.
I think this a confusing question because creating a user and writing to the database are completely different things. So i will just show how i do it in my app.
First step is creating the user
Next log the user in because creating doesn't automaticly log the user in (I do this in the callback function of create user)
Last step is writing the user data to firebase
I use the following rule to make sure each user can only write to his own node in firebase (documentation):
{
"rules": {
"users": {
"$user_id": {
".write": "$user_id === auth.uid"
}
}
}
}
And one thing to keep in mind is that set() will replace any existing data at that path. So make sure you use the uid of the user and not the users node.
Finally i want to point out a huge flaw in the rules you posted in your question:
".write": "!data.exists() || auth.uid === $uid"
This rule states you can write if there isn't any data yet OR you have the correct uid. The first part of this statement is the problem because ANYONE can write to this locaion when there isn't any data. Also because $uid is a dynamic path you can add anything there like:
"users": {
"WoWIjustMadeThisUp": {
"nice": "Some huge value making you go over your limit"
}
}
If you want users to only write an initial value and after that won't be able to edit it just use a validate rule to check if there is already data at that location.

Adding Security to Firebase to prevent the insertion of additional data

I have a field within Firebase called 'pending_members' which contains a list of members pending permission to be granted by an 'owner', as such, 'pending_members' requires the following rules:
The current user can only add themselves (uid)
The current user can remove only themselves (uid) from the list
The 'owner' can remove any member from the list
Only the 'owner' can read the list
I've tried various security rules but seem to miss many corner cases, for example, a user is given write access because the data contains their uid but they can then submit someone else's uid along with this.
Can anyone suggest appropriate rules for this situation? Many thanks
"pending_members" : {
".write" : "auth !== null &&
// The user is authenticated AND
(newData.child(auth.uid).exists() ||
// The new data contains either the current user's id OR
(!newData.exists() &&
// There's no new data (a delete operation) AND
data === auth.uid))",
// The old data is the current user's id
"$member" : {
".validate" : "newData.isString()",
"$other": { ".write": false, ".read": false }
}
}
Edit:
Structure Example:
users ->
personal_data ->
email (user email address)
first_name (user first name)
last_name (user last name)
networks_index ->
networks ->
members (list of uids of users linked to the network)
owner (uid of the owner/primary user)
pending_members (list of uids of users wishing to link to the network)
Data Example (image)
Complicated structure you have but i will give it a try:
Keep in mind standard value for read and write is false.
{
"rules": {
"networks": {
"$networkid": {
//Give read and write access to the owner of the network
".read": "auth != null && "root.child('networks').child($networkid).child('owner').val() == auth.uid",
".write": "auth != null && "root.child('networks').child($networkid).child('owner').val() == auth.uid",
"pending_members": {
"$uid": {
//Give members write access to their own node inside pending_members
".write": "auth != null && auth.uid == $uid",
//Use validate to check if the value is a bool or emty(removal)
".validate": newData.isBoolean() || !newData.exists()
}
}
}
}
I have only concentrated on the pending_members here, I hope that is enough and it is clear enough. If it doesn't work i suggest testing each rule seperatly to see which one is causing a ploblem so I (or someone else) can help fix it.

Firebase security rule gives permission denied?

I'm struggling to set the proper security rules for my application.
An overview of the application I'm writing is that users can register themselves using email and password (I'm using Firebase Simple Login for this which works perfectly). Once logged in, user can add their todos.
angularFire('https://<firebase>/firebaseio.com/todos', $scope, 'todos');
And to add a new todo against any user, I simply update the todos model.
$scope.todos.push({
user: 'a#b.com',
todo: 'What to do?'
});
This security rules I'm using to restrict non-registered user to add any todo:
{
"rules": {
".read": true,
"todos": {
".write": "auth != null",
".validate": "auth.email == newData.child('user').val()"
}
}
}
But it does not allow even an authenticated user to write any data and throwing an error,
"FIREBASE WARNING: on() or once() for /todos failed: Error: permission_denied."
But If I add the following data in simulator then it works as expected.
{user: "a#b.com", todo: 'What to do?'}
Here is the log:
/todos:.write: "auth != null"
=> true
/todos:.validate: "auth.email == newData.child('user').val()"
=> true
/todos:.validate: "auth.email == newData.child('user').val()"
=> true
Write was allowed.
push adds a new child with a randomly generated ID (in chronological order) to /todos. So, newData isn't pointing to what you think it is pointing to. Change your rules to:
{
"rules": {
".read": true,
"todos": {
"$todoid": {
".write": "auth != null",
".validate": "auth.email == newData.child('user').val()"
}
}
}
}
Update: Above rule is valid but angularFire currently writes the whole array back to the server causing the auth to fail. You can use angularFireCollection instead, to only write the new TODO back, like so:
$scope.todos = angularFireCollection(new Firebase(URL));
$scope.todos.add({user: 'a#b.com', todo: 'What to do?'});
There's an open issue to optimize angularFire's behavior when new items are added to the list, but in the meantime you can use angularFireCollection to get the right behavior.

Resources