Firestore Rules break if the document path has a comma - firebase

So, I am building a multi tenant application which involves multitude of collections and user invitation. Since I don't know which user ID the signup is going to use and I want the companies to start making changes and updates to their user data I am pre setting the user ID and I use it in several paths as:
/companies/{company}/customers/{userId}
/users/{userId}
Now, this userId is pretty much the email with the dot replaced by a comma. The dot is a character not allowed in firebase but the comma is, and viceversa with emails so it makes sense and there is no problem there.
The issue is when I need to make some rules. For example I have this in the configuration since I want admins to access every company:
function cleanEmail(){
return request.auth.token.email.split('.').join(',') //Here I also tried %2C
}
function isSuperadmin() {
return exists(/databases/$(database)/documents/admins/$(cleanEmail()))
}
match /companies/{company} {
allow write: if isSuperadmin();
allow read: if isSuperadmin() || belongsToCompany(company)
}
The simulator was breaking but I could not understand why, so I even sent a bug report. Eventually I tried making the path hard coded and then I found the problem:
function isSuperadmin() {
return exists(/databases/$(database)/documents/admins/test,account#gmail,com) //Same issue with get()
}
I tried with exists() because I thought it could be a bug with get() but the issue remains. I believe this should be a bug since that is a valid Firestore path and I have seen some people already using this "clean email" strategy.
Funny thing is that this was not happening during first testing and I realized that this bug only happens if the comma is BEFORE the #. If you remove the one before that, and leave the second one, seems to work:
I might add a new step to the clean email that turns that into base64 and that might work tho. If someone has a solution great.

I do something similar that looks like this in outline:
/invitation
/fb-generated-doc-name
email: somecustomer#gmail.com
trip: db-ref-to-trip
/usertrips
/doc-name-is-UID
trip: db-ref-to-trip
/trips
/fb-generated-doc-name
{ ... describes the trip ... }
The outline for onCreate auth'd user is:
exports.authDidCreateUser = functions.auth.user().onCreate((authUser, context) => {
/*
find invitation where email == authUser.email
let docref = collection('usertrips').doc(authUser.uid)
docref.set({ trip: invitation.trip })
*/
});

Related

Firestore and Rules | Allowing/disallowing read access to post under a public/private user?

Situation
I have the following Firestore setup
/posts/{id}
/posts/{id}/comments/{id}
/users/{id}/followers/{userId}
A user profile can either be public or private. All users can see posts by public users, but only users who follow private users can see said post, ie. they are in the owner's followers collection.
Current Solution
The post doc looks like this:
owner_account_visibility: public || private
ownerId: uid
The comment doc looks the same:
owner_account_visibility: public || private
ownerId: uid
My rules look like this
match /events/{eventId} {
allow read: isValid();
match /eventComments/{commentId} {
allow read: isValid();
}
}
function isValid(){
return (resource.data.owner_account_visibility == "public" || exists(/users/$(resource.data.ownerId)/followers/request.auth.uid)))
}
Problem
I see problems/questions with this solution:
Problem: A user may create many posts, which in turn may have lots of comments. This means that if a user updates their account visibility, a cloud function has to update possibly thousands of post and comment documents
Problem: A user may load many private posts and comments, and for each one of those is a database read, which can get very expensive as the user scrolls their feed
Question: In the isValid() function, there are two conditions seperated by an OR sign (||). Does this mean that if the first condition returns true (resource.data.owner_account_visibility == "public") then the function will not check the second condition (exists(/users/$(resource.data.ownerId)/followers/request.auth.uid)), saving me a database read? If this isn't the case, then I will waste a loooot of reads when a user loads tons of comments from a post even though it is public...
Does anyone have a proposed solution to this problem? Any help would be appreciated :)
I solved this myself. In short, instead of letting a user set their accounts' visibility, I let them set each post's visibility. This is simply because that is the functionality I want in my app. Now, I can simply use resource.data.post_visibility == "public", avoiding the issue of having to update every post if a user changes their account's visibility. If the first condition is false, I do as I did in my current solution in the question (exists(/users/$(resource.data.ownerId)/followers/request.auth.uid)). Also, comments and replies to a post are opened to all authenticated users even though the post is set to private, since comments aren't necessarily the post owner's own content/sensible information

Firestore realtime database rules : how to use .matches with auth.token.email?

