I have the following collections
/companies
/users
Within the user document I have a companyUuid to reference its company membership and an object called permissions that has two attributes:
{
admin : true|false
superAdmin: true | false
}
My app logic is as follows:
Superadmins can do anything, including granting superadmin or admin permission to other users
Admins can only grant admin access to other users with the same companyUuid
Non-admins cannot grant any admin permissions
Admins should not remove admin permissions on themselves.
My concern is, given that I store the permissions within the user document, how do I effectively prevent non-Admins from writing to their permissions object while also allowing them to edit the fields within the user document?
I have some code here, it looks way too complicated I think, there has to be a simpler way. I would greatly appreciate if you point me in the right direction:
match /users/{userUuid} {
allow read: if request.auth != null && belongsToSameCompany()
function areAdminPermissionsIntact(){
return request.resource.permissions.admin == resource.permissions.admin
}
function areSuperAdminPermissionsIntact(){
return request.resource.permissions.superAdmin == resource.permissions.superAdmin
}
function isNotTheSameUser(){
return resource.data.uuid != request.auth.uid
}
function belongsToSameCompany(){
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.companyUuid == resource.data.companyUuid
}
allow write: if request.auth != null
&&
(areAdminPermissionsIntact() || areAdminPermissionsIntact() == false && resource.data.permissions.admin == true && isNotTheSameUser() && belongsToSameCompany())
&&
(areSuperAdminPermissionsIntact() || areSuperAdminPermissionsIntact() == false && resource.data.permissions.superAdmin == true && isNotTheSameUser() && belongsToSameCompany())
}
It doesn't really get any simpler than this. You have to check that the fields that should not change are not changing, and that's exactly what you're doing here.
You could express this a bit differently using the MapDiff API to check the list of fields that are unchanged between the request and resource, but honestly, it's not going to make this any less complicated.
Related
I am building an admin UI for an app (in firebase/firestore) that allows supervisor admins to access their team's individual user accounts and view their dashboards / data directly. Since this is essentially identical to creating an admin for different user groups, I imagine that it can be accomplished with custom claims. The problem is that I am having trouble implementing this without weakening the firestore rules that protect our user account data or storing login credentials within the 'admin account' firestore profile doc. Could someone help me understand what I am missing here? Thank you!
This is a snippet from the current firestore rules we have in place.
match /teams/{team}{
allow create
allow read: if (request.auth.token.role == 'member' || request.auth.token.role == 'supervisor') && request.auth.token.email_verified == true
allow update, delete: if (request.auth.token.role == 'member' || request.auth.token.role == 'supervisor') && request.auth.token.email_verified == true
}
match /supervisors/{supervisor}
{
allow create
allow read: if (request.auth.token.role == 'supervisor' || request.auth.token.role == 'member') && request.auth.token.email_verified == true
allow update, delete: if request.auth.token.role == 'supervisor' && request.auth.uid == supervisor && request.auth.token.email_verified == true
}
Well I would suggest to not to create two DB and go with single database "teams" in which create a role for supervisor assigning the particular team name aswell.
Create a security rule to check if the person is supervisor of that particular team(by checking team name parameter) or not.
Edit:
you can add iSSupervisor and SupervisorOf (the name of team) as a parameter actually and can add security rule like
function notAllowedChange(field) {
//Parameters not allowed to be changed
return !(field in request.resource.data) || (field in resource.data && resource.data[field] == request.resource.data[field]);
}
match /teams/{userId} {
allow write: if userId == request.auth.uid
&& notAllowedChange('iSSupervisor') && notAllowedChange('SupervisorOf');
allow write: if get(/databases/$(database)/documents/teams/$(request.auth.uid)).data.iSSupervisor == true && get(/databases/$(database)/documents/teams/$(request.auth.uid)).data.SupervisorOf == resource.data.teamName;
allow read: if true;
}
Hope this helped you!
Sorry for including the code later.
I have this data structure in firestore where I'm trying to link user to profile then to event. A profile can be shared by multiple users and should be able to access events for that profile.
user
- id
- email
- name
- profilePicUrl
profile
- id
- name
- dateOfBirth
- owners: [ "user1","user2" ]
- etc.
event
- id
- profileId
- name
- startDate
- endDate
I currently have:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{id} {
allow read, write: if request.auth.uid == id;
}
match /profiles/{id} {
allow read, write: if ("owners" in resource.data && resource.data.owners != null && request.auth.uid in resource.data.owners);
}
match /events/{id} {
allow read, write: if hasAccess(userId, resource) == true;
}
}
}
function hasAccess(userId, resource) {
// Not sure what to put here but basically need
// to get profiles where user is owner
// and get events for these profiles
}
But not sure what to put in the hasAccess function. Appreciate if someone can guide me.
UPDATE 2019/10/11
Somehow I got this to work by using the following rule:
match /events/{id} {
allow read, write: if (exists(/databases/$(database)/documents/profiles/$(resource.data.profileId)) &&
"owners" in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data &&
get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners != null &&
request.auth.uid in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners);
}
UPDATE 2019/10/14
I have some permission issues with the write so I had to revise it as shown below:
match /events/{id} {
allow read: if ( exists(/databases/$(database)/documents/profiles/$(resource.data.profileId))
&& "owners" in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data
&& get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners != null
&& request.auth.uid in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners);
allow write: if ( request.auth.uid in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners );
}
What you're trying to do is actually not possible with security rules given the existing structure of your data. This is due to the fact that security rules can't perform queries against collections. The only thing you can do is get() a specific document using its known path in order to read its fields, which isn't going to help you link up documents where you can't build that path.
What you can do instead is duplicate the data required for the rule into each document that needs to be protected. This means each event document needs to have a copy of each list of owners as a field. Yes, that is going to be more hassle to keep all the events up to date if the list of owners for an event has to change.
I'm currently writing some rules for my app with a Firestore database.
Currently everyone can read data and authenticated users can write.
match /quizzes/{quizId} {
allow read;
allow write: if request.auth != null;
}
That works fine, but I also want unauthenticated users to write only to a specific key in a document.
Example content of a document:
{
title: 'title',
plays: 12,
playedBy: [//Filled with user id's],
...
}
Is there any way that limits unauthenticated users to only have write access to the playedBy array and not the other keys of that document?
Sure thing. But it may become a bit involved if you have a lot of fields.
Let's start with the simplest example. Something like this allows an unauthenticated user to write the playedBy as long as that is the only field in the document:
if request.auth != null || request.resource.data.keys().hasOnly(['playedBy'])
This works if the unauthenticated user is creating a new document, or updating an existing one. But it will stop as soon as the document contains more fields, since request.resource.data contains all fields the document will have after the write succeeds.
So the better alternative is to check that only the playedBy is modified, and that all other fields have the same value as before. The tricky bit there is handling the non-existence of fields, which I typically handle with a few helper functions:
function isUnmodified(key) {
return request.resource.data[key] == resource.data[key]
}
function isNotExisting(key) {
return !(key in request.resource.data) && (!exists(resource) || !(key in resource.data));
}
And then:
if request.auth != null &&
request.resource.data.keys().hasOnly(['title', 'plays', 'playedBy']) &&
isUnmodified('title') &&
isUnmodified('plays')
The exact rule might be a bit off, but I hope this is enough to allow you to complete it yourself.
After the earlier answer (late 2019, I believe), Firebase has brought in Map.diff.
Something like:
match /quizzes/{quizId} {
allow read;
allow write: if request.auth != null ||
request.resource.data.diff(resource.data).affectedKeys() == ["playedBy"].toSet()
}
Tested code where I use it can be seen here.
Using Firebase Firestore I am storing chat room details in documents containing some data like:
roomName
roomAvatar
createDate
isDeleted
My application and my rules are requiring these fields to be set on "create" of the document. However, I would also like to allow the document to be "updated" by authorized users as well.
I already have the checks working to only allow certain users the ability to update the document and verify that they NOT be allowed to modify a field like "createDate". But, I cannot seem to create a rule that enforces an authorized user doesn't remove the "createDate" field, either by passing FiledValue.delete() or using ref.set(objectMissingCreateDate).
Lastly, I don't think it should be required that the client pass in all data fields on an update if they are only trying to update one or two fields.
I have an "update" rule as follows:
allow update: if isAuthenticated() && isMemberOfRoom() &&
(
(!("roomName" in request.resource.data) ||
request.resource.data.roomName == resource.data.roomName ||
hasRoomPermission("UpdateRoom")) &&
(!("roomAvatar" in request.resource.data) ||
request.resource.data.roomAvatar == resource.data.roomAvatar ||
hasRoomPermission("UpdateRoom")) &&
(!("createDate" in request.resource.data) ||
request.resource.data.createDate == resource.data.createDate) &&
(!("isDeleted" in request.resource.data) ||
request.resource.data.isDeleted == resource.data.isDeleted ||
hasRoomPermission("DeleteRoom"))
);
The main issue is protecting the data from an authorized user passing in FieldValue.delete() or using the [Android] docRef.set() while missing required values.
I would even accept the functionality to turn off FieldValue.delete() and destructive "set" operations for non-admin clients.
With the help of Doug and Puff's comments, I realize what I was doing wrong.
First, I was not aware that an update (or a set with merge options) that passed only a few fields, would result in the remaining fields being copied on the request.resource from the database's resource.
Second, a set (without merge) or passing in a FieldValue.delete() would be the only way that the request.resource would be missing an existing document field.
With this new knowledge, I have updated my previous rules to the following:
allow update: if isAuthenticated() && isMemberOfRoom() &&
(
("roomName" in request.resource.data &&
(request.resource.data.roomName == resource.data.roomName ||
hasRoomPermission("UpdateRoom"))) &&
("roomAvatar" in request.resource.data &&
(request.resource.data.roomAvatar == resource.data.roomAvatar ||
hasRoomPermission("UpdateRoom"))) &&
("createDate" in request.resource.data &&
request.resource.data.createDate == resource.data.createDate) &&
("isDeleted" in request.resource.data &&
(request.resource.data.isDeleted == resource.data.isDeleted ||
hasRoomPermission("DeleteRoom")))
);
These new rules now make createDate locked for clients, while allowing the editing of roomName, roomAvatar, and isDeleted for clients who pass the permission test(s). It will also not allow any of these fields to be updated with FieldValue.delete() or calling docRef.set(object) where the object is missing one of these fields.
So I currently have two roles for all users: isAdmin and isReader.
An Admin is allowed to read and write data and an Reader is allowed to read data.
When someone creates an account he has no rights. Not even isReader. Only an Admin can change rules.
This is how I planned to do it:
Once someone creates an account I create an Document in the Users Collection like this:
uid: user.uid,
email: user.email,
role: {
isAdmin: false,
isReader: false,
}
On each login I update 'email' and uid but keep role untouched. To secure this behaviour I have these rules:
match /Users/{userId} {
allow read: if isOwner(userId) || isAdmin();
allow create: if request.resource.data.hasAll(['uid', 'email', 'role']) && request.resource.data.role.isAdmin == false && request.resource.data.role.isReader == false;
allow update: if resource.data.role == null || isAdmin();
}
function isAdmin() {
return getUserData().role.isAdmin == true;
}
I think I have 2 errors:
for some reason the data.hasAll(['uid', 'email', 'role']) does not work. When I remove this part the create rule works as planned.
resource.data.role == null does not work. I intend to check if the data contains any updates for role because I can't allow it is it doesn't come from an Admin. But for some reason it does not work.
Any Ideas what I'm doing wrong? Also is my strategy save or is there a way someone could "hack" himself Reader or Admin rights?
This looks like it may be a good use case for custom auth claims. You can set specific roles on a user within a secured environment, as shown in this codelab. Below is an example of setting a custom claim in your server. You can even use Cloud Functions for this. I recommend you check out the full code of the Codelab so you can see how to ensure not just anyone can request custom claims be added to their user.
admin.auth().setCustomUserClaims(uid, {Admin: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
Then you can check for those roles on the user in your security rules.
service cloud.firestore {
match /databases/{database}/documents {
match /Users/{userId} {
allow read: if request.auth.token.Owner == true || request.auth.token.Admin == true;
allow create: request.auth.uid == userId &&
request.resource.data.uid == request.auth.uid &&
request.resource.data.email != null;
allow update: request.auth.uid == userId || request.auth.token.Admin == true;
}
}
}
Notice all the rules about "role" have been removed because they're no longer needed. Let me know if you have questions about implementation because I'm trying to make some more content around this since it's such a common problem.
request.resource.data.hasAll(['uid', 'email', 'role']) does not work, because request.resource.data is a Map and not a List. You should use keys() to create a List from the Map and ensure certain keys exist.
In regards to your second issue, you should just check whether there is a write to roles: allow update: if !('roles' in request.writeFields) || isAdmin();. This will ensure that any updates to roles will fail unless a user is an Admin.
About your security question; I see a couple issues. The first is anyone can create unlimited users which also means that any Admin can create unlimited other Admin accounts. To stop this from happening, I would add another section to the allow create that restricts creation to the user:
allow create: if userId == request.resource.data.uid
&& request.auth.uid == request.resource.data.uid
&& request.resource.data.hasAll(['uid', 'email', 'role'])
&& request.resource.data.role.isAdmin == false
&& request.resource.data.role.isReader == false;`
The second is anyone can change their uid and try to impersonate someone else. Obviously this doesn't change the uid associated to their Auth Token, but depending on how you write the rest of your rules, the backend security, or even the frontend display, someone could use that flaw to exploit your code or another user (potentially an Admin). You can ensure no one changes their uid by checking whether it is in the writeFields (you will also need the previous security solution to also ensure they don't impersonate during creation).
allow update: if !('uid' in request.writeFields)
&& (!('roles' in request.writeFields) || isAdmin());
I hope this helps.