Security rules for nested subdocuments with firebase - firebase

I am trying to create a text editor app using firebase that allows users to create documents, but they can also nest a new document inside an existing document (when editing a document, they would be able to click on a button that would add a new document in the database and insert a link in the editor that redirects towards this page):
A user would be able to share a document with other users, but then they should have access to all the nested documents as well. So now I am wondering how to write the security rules to do that.
I think the best way to structure the realtime database would be to store all documents at the root, and then add a parentDocument or path property to each document:
{
"documents": {
"doc-1": {
"title":"Lorem ipsum",
"content": "...",
"path":"/",
"owner":"user-1",
"canAccess":{
"user-3":true
}
},
"doc-2": {
"title":"Dolor sit",
"content": "...",
"path":"/doc-1/",
"owner": "user-1"
"canAccess": {
"user-2":true
}
}
},
"users": {
"user-1": { ... },
"user-2": { ... },
"user-3": { ... }
}
}
↑ In the example below,
doc-2 is nested inside doc-1
user-1 can access both doc-1 and doc-2
user-2 can access doc-2 only
user-3 can access both doc-1 and doc-2
But now I do not know how to manage the security rules, because to check if a user has access to a specific document, I guess it would need to go through each of its parents (using its path or parentDocument prop). Perhaps I could also specify the canAccess prop on each document, but then I would have to update each nested document whenever a parent's canAccess prop is updated...
Any help would be greatly appreciated

In the Firebase Realtime Database model permission automatically cascades downwards. This means that once you grant a user (read or write) permission on a specific path, they can also access all data under that path. You can never revoke the permission at a lower level anymore.
So your requirement actually matches really nicely with this model, and I'd recommend just trying to implement it and reporting back if you run into problems.

Related

How can I read only top level data in firebase realtime database, and not all the lower level data?

So the simple answer here would be to use Firestore database, since it offers functionality to read only top level documents, but it is important for my app to use realtime database, I want to read the top level data, and only read the lower level data when user requests it specifically. I read the Realtime Database documentation and it says
So, it's not allowed 'by default'. I want to know if it is allowed any other way?
Read operations in the Firebase Realtime Database SDKs always return all data under all paths that are returned. There is no method in the SDKs that returns only the keys under a node, and not the further data under it.
I'd recommend changing your data structure to have an "index" data structure with just the keys, while also keeping your current structure. For example:
Users: {
"userId1": {
name: "...",
address: "...",
createdAt: 1932831239123,
lastSeenAt: 1932831239127,
},
"userId2": {
name: "...",
address: "...",
createdAt: 1632831239123,
lastSeenAt: 1732831239127,
}
},
UserKeys: {
"userId1": true,
"userId2": true
}
The true values in the UserKeys nodes are needed because you can't create a path without a value, but have no specific meaning.
Now you can can load just the keys, or the full profiles as needed.
Alternatively you can use the REST API and pass the shallow=true parameter.

Setting up 'Trigger Email' Firebase Extension

I learned about firebase and cloud functions recently and have been able to develop simple applications with them.
I now want to expand my knowledge and really struggling with Trigger Email Extension.
On a specific event on my firebase, I want to fire an email to the user in a custom format, but I am unable to even activate the extension for now.
Can someone please explain with example please about these fields marked in the picture?
I had this question too, but got it resolved. Here's your answer:
"Email documents collection" is the collection that will be read to trigger the emails. I recommend leaving named "mail" unless you already have a collection named mail.
"Users collection (Optional)" refers to a collection (if any) that you want to use in tandem with a user auth system. I haven't had this specific use case yet, but I imagine once you understand how Trigger Email operates, it should be somewhat self-explanatory.
"Templates collection (Optional)" is helpful for templates in which you can use handlebar.js is automatically input specific information per user. (eg. <p>Hello, {{first_name}}</p> etc.) Similar to the previously mentioned collections, you can name it whatever you want.
How to create a template (I have yet to actually implement this, so take this with a grain of salt):
In your templates collection, you want to name each document with a memorable ID. Firebase gives the example:
{
subject: "#{{username}} is now following you!",
html: "Just writing to let you know that <code>#{{username}}</code> ({{name}}) is now following you.",
attachments: [
{
filename: "{{username}}.jpg",
path: "{{imagePath}}"
}
]
}
...specifying a good ID would be following. As you can see, the documents should be structured just like any other email you would send out.
Here is an example of using the above template in javascript:
firestore()
.collection("mail")
.add({
toUids: ["abc123"], // This relates to the Users Collection
template: {
name: "following", // Specify the template
// Specify the information for the Handlebars
// which can also be pulled from your users (toUids)
// if you have the data stored in a user collection.
// Of course that gets more into the world of a user auth system.
data: {
username: "ada",
name: "Ada Lovelace",
imagePath: "https://path-to-file/image-name.jpg"
},
},
})
I hope this helps. Let me know if you have an issues getting this set up.

