Skip to content

Instantly share code, notes, and snippets.

@katowulf
Last active April 11, 2024 12:07
Show Gist options
  • Star 58 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save katowulf/4741111 to your computer and use it in GitHub Desktop.
Save katowulf/4741111 to your computer and use it in GitHub Desktop.
Firebase security rules for a simple chat room model
{
"chat": {
// the list of chats may not be listed (no .read permissions here)
// a chat conversation
"$key": {
// if the chat hasn't been created yet, we allow read so there is a way
// to check this and create it; if it already exists, then authenticated
// user (specified by auth.id) must be in $key/users
".read": "auth != null && (!data.exists() || data.child('users').hasChild(auth.id))",
// list of users authorized to participate in chat
"users": {
// if the list doesn't exist, anybody can create it
// if it already exists, only users already in the list may modify it
".write": "!data.exists() || data.hasChild(auth.id)",
"$acc": {
// for now the value is just a 1, later it could be a read/write/super privilege
".validate": "newData.isNumber()"
}
},
// timestamps recording last time each user has read this chat
"last": {
"$acc": {
// may only written by the authenticated user and if user is in $key/users
".write": "$acc === auth.id && root.child('chat/'+$key+'/users').hasChild($acc)",
".validate": "newData.isNumber()"
}
},
"messages": {
"$msg": {
// to write a message, it must have all three fields (usr, ts, and msg)
// and the person writing must be in $key/users
".write": "root.child('chat/'+$key+'/users').hasChild(auth.id)",
".validate":"newData.hasChildren(['ts', 'usr', 'msg'])",
"usr": {
// may only create messages from myself
".validate": "newData.val() === auth.id"
},
"msg": {
".validate": "newData.isString()"
},
"ts": {
".validate": "newData.isNumber()"
}
}
}
}
}
}
@katowulf
Copy link
Author

katowulf commented Feb 8, 2013

The schema looks as follows:

/chat/$key - a chat conversation
/chat/$key/users - list of users allowed to participate
/chat/$key/last - last time each user checked the chat (for marking messages new)
/chat/$key/messages - the chat history

@mikelehen
Copy link

This looks great. If I were to suggest any improvement, it'd just be a tiny tweak to perhaps improve readability. Instead of:

  ".write": "newData.hasChildren(['ts', 'usr', 'msg']) && "
        +"root.child('chat/'+$key+'/users').hasChild(newData.child('usr').val())",

You could do:

  ".write": "root.child('chat/'+$key+'/users').hasChild(auth.account)",
  ".validate":"newData.hasChildren(['ts', 'usr', 'msg'])"

But overall I really like it. Simple and clean. :-)

@bennlich
Copy link

Awesome! Tell me if I've got this right:

  1. Anyone can start a new chat.
  2. Anyone participating in a chat can grant someone else permission to participate.
  3. Anyone participating in a chat can revoke anyone else's permission to participate.
  4. No one can join or read an existing chat without being given permission by one of its participants.

@psamaan
Copy link

psamaan commented Nov 9, 2015

as neat and clean as it looks, I'm afraid that according to the docs that's an antipattern for Firebase (as of Nov 8, 2015), because the entire tree will have to be fetched (including all messages) just to iterate over room keys, for example. See here: https://www.firebase.com/docs/web/guide/structuring-data.html#section-nested

@AdrienDC
Copy link

AdrienDC commented May 4, 2016

How can I do a firebaseArray on chat if I can't read the chat list?

@katowulf
Copy link
Author

You can shallow=true to iterate room keys but there's no reason to do so. If you want a list of rooms for presentation on the client then keep a list of rooms for presentation on the client (in a separate path in easily digestible format). Don't fetch the chat data to get the room list; fetch the room list.

@quantuminformation
Copy link

quantuminformation commented Oct 17, 2020

Did anyone add e2e encryption on top of this?

@psamaan, you wouldn't need to iterate of the room keys, if you need a list of rooms a user is involved with you would store that in a different path.