I am trying to check that a new Post received, contains the string corresponding to a specific email
(contained in auth.token.email, let us say toto#tata.com. We used google-sign and that user is authenticated)
The regular expression below is mainly searching for the string "auth.token.email" which is not what we need.
It should search for toto#tata.com
{
"rules":
{
"Posts":
{
".validate" : "newData.val().matches(/auth.token.email/)"
}
}
}
The syntax above must miss something, but I could not find it on internet so far
thanks
Additional note:
I tried also this rule below, but it does not work too (return FALSE)
".validate" : "newData.val().contains(auth.token.email)"
it seems this is not feasible (I do not need to implement it anymore) as not many (no one) would want to implement that model

How do you debug Firestore security rules?

I'm crying myself to sleep on this one.
My getAfter is returning an object that only has 1 field, as every other field type is incorrect. Which I have no idea how to check without any debugging tools (I can't see the data, so its all guess and check).
Here is a watered down version of my rules for users.
match /users/{userId} {
function isValidUser(user) {
return user.id is string &&
(user.address is string || user.address == null) &&
(user.dateOfBirth is number || user.dateOfBirth == null) &&
user.email is string &&
user.name is string &&
(user.phoneNumber is string || user.phoneNumber == null);
}
function isValidWrite(userId, user) {
return signedIn() &&
writeHasMatchingId(userId, user) &&
isValidUser(user);
}
allow read: if signedIn();
allow create: if signedInAndWriteHasMatchingId(userId) &&
userHasId(userId) &&
isValidUser(request.resource.data); // Tested
allow update: if isValidWrite(
userId,
getAfter(/databases/$(database)/documents/users/$(userId))
);
}
and this is the transaction I am trying to run.
const user1Ref = this.userCollection.doc(user1Id);
const user2Ref = this.userCollection.doc(user2Id);
const batchWrite = this.store.batch();
batchWrite.update(user1Ref, {
"details.friend": user2Id,
});
batchWrite.update(user2Ref, {
"details.wishlist": true,
});
batchWrite.commit();
If I comment out the isValidUser(user) line, the operation succeeds. If I leave any line uncommented out inside the function isValidUser(user) except user.id is string, it fails.
Why would the getAfter document only have the id field and no others when they are listed in the Firebase console? Is there a way to output or debug the value of getAfter so I can see what it even is?
I'm answering based on just one line of your question:
Is there a way to output or debug the value of getAfter so I can see what it even is?
There kind of is - at least in 2020.
When one runs something in the Rules Playground (Rules Simulator, see bottom left), the steps taken in the rule evaluation are shown like this:
This list sometimes gives indications that help figure out what the rules evaluator is doing. It's a bit tedious that one needs to 'click' the steps open, individually, instead of seeing true/false just by glancing. But it's better than nothing.
Note: I presume this feature is under development by Firebase. It sometimes seems to give wrong information - or I have failed to read it correctly. But it may help, and looks like a good place for providing such information to the developers. We really would like to see: with the current data, the built query document, and the rules, how does Firebase see it and why does the rule evaluate to true or false?
Another approach, not mentioned here yet and likely not available at the time the question was raised, is wrapping your rules with debug().
Why this is cool?
Allows to see the values suspected of not being right; I still use the same comment-out-narrow-down method that #ColdLogic nicely described in one of their comments
Why this is not enough?
There is no tagging about which value was output; just eg. int_value: 0. Debug would benefit from eg. printing the first 10 letters of the equation it's evaluating, in the output.
Security Rules rejection reasons are still awfully short, as false for 'update' # L44.
the line number always points to the main expression being evaluated. Never to a function called, or a subexpression with && that really causes the fail.
Firebase could fix this (not change the output syntax; just give a more detailed line number). That would eliminate the need to comment-out-and-narrow-down.
The output goes to firestore-debug.log (fairly hidden), so one needs to open yet another terminal and keep an eye on it.
Debugging Security Rules is unnecessarily difficult - and I'm afraid it means people don't use their full potential. We should change this.

Firestore Rule function to check role OR company

I need to implement role based authorization in my app before we launch for production. I have a function that checks whether the user is accessing data from only their company. I need to also make sure that a user with the role "SuperUser", "Tester", or "Manager" can also access the data even though they aren't part of the company. I've looked at other role based implementations but they don't seem to address this scenario of having to check either the company or the role.
I have the rule that has the OR statement here:
allow read: if exists(/databases/$(database)/documents/companies/$(company)/users/$(request.auth.uid))
|| hasRole(['SuperUser', 'Manager', 'Tester']);
And I have this as the function for hasRoles:
function hasRole(roles) {
return get("/databases/$(database)/documents/companies/Seva Development/users/$(request.auth.uid)").data.roles.hasAny(roles);
}
When I add this function the user is denied access, even when the left side of the OR statement is true. I can only assume something is wrong with the function, but I've been unable to find where from firebase documentation.
The document I'm trying to access looks like this:
How can I modify this function to allow me to check if the user is part of our organization and has one of the specified roles?
This works if you have the role but aren't part of the company:
allow read: if isUserCompany(company)
|| hasRole(['SuperUser', 'Manager', 'Tester']);
But it will fail if you are part of the company and don't have the role. What I can't figure out, is that this works if you are part of the company but don't have the role:
allow read: if isUserCompany(company)
So the left side works, the right side works. But they don't work together.
It also works if I write:
allow read: if isUserCompany(company) || false;
Which makes me assume the problem must be with the hasRole function if the role doesn't exist. I tried writing a function to check for the role first, but it still failed.
The problem is that you're passing a string to get() instead of a path. Note the reference documentation for get() says the argument is a Path. According to the docs for Path:
Paths can be created in two ways. The first is in the "raw" form
beginning with a forward slash /:
/path/to/resource
The second is by converting from a string using the path() function:
path("path/to/resource")
If you want to compose a string to become a path (because you have a space in there somewhere), you'll have to use the path() function. Or, you can use the bind() function to insert the values that have spaces in them. Using path():
function hasRole(roles) {
return get(path("databases/" + database + "/documents/companies/Seva Development/users/" + request.auth.uid)).data.roles.hasAny(roles);
}
Note that the leading slash is missing from the string form of the path, and all variables need to be added by concatenation.
Or you can just remove the space from "Seva Development" and just use the raw form of the path.

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