Security Rules: Storing unique usernames in one document - firebase

On my app I am trying to make it so that users have to have a unique username.
My current method is to have a Social Collection with one document called Usernames. That document will store the users userID as the key for the field and then their username for the value.
I am struggling to write the correct security rules for this. I would like it so that:
All signed-in users can get this document
Users can only update their own data in the document, formatted as [theirUserId: theirUsername]
There can be no duplicate usernames, e.g.
userIdA: "foo"
userIdB: "foo"
At the moment the only point that I can't get to work is checking to see whether a username is already taken.
Another solution I have thought of is to reverse the fields (username: userID). But I can't figure out a way how to write the security rules for this method either.
Current Rules
// Usernames
match /Social/Usernames {
allow get: if request.auth.uid != null;
allow update: if isUserNameAvailable();
}
// Functions
function isUserNameAvailable() {
// This line works
return (request.writeFields.hasOnly([request.auth.uid]))
// This one doesn't
&& !(resource.data.values().hasAny([request.writeFields[request.auth.uid]]));
}
Firestore Data Structure
Any help is greatly appreciated, many thanks!

Related

Is it possible to prevent send custom id by rules in firebase

Is there any way to prevent user send custom id for newly created document?
For example
This operation create's new document with id "8pqLuAc6BXCVRF7SB6xT"
db.collection("users").add({foo : 1})
And in my application i always want to have id generate by firebase sdk not by user
because as we know user can hack app and trigger operation like this
db.collection("users").doc('CUSTOM_ID').set({foo : 1})
After this operation id will be "CUSTOM_ID" instead generated by SDK like '8pqLuAc6BXCVRF7SB6xT'
So question is how to write rule to ensure me that id is generated from sdk
Update following your clarification:
It is not possible with Security Rules to avoid a user specifying the ID of a document when it is created.
Even if the code in your front end only uses the add() method, one can write his own code and use the doc() method to write to your DB (as soon as he gets the Firebase configuration object, which is easy).
One possibility would be to write to Firestore via a Cloud Function, but this has some drawbacks see this article.
Original answer:
It is not crystal clear what you mean by a "custom id" but I understand that you probably want a given user to be able to create a document in the users collection only if this document ID is corresponding to his user uid (from the Auth service)
The following Security Rule should the trick:
service cloud.firestore {
match /databases/{database}/documents {
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow create: if request.auth != null && request.auth.uid == userId;
// ...
}
}
}

Firestore Rules with multi-tenancy?

The Firebase Rules docs suggest building conditions comparing the authenticated user's token (i.e., request.auth) with the target Firestore document(s). Something like:
match /posts/{postId} {
allow read, write: if (request.auth.uid != null) &&
(resource.data.tenantId == request.auth.token.tenantId);
}
However, tenantId doesn't appear to be available in Firebase Rules like other related auth fields (e.g., uid, email, email_verified, etc.).
One option appears to be to add tenantId separately as a custom claim using the firebase-admin SDK. But that would create duplicate info on the user object:
{
uid: 'nzjNp3QIfSR6uWy',
emailVerified: true,
displayName: 'pickleR'
...
tenantId: 'wubalubadubdub',
customClaims: { tenantId: 'wubalubadubdub' },
}
An alternative option appears to be to create a tenants collection in Firestore. However, that approach seems to introduce needless complexity and inflate the # of required Firestore queries.
Are there alternatives for accessing the tenantId in Firestore Rules and/or alternative best practices for using Firestore with multi-tenancy?
Having gone down the custom claim route, I've then found the tenant Id is already stored in a nested object as 'request.auth.token.firebase.tenant'
In your example the rule would be:
match /posts/{postId} {
allow read, write: if (request.auth.uid != null) &&
(resource.data.tenantId == request.auth.token.firebase.tenant);
}
The two options you describe are the idiomatic ones:
Pass the information into the rules as part of the ID token as a custom claim.
Look up the information in the database from the rules by the request.auth.uid.
Neither of these is always better than the other: custom claims are more convenient and readable, while using the database is usually faster. It's common to use the database lookup for more volatile information, and claims for information that is "once".
Since this is for a tenant ID, which is unlikely to change quickly, I'd probably go for a custom claim.

Firestore auth rule to test reference value in list

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.

Do I need to do a get for every field of a document I want to access

I see that to get a field of a document in security rules one must use get. The example below shows getting the 'admin' field of some document in the users collection. If I wanted to get another field, would I have to do another get request or can I just do one get request and get all the fields I need in the document.
Here is the example I'm referring to in the documentation.
https://firebase.google.com/docs/firestore/security/rules-conditions
service cloud.firestore {
match /databases/{database}/documents {
match /cities/{city} {
// Make sure a 'users' document exists for the requesting user before
// allowing any writes to the 'cities' collection
allow create: if exists(/databases/$(database)/documents/users/$(request.auth.uid))
// Allow the user to delete cities if their user document has the
// 'admin' field set to 'true'
allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
}
}
}
Yes, you would have to write another get(). There are no variables in Firestore security rules, so you can't store the contents of a get() in order to use its data multiple times.
Multiple gets accessing the same document might not incur multiple read charges. The documentation states:
Some document access calls may be cached, and cached calls do not count towards the limits.

Firebase JSON Security and Arrays

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.

Resources