Firebase Firestore security rules - read all nested docs based on resource (but it's null)

I think I'm doing something wrong but I can't figure which part... Thanks for all the clarification I can get.
So I have a collection named Bases that looks like this:
{
"id1": {
"name": "My base 1",
"roles": {
"idUser_123": {
"name": "John"
}
}
},
"id2": {
"name": "My base 2",
"roles": {
"idUser_456": {
"name": "Jane"
}
}
}
}
idUser_123 log in and want to access his collection. So I do:
db.collection('bases').get()
And I use a match rule to make sure John is not reading Jane's bases. And that's where I think I'm wrong cause I'm using rule for filter purpose.
match /bases/{document=**}{
allow read: if request.auth.uid in resource.data.roles;
}
Which failed because resource is null... I tried to do this:
match /bases/{baseId}{
allow read: if request.auth.uid in resource.data.roles;
}
This work in the simulator when requesting specific document but fails when I'm get() without baseId from client - cause I want them all.
So how am I supposed to handle this very basic use case (IMO)?
I can't put all user's baseId in user.token as it'll be over 1000 bytes quite fast.
I can make an other collection Roles to create a relation between a baseId and my user but that's seems overengineered for a simple use case.
Or I can make the request on a server and filter where("roles", "has", user.uid) ? Defeat the purpose of fetching data on client side very very quickly in my opinion...
Any recommendation on how to address this will be gladly appreciated! Thanks a lot :)
There are two problems here.
Firstly, your query is demanding to receive all of the documents in bases, but your rules do not allow anyone to simply receive all documents. It's important to realize that Firestore security rules are not filters. They do not strip documnts from a query - this actually does not scale. If a rule puts requirements on who can query certain documents based on their contents, the query will have to use a filter that matches what the rule requires.
Secondly, your data isn't structured in a way that the client can perform a filter of only certain documents based on the user's UID. Firestore doesn't have queries that can check for the existence of a map field. It can only check for map values. You might want to restructure your document data so that the client can actually perform a query with a filter to get only the documents for the current user.
If roles was an array of UIDs:
"id1": {
"name": "My base 1",
"roles": [ "idUser_123" ]
},
Then the client could filter on that field:
firebase.firestore()
.collection("bases")
.where("roles", "array-contains", uid)
And you could enforce the use of that filter in rules:
match /bases/{document=**} {
allow read: if request.auth.uid in resource.data.roles;
}
But you might want to do something different.

Correct way to reference images in Firestore document

In my application, my user documents have an avatar image associated with them which is kept in cloud storage. Currently I have a field in the user object that references the download URL of its image. Just wondering if this is the correct/best way to do it.
There isn't really a best way to materialize the link between an avatar image that you store in Cloud Storage and a specific user of your Firebase project.
You can very well do the way you do (having a "field in the user object that references the download URL").
Another approach would be to store the avatar images in a public "folder" under your default bucket using the user UID to name the avatar image (see at the bottom the note on "folders").
Then you you can use a link with the following structure to directly download the image (or include it in a img src HTML tag)
https://firebasestorage.googleapis.com/v0/b/<yourprojectname>.appspot.com/o/users%2F38r174prM9aTx4JAdcm50r3V0Hq2.png?alt=media
where users is the name of the "folder" dedicated to public avatar images
and 38r174prM9aTx4JAdcm50r3V0Hq2.png is the image file name for a specific user (i.e. user UId + png extension).
Note that the / is encoded as %2F (standard URL encoding).
You would then set your Cloud Storage security rules like the following:
service firebase.storage {
match /b/{bucket}/o {
match /privateFiles { //All other files that are not under users
match /{allprivateFiles=**} {
allow read: if false;
allow write: .....
}
}
match /users/{userId} { //Public "folder"
allow read;
}
}
}
Note: Actually Google Cloud Storage does not have true "folders", but by using a "/" delimiter character in the file path it will behave similarly to folders. In particular the Firebase console will display the files organised in folders.

