I want to create a Cloud Firestore realtime database containing groups which users can join and share information to other members of their group. I want to preserve users anonymity so the way I see it to be implemented is:
Group creator generates a group key in format XXXX-XXXX-XXXX-XXXX
Those who want to join must have the group key which they enter in the app, after that they should be able to read, create and update data in that group
So basically the data structure is something like this:
/groups/ : [
//groups as documents
"ABCD-0000-0000-0001" : { /*group data model*/ }
"ABCD-0000-0000-0002" : { /*group data model*/ }
"ABCD-0000-0000-0003" : { /*group data model*/ }
]
The question is, what security rules should I write to permit users to read, create and update data ONLY in the group they belong to (have its group key)? At the same time, how to forbid users from finding out other groups' keys?
Your group structure can remain as is-
groups (Collection) : [
//groups as documents
"ABCD-0000-0000-0001" : { /*group data model*/ }
"ABCD-0000-0000-0002" : { /*group data model*/ }
"ABCD-0000-0000-0003" : { /*group data model*/ } ]
And to maintain the access, you can have another separate collection named group_users as-
group_users(Collection)/ <group_id>(Document)/ members(Collection)/ :
uid_1 (document)
uid_2 (document)
...
Now the rule to allow can be like-
service cloud.firestore {
match /databases/{database}/documents {
match /groups/{group_id} {
allow read, create, update: if exists(/databases/$(database)/documents/group_users/$(group_id)/members/$(request.auth.uid));
}
}
}
When a member joins the group, you will have to add the user's uid to that new collection-
group_users/<group_id>/members
For admins, you can have a similar collection, and uid will be added when the admin creates the group-
group_users/<group_id>/admins
Instead of having a separate collection outside of groups collection, you could have a collection within group itself as an alternative solition, then you will have to modify the group data model to some more extent.
Also FYI, each exists () call in security rules is billed (probably as one read operation).
And here is a detailed documentation explaining almost every possible aspect of firestore security rules.
You can save the group ID in the user's profile. Then create a rule which only allows CRU permissions if that group ID exists.
db.collection.('users').doc({userId}.update({
ABCD-0000-0000-0001: true
})
match /groups/{groupId} {
allow read, create, update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.$(groupId) == true;
}
Related
First, sorry for my terrible English, it is not my native language...
I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data.
In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document.
Here is my group's document:
{
"name":"fefefefe",
"days":[false,false,false,false,true],
"members":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
}
How can I check with the security rules if a user is in a group ?
Should I use an object instead ?
I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...
Thank you for your time !
EDIT:
Thanks for the answer. I will change it to an object :
"Members": { uid1 : {}, uid2 : {} }
In general, you need to write a rule like the following:
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{documentId} {
// works if `members` = [uid1, uid2, uid3]
// no way to iterate over a collection and check members
allow read: if request.auth.uid in resource.data.members;
// you could also have `members` = {uid1: {}, uid2: {}}
allow read: if resource.data.members[request.auth.uid] != null;
}
}
}
You could also use subcollections:
service cloud.firestore {
match /databases/{database}/documents {
// Allow a user to read a message if the user is in the room
match /rooms/{roomId} {
match /documents/{documentId} {
allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
}
match /users/{userId} {
// rules to allow users to operate on a document
}
}
}
}
I made it happen with this code
Allow some user to read/write some document of a collection if this same user is present into an array of another collection
service cloud.firestore {
match /databases/{database}/documents {
match /repositories/{accountId} {
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
}
}
}
Just offering an alternative solution. In my case I store two separate fields. In your case it would be:
"membersSummary":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
"members": ["aaaaaaaa", "bbbbbbbb"]
I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.
I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.
If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.
I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.
I'm struggling use Firestore security rules to secure a many to many relationship.
I have the following collections:
Key:
documentId: [field: value, ...]
groups {
group1: [name: Group1]
group2: [name: Group2]
}
users {
bobUser: [name: Bob]
aliceUser: [name: Alice]
fredUser: [name: Fred]
}
// Contains data specific to a user in a particular group.
// Specifically the user's role
userGroups {
userGroup1: [userId: bobUser, groupId: group1, role: "admin"]
userGroup2: [userId: aliceUser, groupId: group1, role: "member"]
userGroup3: [userId: fredUser, groupId: group2, role: "admin"]
}
How can I construct a firestore security rule so that:
A user with role:"admin" can read another user's document if they both are found in the same group
So in the example above, Bob can read Alice's user document as he has an "admin" role but Fred can't as he is an admin for another group.
Or to put it another way:
If bobUser makes the below request, then it should pass security rules:
db.collection("users").doc("aliceUser");
as bob is has an admin role in the same group as Alice
In contrast, if fredUser was logged in, the below request would fail:
db.collection("users").doc("aliceUser");
Fred is an admin user, but not in the same group and so the rule would block the request.
In the security rule I think I need to split into a few stages:
Query userGroups to find all groupIds where requesting userId is role: "admin"
Query userGroups to find all groupIds where requested user exists
Allow write if there is a match of groupIds in both groups
But I'm having trouble getting this logic into the rule. Security rules don't seem be able to filter like this. Any help would be great!
In order to solve this problem, you need to keep in mind that you cannot transfer relational database patterns to a non-relational database. When working with Firestore, you should start by asking "What queries should be possible?" and then structure your data based on that. Building up Security Rules will follow naturally.
I wrote a blogpost about exactly your use case: "How to build a team-based user management system with Firebase", so if anything from the following answer is unclear, go there first to see if it helps.
In your case, you'd probably want the following queries:
Get all users of a group (given the current user is a member of this group and has the correct permission).
Get all groups of a user (given you are only querying groups of the currently authenticated user).
As you noticed, many-to-many relationships are hard to work with through Firestore and Security Rules, because you would need to make additional requests to join the datasets. To avoid that, I recommend renaming the userGroups collection to memberships and turning it into a subcollection of each doc in the groups collection. So your new structure would look like
- collection "groups", a doc contains:
- field "name"
- collection "memberships", a doc contains:
- field "name"
- field "role"
- field "user" → references doc from "users"
- collection "users", a doc contains:
- field "name"
This way you can easily solve the first query "Get all users of a group" by querying the subcollection "memberships" like collection("groups").doc("your-group-id").collection("memberships").get().
Now, to secure that, you can write a helper functions in Security Rules:
function hasAccessToGroup(groupId, role) {
return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
}
Given a groupId and a role, you can use it allow only users who are a member and have a specific role access to data and subcollections within the group. In order to protect the memberships collection on a group this might look like this:
rules_version = '2'
service cloud.firestore {
match /databases/{database}/documents {
function hasAccessToGroup(groupId, role) {
return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
}
match /groups/{groupId} {
// Allow users with the role "user" access to read the group doc.
allow get: if
request.auth != null &&
hasAccessToGroup(groupId, "user")
// Allow users with the role "admin" access to read all subcollections, including "memberships".
match /{subcollection}/{document=**} {
allow read: if
request.auth != null &&
hasAccessToGroup(groupId, "admin")
}
}
}
}
Now there is only the second query "Get all groups of a user" left. This can be achieved through a Collection Group Index, which allows to query all subcollections with the same name. You want to create one for the memberships collection. Given a specific user, you can then easily query all of his groups with collectionGroup("memberships").where("user", "==", currentUserRef).get().
In order to secure that, you need to setup a Rule that allows such requests only if the queried user reference equals the currently authenticated user:
function isReferenceTo(field, path) {
return path('/databases/(default)/documents' + path) == field
}
match /{document=**}/memberships/{userId} {
allow read: if
request.auth != null &&
isReferenceTo(resource.data.user, "/users/" + request.auth.uid)
}
One last thing to talk about is how you keep the data in the memberships collection up-to-date with the data in the users doc that it references. The answer are Cloud Functions. Every time a users doc changes, you query all of its memberships and update the data.
As you can see, answering your original question how you can construct a Firestore Rule so that a user with the correct permission can read another user's document if they both are found in the same group, takes a different approach. But after restructuring your data, your Security Rules will be easier to read.
I hope this helped. Cheers!
I am having an issue with Firestore rules when the permission is stored in another document in another collection. I haven't been able to find any examples of this, but I have read that it can be done.
I want to do it this way to avoid having to do a lot of writes when a student shares his homework list with many other students. Yes, I know this counts as another read.
I have 3 collections, users, permissions, and homework along with some sample data.
users
{
id: fK3ddutEpD2qQqRMXNW,
name: "Steven Smith"
},
{
id: YI2Fx656kkkk25,
name: "Becky Kinsley"
},
{
id: CAFDSDFR244bb,
name: "Tonya Benz"
}
permissions
{
id: fK3ddutEpD2qQqRMXNW,
followers: [YI2Fx656kkkk25,CAFDSDFR244bb]
}
homework
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false
}
The start of my firestore rules:
service cloud.firestore {
//lock down the entire firestore then open rules up.
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /homework/{ } {
allow get: if isSignedIn()
}
// helper functions
function isSignedIn() {
return request.auth != null;
}
function isUser(userId) {
return request.auth.uid == userId;
}
function isOwner(userId) {
return request.auth.uid == resource.data.uid;
}
}
}
Use case:
Steven Smith Shared his homework list with Tonya Benz.
Tonya Benz is logged into the app to view her friend Steven's homework. The app runs this query to get the homework list of Steven Smith.
var homeworkRef = db.collection("homework");
var query = homeworkRef.where("owner", "==", "fK3ddutEpD2qQqRMXNW");
Question:
What is the proper Firestore match rule that takes the "owner" field from the homework collection to look up it up as the id in the permissions collection when the user Tonya Benz is signed in so this query can run.
With your current query and database structure, you won't be able to achieve your goal using security rules.
Firstly, it sounds like you're expecting to be able to filter the results of the query based on the contents of another document. Security rules can't act as query filters. All the documents matched by the query must be granted read access by security rules, or the entire query is denied. You will need to come up with a query that is specific about which documents should be allowed access. Unfortunately, there is no single query that can do this with your current structure, because that would require a sort of "join" between permissions and homework. But Firestore (like all NoSQL databases), do not support joins.
You will need to model your data in such a way that is compatible with rules. You have one option that I can think of.
You could store the list users who should have read have access to a particular document in homework, within that same document, represented as a list field. The query could specify a filter based on the user's uid presence in that list field. And the rule could specify that read access only be granted to users whose IDs are present in that list.
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false,
readers: [ 'list', 'of', 'userids' ] // filter against this list field
}
The bottom line here is that you'll need to satisfy these two requirements:
Your query needs to be specific about exactly which documents that it expects to be readable. You can't use a rule to filter the results.
Your rule needs a way to determine, using nothing more complicated than the fields of the document itself, or a get() on other known documents, what the access should be for the current uid.
So in my database schema I have something like this
/collections/users/userId1 {
...,
id,
relatedContracts: [id1, id2, id3]
}
/collections/contracts/contractId {
...,
id,
}
In my firebase security rules I have this
match /contracts/{contract} {
function isOwnContract() {
return request.auth.id == resource.data.creatorId || request.auth.id == recipientId
}
allow read, write: if isOwnContract()
}
Now I am very confused on how to filter on the client to get only my related contracts. If the introduction of the security rules I can no longer do my filtering on the client.
I know this exists
firestore.collection(CONTRACTS).where(id, "==", contractId)
But how do I do this filtering based on multiple ids
You currently can't fetch multiple ids per query on the client, so your choices are to either:
Iterate each contract id in the user document, and fetch each document separately (which is not as bad as you probably think)
Maintain another relationship in each contract that contains a list of users that have it as a relation. Then search the contracts collection for a user id in that list.
First, sorry for my terrible English, it is not my native language...
I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data.
In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document.
Here is my group's document:
{
"name":"fefefefe",
"days":[false,false,false,false,true],
"members":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
}
How can I check with the security rules if a user is in a group ?
Should I use an object instead ?
I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...
Thank you for your time !
EDIT:
Thanks for the answer. I will change it to an object :
"Members": { uid1 : {}, uid2 : {} }
In general, you need to write a rule like the following:
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{documentId} {
// works if `members` = [uid1, uid2, uid3]
// no way to iterate over a collection and check members
allow read: if request.auth.uid in resource.data.members;
// you could also have `members` = {uid1: {}, uid2: {}}
allow read: if resource.data.members[request.auth.uid] != null;
}
}
}
You could also use subcollections:
service cloud.firestore {
match /databases/{database}/documents {
// Allow a user to read a message if the user is in the room
match /rooms/{roomId} {
match /documents/{documentId} {
allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
}
match /users/{userId} {
// rules to allow users to operate on a document
}
}
}
}
I made it happen with this code
Allow some user to read/write some document of a collection if this same user is present into an array of another collection
service cloud.firestore {
match /databases/{database}/documents {
match /repositories/{accountId} {
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
}
}
}
Just offering an alternative solution. In my case I store two separate fields. In your case it would be:
"membersSummary":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
"members": ["aaaaaaaa", "bbbbbbbb"]
I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.
I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.
If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.
I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.