Partial write access to document - firebase

Is it possible to set the security rules so that only some field could be updated based on the user role?
Consider a user attempts to edit the following document to edit:
/tasks/task1
{
"id":"task1",
"description":"anyone can edit this",
"sensitive_info":"only editor can edit this",
"very_sensitive_info":"only admin can edit this",
}
}
And here is the users collection with roles
/users/user1
{
"role":"admin"
}
/users/user2
{
"role":"editor"
}
/users/user3
{
"role":"anyone"
}
match /tasks/{userId} {
allow read: if true;
allow create: if true;
allow update: if <CONDITION HERE>; // <-WHAT GOES HERE?
}
How to allow the field "sensitive_info" to only be editable by user2 and user1 but not user3?

Write rules either allow access to an entire document, or they deny it. So at first glance that may seem like you can't restrict what a user can modify.
But you can actually control what a user can write by comparing the document before and after the write operation. For example, I often have a check like this in my rules:
allow write: if request.resource.data.creator == resource.data.creator
This allows the write operation if the creator field is unmodified.
In fact, this is so common that I have a helper function for it:
function isUnmodified(request, resource, key) {
return request.resource.data[key] == resource.data[key]
}
allow write: isUnmodified(request, resource, 'creator');
Note that a rule will fail once you try to access a non-existing field, so using isUnmodified often goes hand in hand with:
function isNotExisting(request, resource, key) {
return !(key in request.resource.data) && (!exists(resource) || !(key in resource.data));
}
allow write: isNotExisting(request, resource, 'creator') || isUnmodified(request, resource, 'creator');
If you define the helper functions in the right scope, you can do without having to pass request and resource and shorten the whole thing to:
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));
}
allow write: isNotExisting('creator') || isUnmodified('creator');

Related

Why my rule in firebase database is not working?

I'm trying to add a rule that automatically merges two users if the user already exist with the same email and just keep one of them with new user newest data.
match /users/{userId} {
allow create: if request.resource.data.email != null;
allow update: if request.resource.data.email != null && request.auth.uid == userId;
function isDuplicateEmail() {
return get(/databases/$(database)/documents/users/$(request.resource.data.email)).exists;
}
function mergeUsers(userId) {
// Get the data of the new user
let newUser = get(/databases/$(database)/documents/users/$(userId)).data;
// Get the data of the existing user
let existingUser = get(/databases/$(database)/documents/users/$(newUser.email)).data;
// Merge the data from the two users
let mergedData = {...existingUser, ...newUser};
// Update the data of the existing user
return update(/databases/$(database)/documents/users/$(newUser.email), mergedData);
}
allow create: if !isDuplicateEmail()
allow create: if isDuplicateEmail() && mergeUsers(userId);
}
But I'm seeing an error in the rule editor: "Unexpected "}". Line 40:
let mergedData = {...existingUser, ...newUser};
What I'm missing?
Thanks.
The security rules expression language does not support the ... spread operator like JavaScript. In fact, it is not JavaScript at all - it just looks a bit like JS. You might want to read about its syntax in the documentation.
On top of that, there is no function called update. You can't modify data in security rules at all. You can only check to see if the incoming access should be allowed or denied. If you want to modify document data, you will have to write application or backend code for that.
The } is closing the match statement before the allow create statement that uses the mergeUsers() function. Try:
match /users/{userId} {
allow create: if request.resource.data.email != null;
allow update: if request.resource.data.email != null && request.auth.uid == userId;
function isDuplicateEmail() {
return get(/databases/$(database)/documents/users/$(request.resource.data.email)).exists;
}
function mergeUsers(userId) {
// Get the data of the new user
let newUser = get(/databases/$(database)/documents/users/$(userId)).data;
// Get the data of the existing user
let existingUser = get(/databases/$(database)/documents/users/$(newUser.email)).data;
// Merge the data from the two users
let mergedData = {...existingUser, ...newUser};
// Update the data of the existing user
return update(/databases/$(database)/documents/users/$(newUser.email), mergedData);
}
allow create: if !isDuplicateEmail()
allow create: if isDuplicateEmail() && mergeUsers(userId);
}
Also, if you going to use the update function, you also need to include a rule allowing the update to happen.

Firebase firestore security rules sharing data

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.

Cloud Firestore Security Rules - only allow write to specific key in document

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.

Firestore security rules for specific document

