How to set firebase security rules in order to allow only update of certain fields in document? - firebase

I would like to know how I can achieve that my user
that has the following fields: uid, friends, notifications, name, username.
Currently my security rules look like this for the user folder
function signedIn() {
return request.auth.uid != null;
}
match /users/{user} {
allow read, update, write: if signedIn();
}
So how can I make a rule for update: "condition",
so that only friends and notifications are updateable,
but not username, uid or name. Any ideas ?

You're looking for map diffs.
For example:
request.resource.data.diff(resource.data).affectedKeys()
.difference("username", "uid", "name"].toSet()).size() === 0;
Or shorter:
!request.resource.data.diff(resource.data).affectedKeys()
.hasAny("username", "uid", "name"];
Also see:
The documentation on map diff operations
The documentation o controlling field access.

Related

Can a user read a collection of users in firestore from frontend?

I am saving the below Data in the user's collection in firebase
{
"uid":"randomid",
"name":"name",
"number":"1234"
}
when I try to check if the user exists the below code works ok
const result = await firestore().collection('users').where('uid', '==', userid).get()
so can an authenticated user read the whole users' collections?
const result = await firestore().collection('users').get()
What security rules I can write to prevent users from reading a collection but only reading their info based on uid?
In security rules you can split the read access to get and list. So if you want the give access to each user to get only his own data you need to use the following rule (I assume each user document in the collection is the uid of this user):
match /users/{user} {
function isUserOwner() {
return request.auth.uid == user
}
allow get: if isUserOwner();
allow list: if false;
}
First you need to set the uid field to the UID of the user who created the document.
To get the current user id See documentation
const uid = user.uid;
To add the currently logged in User id as a field visit stack overflow example link for javascript
After adding UID you can use request.auth and resource.data variables to restrict read and write access for each document to the respective users. Consider a database that contains a collection of story documents. Have a look at below example
{
title: "A Great Story",
content: "Once upon a time...",
author: "some_auth_id",
published: false
}
You can use below security rule to restrict read and write access for each story to its author:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{storyid} {
// Only the authenticated user who authored the document can read or write
allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
}
}
}
Note that the below query will fail for the above rule even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database
// This query will fail
db.collection("stories").get()
The appropriate query for the above rule is
// This query will work
var user = firebase.auth().currentUser;
db.collection("stories").where("author", "==", user.uid).get()
For additional information on the above rules and query see official documentation

Access userId without having it as a field

I'm writing Firestore security rules for my project. I want to allow users to edit information in their own user page, but not in anyone else's. Right now I don't save userId as a field in each user, only as the reference to the user document. I know how to access fields in each user, but not the reference to them. See picture:
match /Users/{document} {
allow update: if request.auth.uid == userId; //how do I reach the userId without having it as a field
}
I do not want to add userId as a field in each user, there must be an easy way of accessing the path.
As mentioned in the Firestore docs you get the document id from the match query.
In your case this would be document from match /Users/{document}. You could also rename this query to match /Users/{userId} to make it work.
Check the second example in the documentation on using authentication information in security rules:
Another common pattern is to make sure users can only read and write their own data:
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 read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
So in your case that'd be if request.auth.uid == document.

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. ;)

firebase - setting specific fields as private

I want to set up certain fields to be private on my user profiles. I have a user documents with name, email, etc but I want to make the gold field read only as I plan to use a cloud function to update this value when a user makes an in app purchase. I've not done in app purchases before so this is the only way I can think of doing it.
I understand I can use wildcard vars in the path when using Firestore security rules, however as far as I'm aware, I can only use wildcard vars in place of the documents and collections.
You are correct that wildards can only be used to identify collections and documents, but not fields. However one option you could have is to create an additional 'private collection' which you could secure with the standard security rules. For example -
users
user1
email
name
gold
user1
goldValue
Then in your security could look something like -
service cloud.firestore {
match /databases/{database}/documents {
match /gold/{userId} {
allow read, update, delete: if request.auth.uid == userId
allow create: if request.auth.uid != null;
}
match /users/{document=**} {
allow read;
allow write: if request.auth.uid != null;
}
}
}

Recursive wildcards in Firestore security rules not working as expected

I have a data structure like this (Collections and Documents rather than JSON of course but you get the idea):
{
users: {
user1:{
name: Alice,
groups: {
groupA:{subbed:true},
groupB:{subbed:true}
}
},
user2:{
name: Bob,
groups: {
groupC:{subbed:true},
groupD:{subbed:true}
}
}
}
}
Basically this is registered users IDs and the group IDs that each user is subscribed to. I wanted to write a security rule allowing access to a users profile and sub-collections only if they are the current auth user and, based on my reading of the docs, I thought that a wildcard would achieve this...
match /users/{user=**}{
allow read,write: if user == request.auth.uid;
}
With this in place I can read the user document fine but I get a permissions error when I try and read the groups sub-collection. I can only make it work by matching the sub-collection explicitly...
match /appUsers/{user}{
allow read,write: if user == request.auth.uid;
match /groups/{group}{
allow read,write: if user == request.auth.uid;
}
}
...so my question is, what is the difference between the two examples and what am I misunderstanding about the recursive wildcards? I thought that the {user=**} part of the first example should grant access to the user document and all its sub-collections, sub-sub-collections etc etc ad infinitum (for the authorised user) and should remove the need to write rules specifically for data stored lower down as I have had to do in the second example.
I've only been messing around with Firestore for a short time so this could be a real dumb question :)
Thanks all
The firebase docs are a bit confusing when it comes to using the recursive while card. What I found in testing was that I needed to set two rules to give a user permission to write to the users document and all sub collections (and their sub documents) which is the most logical setup for managing user data.
You must set two rules.
Give user permission to the /users/{userId} document
Give user permission to all sub collections and their sub documents that begin at the /users/{userId} path.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
match /users/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Rules
Sorry about including the images. I couldn't get SO to format them correctly.
I think the problem is that, while you are indeed using the subcollections wildcard =**, you are then allowing permissions only if user == request.auth.uid, so this is what happens (pseudocode):
(when accessing users/aHt3vGtyggD5fgGHJ)
user = 'aHt3vGtyggD5fgGHJ'
user == request.auth.uid? Yes
allow access
(when accessing users/aHt3vGtyggD5fgGHJ/groups/h1s5GDS53)
user = 'aHt3vGtyggD5fgGHJ/groups/h1s5GDS53'
user == request.auth.uid? No
deny access
You have two options: either you do as you've done and explicitly match the subcollection, or use this:
function checkAuthorization(usr) {
return usr.split('/')[0] == request.auth.uid;
}
match /users/{user=**}{
allow read,write: if checkAuthorization(user);
}
(the function must be inside your match /databases/{database}/documents, like your rule)
Let me know if this works :)
Security rules now has version 2.
match/cities/{city}/{document=**} matches documents in any
subcollections as well as documents in the cities collection.
You must opt-in to version 2 by adding rules_version = '2'; at the top
of your security rules.
Recursive wildcards (version 2).
This is what works for me:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Matches any document in the cities collection as well as any document
// in a subcollection.
match /cities/{city}/{document=**} {
allow read, write: if <condition>;
}
}
}

Resources