How to use firebase rule to check is user group array and record group array intersect

I have a list of records in firebase which will have a group property with zero or more groups on it. I also have the firebase auth object which will also have zero or more groups on it as well. I would like to set up a .read firebase rule for my records that will check if the two have at lease one group that exists in both lists.
Put another way I have a user that has an array of groups that have been assigned to it. I have some records that also has some list of groups on them that specify what groups the user must have to access them. If the logged in user tries to access the record, I want to make sure that the user has at least one group that the record requires.
On the client I would do something like _.intersect(userGroups, recordGroups).length > 0
I'm not sure how I would do this in a firebase rule expression. It would be cool if it worked something like this.
Record:
{
someData: "test"
groups: ['foo', 'bar']
}
Firebase Auth Object:
{
userName: "Bob",
groups: ['foo', 'bar']
}
Rule Data:
{
"rules": {
"records": {
"$recordId": {
".read": "data.child('groups').intersectsWith(auth.groups)"
}
}
}
}
Thanks.
Update:
I think that if hasChildren() used || instead of && I could put the group names in they key position and check for their existence this way. Something like "data.child('groups').hasChildren(auth.groups, 'or')"
Where Record:
{
someData: "test"
groups: {
'foo': '',
'bar': ''
}
}
Update2:
Based off Kato's comment & link I realize that even if hasChildren could do OR it still wouldn't work quite right. Requests for individual records would work but requests for all records would error if the current user didn't have access to every record.
It is still not clear how you would structure data to make this work. If a record could belong to many groups how would that work? This is a very common scenario(basically how linux group permissions work) so I can't be the only one trying to do this. Anyone have any ideas/examples of how to accomplish this in firebase?
At the current moment, I believe it's impossible. There's a limited number of variables, methods, and operators allowed, listed here:
Firebase Security Rules API
Since function definitions are not allowed in the rules, you can't do anything fancy like call array.some(callback) on an array to do the matching yourself.
You have three options that I know of:
1) Copy data so you don't need to do the check. This is what I did in my project: I wanted some user data (names) available to users that shared a network in their network lists. Originally I wanted to check both member's network lists to see if there was at least one match. Eventually I realized it would be easier to just save each user's name as part of the network data so there wouldn't have to be a user look up requiring this odd permissions. I don't know enough about your data to suggest what you need to copy.
2) Use strings instead of arrays. You can turn one string into a regex (or just save it in regex format) and use it to search the other string for a match.Firebase DB Regex Docs
3) If you have enough weird cases like this, actually run a server that validates the request in a custom fashion. In the DB, just allow permissions to your server. You could use Firebase Cloud Functions or roll your own server that uses the Firebase Admin SDK
Nowadays, there's another possibility: to use Firestore to deliver your content, possibly in sync with the Realtime Database.
In Firestore, you can create rules like this:
function hasAccessTo(permissionList) {
return get(/databases/$(database)/documents/permissions/$(request.auth.uid))
.data.userPermissions.keys().hasAny(permissionList)
}
match /content/{itemId} {
allow read: if hasAccessTo(resource.data.permissions.keys());
}
The following data would allow a read of $CONTENTID by $UID, because the user permissions set intersects with the possible permissions required to access the content (with access123). My scenario is that a piece of content can be unlocked by multiple In-App Purchases.
{
permissions: {
$UID: { userPermissions: { access123:true, access456:true } },
...
},
content: {
$CONTENTID: { ..., permissions: { access123, access789 } },
...
}
}
For a progressive migration, you can keep data in sync between the Realtime Database and Firestore by using a one-way Cloud Function like this for example:
exports.fsyncContent = functions.database
.ref("/content/{itemId}")
.onWrite((snapshot, context) => {
const item = snapshot.after.val();
return admin
.firestore()
.collection("content")
.doc(context.params.itemId)
.set(item);
});

Resources