I am trying to apply the following situation :
all authenticated users have read and write access to the database except for admin document.
Admin document is accessible only for him for read and write.
My rules:
service cloud.firestore {
match /databases/{database}/documents {
//Functions
function isAuthenticated(){
return request.auth != null;
}
function isAdministrator(){
return request.auth != null && request.auth.token.name == resource.data.oid;
}
//Administrator Identity Check Point
match /admin/identity {
allow read, write: if isAdministrator();
}
//Allow Reads and Writes for All Authenticated Users
match /{document=**}{
allow read, write: if isAuthenticated();
}
}//databases/{database}/documents
}//cloud.firestore
Is there any way i can achieve this, actually when testing these rules, the tests succeed because only isAuthenticated() is being called because of the tag /{document=**}. I also tried /{document!=/admin/identity} but it does not work.
How can I write a security rule that follow this model ?
Maybe on your default user rule you could check if the collection isn't admin, something like this:
//Allow Reads and Writes for All Authenticated Users
match /{collection}/{document=**}{
allow read, write: if (isAuthenticated() && collection != "admin") || isAdministrator();
}
Since June 17, Firebase has provided new improvements to Firestore Security Rules.
Firebase blog - 2020/06 - New Firestore Security Rules features
New Map methods
We'll use Map.get() to get the "roleToEdit" field. If the document doesn't have the field, it will default to the "admin" role. Then we'll compare that to the role that's on the user's custom claims:
allow update, delete: if resource.data.get("roleToEdit", "admin") == request.auth.token.role;
Local variables
Say you're commonly checking that a user meets the same three conditions before granting access: that they're an owner of the product or an admin user.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function privilegedAccess(uid, product) {
let adminDatabasePath = /databases/$(database)/documents/admins/$(uid);
let userDatabasePath = /databases/$(database)/documents/users/$(uid);
let ownerDatabasePath = /databases/$(database)/documents/$(product)/owner/$(uid);
let isOwnerOrAdmin = exists(adminDatabasePath) || exists(ownerDatabasePath);
let meetsChallenge = get(userDatabasePath).data.get("passChallenge", false) == true;
let meetsKarmaThreshold = get(userDatabasePath).get("karma", 1) > 5;
return isOwnerOrAdmin && meetsChallenge && meetsKarmaThreshold;
}
match /products/{product} {
allow read: if true;
allow write: if privilegedAccess();
}
match /categories/{category} {
allow read: if true;
allow write: if privilegedAccess();
}
match /brands/{brand} {
allow read, write: if privilegedAccess();
}
}
}
The same conditions grant access to write to documents in the three different collections.
Ternary operator
This is the first time we've introduced an if/else control flow, and we hope it will make rules smoother and more powerful.
Here's an example of using a ternary operator to specify complex conditions for a write.
A user can update a document in two cases: first, if they're an admin user, they need to either set the field overrideReason or approvedBy. Second, if they're not an admin user, then the update must include all the required fields:
allow update: if isAdminUser(request.auth.uid) ?
request.resource.data.keys().toSet().hasAny(["overrideReason", "approvedBy"]) :
request.resource.data.keys().toSet().hasAll(["all", "the", "required", "fields"])
It was possible to express this before the ternary, but this is a much more concise expression. ;)

Create group access to firebase storage without using custom authorization

Is there a way to control uploads to a path in Firebase Storage by group?
For instance have an admin group that can upload anywhere or a team that can only upload to a certain path.
After searching around a bit, I didn't find a ready answer, so I'll post what I have so far. It would be nice to know if there are other (better) ways of doing this.
Since I'm trying NOT to use another server, custom authentication tokens are out. However, the request.auth.uid is available to the storage rules. The uid property matches one of the users defined in the Auth area. You'll need to create a function in the storage rules that checks if request.auth.uid is in a group you define.
Firebase storage rules have a unique syntax. It sorta looks like javascript, but it's not. You can define functions, but you can't declare a var within them. Furthermore there is a subset of javascript-like methods available.
For instance, I first unsuccessfully tried the following:
function isAdmin() {
return ["value","list"].indexOf(request.auth.uid) > -1;
}
service firebase.storage {...}
Either the rules editor threw errors when I tried to define a var OR it always returned "unauthorized" when I used .indexOf.
The following ended up working for me.
function isAdmin() {
return request.auth.uid in {
"yaddayadddayaddUserIDKey":"User Name1"
};
}
function isSomeOtherGroup() {
return request.auth.uid in {
"yaddayaddaSomeOtherUID":"User Name2",
"YaddBlahBlahUID":"User Name3"
};
}
service firebase.storage {
match /b/<your bucket here>/o {
match /{allPaths=**} {
allow read, write: if isAdmin();
}
match /path/{allPaths=**} {
allow read, write: if isSomeOtherGroup() || isAdmin();
}
}
}
I don't have enough reputation to comment on #ecalvo's answer. So I am adding an answer. The function isAdmin() can be provided a list as following:
function isAdmin() {
return request.auth !=null && request.auth.uid in [
"uidAdmin1",
"uidAdmin2",
"uidOfOtherAdminsAsCommaSeparatedStrings"
];
}
Rest of the implementation can be borrowed from the answer of #ecalvo. I feel now it is more clear. In the answer of #ecalvo, I was confused why should I give username when I have to compare only uid.

Resources