Skip to content

Commit

Permalink
fix: tighten Firestore rule checks for chats
Browse files Browse the repository at this point in the history
More specifically:
- enforce superfan/member status on chat create
- make sure users can only create chats from themselves (not impersonate others)
  • Loading branch information
th0rgall committed Aug 8, 2023
1 parent ddaaa09 commit 2697500
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 7 deletions.
2 changes: 1 addition & 1 deletion api/seeders/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const seed = async () => {
{ email: 'user3@slowby.travel' },
{ firstName: 'Jospehine', lastName: 'Delafroid', countryCode: 'FR', superfan: true }
).then((user) => createGarden({ latitude: 50.9427, longitude: 4.5124 }, user)),
// No superfan, no garden, has past chats
// No superfan, no garden, has past chats (TODO)
createNewUser(
{
email: 'user4@slowby.travel'
Expand Down
28 changes: 22 additions & 6 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ service cloud.firestore {
return get(/databases/$(database)/documents/chats/$(chatId)).data.users;
}

// Function does not work on creation operations (existingData reference)
// Conditions for chat reads
// Note: this function will not work on *creation* operations (existingData reference)
function canReadChat() {
return isSignedIn() && isVerified() && requesterId() in existingData().users;
}
Expand All @@ -220,13 +221,17 @@ service cloud.firestore {
function isValidChatWrite() {
return isSignedIn() && isVerified()
&& incomingData().keys().hasAll(['users', 'lastActivity', 'createdAt', 'lastMessage'])
// A chat can only exist between exactly two users
&& incomingData().users.size() == 2
// A user can not send a message to themself
&& incomingData().users[0] != incomingData().users[1]
&& incomingData().lastActivity is timestamp
&& incomingData().lastMessage is string
&& incomingData().lastMessage.size() >= 1
&& incomingData().lastMessage.size() <= 500
// The following keys are optional
// The following keys are optional.
// The client creates a chat before it has sent the first message in it, so these message-related properties may be empty.
// Some old chats with existing messages before these features were lauched, may also not have these keys, until someone sends a new message in the chat.
&&
(
!incomingData().keys().hasAny(['lastMessageSeen', 'lastMessageSender'])
Expand All @@ -240,24 +245,35 @@ service cloud.firestore {
)
}

// Conditions for chat creations
function canCreateChat() {
return isValidChatWrite()
// The first user in the participating `users` array must be the chat creator (first message sender)
&& incomingData().users[0] == requesterId()
// Only superfans (members) can create chats
&& get(/databases/$(database)/documents/users/$(requesterId())).data.superfan == true
// Both participants exist
&& userExists(incomingData().users[0])
&& userExists(incomingData().users[1])
}


// Conditions for chat updates
function canUpdateChat() {
return isValidChatWrite()
// Immutable properties didn't change
// Only chat participants can update the chat
&& requesterId() in incomingData().users
// Participants can not change the creation timestamp
&& incomingData().createdAt == existingData().createdAt
&& incomingData().users[0] == existingData().users[0]
&& incomingData().users[1] == existingData().users[1]
// Participants can not change the chat participants
&& incomingData().users == existingData().users
}


function canReadMessage(chatId) {
return isSignedIn() && isVerified() && requesterId() in getChatUsers(chatId);
return isSignedIn() && isVerified()
// this condition doubles as a "does the chat exist" check
&& requesterId() in getChatUsers(chatId);
}

function canSendMessage(chatId) {
Expand Down

0 comments on commit 2697500

Please sign in to comment.