I'm creating an application in Firebase that needs ensure that users can only read/write documents/collections that belong to their team. My question is basically how should I structure the membership data. Specifically here is one idea I was considering:
/teams/{teamid}
{
displayName: "Company X Team",
owner: "userid",
members: ["email1", "email2", "email3"]
}
/teams/{teamid}/projects/{projectid}
{
name: "My Project"
otherProps: "Other Properties"
}
/users/{userid}
{
displayName: "John Doe",
email: "john.doe#somedomain.com"
}
Should I store the "members" as an array as part of the /teams/ document (as pictured in the code sample above)? Or should I store it as its own collection of documents, much like the /projects/?
My main concern is when I go and write security rules. I want to make sure only the owner or members are allowed to make changes to /teams/ and its sub collections (like: /projects/).
Which way is going to be easier to write security rules for? Which one will have better performance?
I would structure the members as a subcollection instead of an array. Make the document ID of each subcollection member the userID.
Then, you can just write a simple exists() security rule. This would also allow you to store additional data about the membership (are some members admins, when did they join, etc).
match /teams/{teamid}/projects/{projectid} {
// Make sure a 'user' is a member of the team
allow write: if exists(/databases/$(database)/documents/teams/${teamid}/members/$(request.auth.uid))
}
https://firebase.google.com/docs/firestore/security/rules-conditions#access_other_documents
Related
I have a simple fireStore collection named jokes where there is a document per joke.
In a joke, there is a key published: boolean
So the idea is to have a single collection with jokes, but each document can be either published or unpublished. I would not like users to view unpublished jokes.
In my fireStore rules, i have the following:
match /jokes/{id} {
allow read: if get(/databases/$(database)/documents/jokes/$(id)).data.published == true
}
In my application, I want users to be able to see only the published jokes, therefore i use the where
this.$fireStore.collection('jokes').where('published', '==', true).get()
When I do this, the console tells me i got insufficient permissions.
Is it possible to use this pattern, or do I have to use cloud functions to serve the published jokes? Or maybe a separate collection for unpublished ones?
You don't need a get() in the rule for this, as the current document is already available as resource. In fact, that get() is the problem here, as the rules engine cannot statically evaluate that for all document in one go.
match /jokes/{id} {
allow read: if resource.data.published == true
}
Also see the documentation on securely querying data, specifically the section on securing and querying documents based on a field
My webapp, like 99% of apps, has in db a collection of users. I can access user at /user/ so I setup rule to allow every user to access only its data.
BUT I need every user join a group
/user/34029380432
- name: realtebo
- group_id: 123
- isAdmin: true
/group/123
- a lot of shared data, all members can erad, only isAdmin can write
How can I setup a rule that allow only members of each group to read it and only admin members to write group data ?!
Of course I could swap side, movin member list, as array, under each group and setting, inside each group, one or more admin id.
But I am not able to compose javascript rule.
I think what you could do is having a messages and users node within your group objects. You can access to parent object for that data node on your security rules and see if that user is a member of users object node by the use of exists()
/group/123
- messages // a lot of shared data, all members can read, only isAdmin can write
- users // list of user uids that are added to this group
- admins // list of admin uids
"group": {
"$groupId": {
"messages": {
".read": "data.parent().child('users').child(auth.uid).exists()",
".write": "data.parent().child('admins').child(auth.uid).exists()"
}
}
}
I'm trying to create a Firestore auth rule that checks the current user against a list of team members in a team document. The members are stored as document references so I've been trying things like this:
match /teams/{document=**} {
allow read: if path("/users/" + request.auth.uid) in resource.data.members;
}
But when I try and access the team document I get told there is an Auth failure.
Each team member has their own document in /users using their UID as a key. So a user might be /users/12345678 and the teams document might have:
/teams/team1 {
members: [/users/12345678, ....]
}
Where the members are Reference types.
So far I've not been able to figure this out as Firestore does not seem to have the concept of a document reference type in it's auth rules.
Any suggestions?
Security rules do have a concept of a reference, and it's represented as a Path type object. When a document reference is read by security rules, you have to treat it like a Path. And that Path will be fully qualified like this:
/databases/$(database)/documents/collection/documentId
Where $(database) comes from your usual top-level database wildcard match.
So, your rule might be implemented like this:
match /teams/{document=**} {
allow read: if /databases/$(database)/documents/collection/users/$(request.auth.uid) in resource.data.members;
}
Note that in security rules, you can build a path simply by starting with a /, and use $(foo) for interpolating variables as path components.
I can't understand the way Firebase db works.
Yes, I know it is a JSON with my data, I know the ways to set one-to-many and many-to-many relationship between different objects. But is there any way to set up some kind of "schema" to database?
An example to understand what I mean: what if I'm creating an Android app, using Firebase SDK, and my friend is creating an iOS app. I'm pushing this to "users": {name: John, city: LA}. And my friend makes a mistake, pushing {name: Tom, cety: NY}. Than, trying to get Toms city - what will I get? Null?
Is there a way to specify a structure to saved data. In Firebase console I see only way to add exact values, not specify the way it should be structured. How to tell other developers, or "me-in-the-future" that my users should contain, for example "name", "gender" and "dog_eyes_color"?
Hope you understand me.
To answer your question, Firebase is a schema-less database. If you push data to a node
users
uid_0
name: John
city: LA
and your friend adds another user
uid_1
name: Frank
cety: NY
Then your users node will have two users with children that have different keys; city vs cety.
As Frank mentioned in his comment, you can 'catch' and prevent data from being written to a node that's invalid. However, ponder this class:
class User {
userId = ""
name = ""
city = ""
func saveToFirebase() {
myRef.setValue( user id etc etc)
}
}
and you tell your friend that any time they want to store a user in Firebase, to use that class. You now have a standardized model in which to interact with Firebase. That class (or structure or whatever you use) defines a schema to work with.
The Firebase database (NoSQL) provides a mechanism for storage and retrieval
of data which is modeled in means other than the tabular relations
used in relational databases
And as a followup to the users question: Firebase DOES store user authentication data 'in the back end' which is not directly accessible (queryable) to the developer. The idea here is that when a user is created in Firebase with Firebase functions such as createUser(), you are provided the user id (UID) of that user when it's created and that's what you can use to store additional information in a /users node you create.
users
uid_0
name: Frank
location: LA
fav_food: Pizza
uid_1
name: Leroy
location: NY
fav_food: Tacos
.validate....
I would not leverage .validate rules to define a structure (schema) or keep other developers in check. Providing coding tools such as the Users class mentioned above will provide far more flexibility and less aggravation (and coding) in the long run and will be much more maintainable.
how about this way of implementation
class User {
constructor(name, city) {
this.name = name;
this.city = city;
return {
name: this.name,
city: this.city,
};
}
}
const saveDocument = function (collectionName, obj) {
db.collection(collectionName)
.add(obj)
.then((docRef) => {
console.log("Document written with ID: ", docRef.id);
})
.catch((error) => {
console.error("Error adding document: ", error);
});
};
saveDocument("user", new User("Jhon", "NY"));
The following answer goes for anyone using Firestore and having this same question.
Firebase security rules, this video does an amazing job explaining them, are a way in which you can sort of create schemas.
An important consideration when using these rules is that if you're using the Firestore Admin SDK (used for backend code) then your rules will be bypassed. To enforce this rules the request must come from a client SDK. I think that one of the best things about Firebase is serverless computing so this type of validations should work just fine with most requests considering they come from the client.
To validate requests coming from a source that bypasses the basic security rules make sure to set up Identity and Access Management (IAM) for Cloud Firestore.
We'd like to use Firepad in our (mostly non-Firebase hosted) project, but we're having some troubles figuring out the best way to approach the problem.
Basically, we have many users, and each user can be a member of many groups. These "groups" each have their own Firepad which users can edit. We already have a deeply developed database structure using MySQL and don't really want to migrate our user data into Firebase right now, so we figured we'd get more creative.
We don't want users being able to edit the Firepads of groups they do not belong to. As such, as part of our authentication token, we figured we'd try sending along the user ID and the list of groups they belong to. Then, using the Firebase JSON security system, we could verify that the Firepad currently being edited is in the list of groups the user belongs to.
The problem is, the JSON system doesn't seem to accept many commands. There's no indexOf, and I can't call hasChild on the auth variable.
How can we ensure that users can only edit the Firepads of groups they belong to, without migrating all of our data to Firebase? (Or maintaining two copies of the database - one on MySQL and one on Firebase)
The trick here is to use an object instead of an array to store the groups (a tad awkward, I know. We'll try to make this easier / more intuitive). So in your auth token, you'd store something like:
{ userid: 'blah', groups: { 'group1': true, 'group2': true, ... } }
And then in your security rules you could have something like:
{
...
"$group": {
".read": "auth.groups[$group] == true",
".write": "auth.groups[$group] == true"
}
}
And then a user will have read/write access to /groups/<group> only if <group> is in their auth token.