CC @Navil

Oh, Kato already answered it for me)

@quantuminformation
Copy link

quantuminformation commented Oct 19, 2020

The only other improvements I can think of is to bin the messages per month or another time period for bandwidth saving.

Also, we need a way to force 1 to 1 conversation as only having one instance. Think messenger and LinkedIn, you can only have one direct conversation with someone.

The last read thing with timestamps is genius.

@quantuminformation
Copy link

quantuminformation commented Oct 19, 2020

In terms of UI I'm thinking how to implement this.

The main problem I see is that there are two keys been generated for the first message. Normally the key is generated when we do a push to a path.

But for a new chat and on the first message (if we create them at the same time), we will have to generate two new IDs one for the actual chat and one for the first message. In this case, I think there are two steps 1st to create the chat and only then we add new messages.

I'm almost thinking a cloud function would be best for creating the chat which would also check to see if a previous conversation is created with the same users, only 1 to 1.

thoughts?

Update----
Previously one-to-one conversations were easy as I could use this code to generate the ID on the fly

export async function sendMessage(myUid, otherUid, content) {
    const conversationId = createConversationId(myUid, otherUid)
    const data = { content, timestamp: new Date().getTime(), authorUid: myUid }
    await firebase.database().ref(`conversations/${conversationId}`).push(data)
    return
}


export function createConversationId(uid1, uid2) {
    return uid1 > uid2 ? `${uid2}${uid1}` : `${uid1}${uid2}`
}

But I was told I could not rely on the length of the UID so that's why am in this thread

FURTHER THOUGHTS-----------
I dont' see a way for if conditions in the rules, so probably different chat paths for 2 way convo vs (2n + 1 ) convos, this would allow the following rules

2 way

checks to see that no other 2 way chat has the same two user

implementation ideas

A cloud function that looks at the chat creation and if there are only 2 users, it sets a property called one2one or smth than can't be changed.
The difficulty is for when a user wants to message another user, finding a good way to search for a previous conversation based on their 2 ids.

3 way group message

nothing special here, we just need a way to not allow group messages with the same 3 or more people , like facebook and linkedin

Last thoughts, I still think it would be quite safe to use the concatenation method for the key because I don't imagine FB is changing the length of your ID any time soon at least for the same project

@quantuminformation
Copy link

I've been thinking about this all day and have come to no elegant solution.

The only elegant solution is the concatenating method which I think firebase should support better; by locking uids format for any given project (it currently works very well using length 28 uids). Given that this is the only real-time usage case for firebase for me I'm wondering if firebase is the correct option here for this aspect of the social network I'm making.

@quantuminformation
Copy link

We decided on hashing users uid's, which means you can look up any existing conversation if you know the other persons uid's.

Each conversation also stores a list of the uids for their security rules, so even if you can guess the hash, you are protected.

@quantuminformation
Copy link

quantuminformation commented Oct 26, 2020

I would also recommend creating all timestamps with cloud functions to account for differences in time zones and computer clocks. Also if the user updates his last read value, that should also be server updated

@quantuminformation
Copy link

Please also note, that when writing to users you need to use update not set as this gotcha:

https://stackoverflow.com/questions/64544925/fireplace-update-allowed-but-set-fails-with-simulated-set-denied/64549981#64549981

@quantuminformation
Copy link

Also note

".write": "!data.exists() || data.hasChild(auth.id)",

should be

       ".write": "!data.exists() || data.hasChild(auth.uid)",

thank me later

@quantuminformation
Copy link

quantuminformation commented Oct 28, 2020

The only thing I would like to add to this is a way for any user in the chat to delete the entire conversation which is not possible as they would have to have had access to the whole conversation, which would break the more granular write rules.

Probably needs a cloud function with a custom token, if anyone has an idea how to do that.

@Carlos-ag
Copy link

How do you include a list of users in realtime database?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment