firestore security rules: allow logged in user to update field - firebase

I have the following firestore security rules
match /users/{user} {
allow read: if request.auth != null;
allow create: if request.resource.id == request.auth.uid;
match /replies {
allow update: if request.auth != null;
}
}
my understanding from the firestore security rules doc is that the two rules are independent of each other. However, a logged in user gets a permission denied message when trying to update the /user/{user}/replies field. It doesn't matter if I nest the rule or not. It still gets denied. (it also doesn't work in Rules Playground in Firebase console)
what am I doing wrong?
my client code is as follows:
const processNewMessage = async (evt, newMessage) => {
myMessage.value = myMessage.value.trim();
if (evt.srcElement.textContent === "send") {
if (replying.value === true) {
const message = {
originalMessage: {
text: props.selectedMessage.text,
photoUrl: props.selectedMessage.photoUrl,
},
reply: {
user: uid,
userDisplayName: auth.currentUser.displayName,
userName: userName.value,
text: newMessage,
createdAt: Timestamp.now(),
g: { geohash: geohash.value, geopoint: myLocation },
photoUrl: photoUrl,
},
};
await updateDoc(doc(db, `users/${props.selectedMessage.user}`), {
replies: arrayUnion(message),
});
}
}
}

Security rules work on a document-level. Your code is trying to update the replies array in the users document, which is not allowed by any of your rules.
The match /replies in your rules applies to a replies subcollection under the users document. It has no effect on fields in the users document.
If you want the owner to update their entire document, and other signed in users to only update the replies field, you'll have to allow that in the rule on users.
Something like this:
match /users/{user} {
allow read: if request.auth != null;
allow create: if request.resource.id == request.auth.uid;
allow update: if request.auth != null &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["replies"])
;
}
For more on how this works, see the release notes for the map diff function.

Related

How to write security rules for users setting documents that don't exist?

When a user registers, a document should be set in Firestore (database/users/${uid}). However, I keep getting a "Missing or insufficient permissions." error.
This is the most relevant security rule
match /users/{documents=**} {
allow read, create, update: if request.auth != null && request.auth.uid == resource.id
}
Another rules I tried implementing was
match /users/{uid=**} {
allow read, create, update: if request.auth != null && request.auth.uid == uid
}
and this is the code that registers the user and sets the document
createUserWithEmailAndPassword(auth, emailInput, passwordInput).then(
(UserCredential) => {
console.log(UserCredential);
uid = UserCredential.user.uid;
sendEmailVerification(auth.currentUser).then(() => {
toast.success(
"Account created and email verification sent! Please check your inbox/spam folder",
{
duration: 10000,
}
);
setDoc(doc(db, "users", uid), {
userSettings: ["example", 0],
});
router.push("/verify");
});
}
);
Why can't I set a document as an authorized user accessing my own user document?
The problem is request.auth.uid == resource.id. From the documentation,
The resource variable refers to the requested document, and resource.data is a map of all of the fields and values stored in the document.
But the document does not exists as user has just registered to your application.
Try the following rules instead:
match /users/{userId} {
allow read, create, update: if request.auth != null && request.auth.uid == userId;
}
This rule will ensure that a user can create/read/update a document with their user ID only.
Also do note that the match path is /users/{userId} and not match /users/{userId=**} as in your question. The value of userId would be /userID and not just userID if you use the recursive wilcard (=**) and rule will fail always.
If the rule must be applied for all nested collections, then use the recursive wildcard on the next path segment:
match /users/{userId}/{path=**} {
// ... can still read userId
}

Firestore: Prevent write/overwrite/update field once it is added

I have a collection in which I am storing user requests in documents having documents ID as user's email. In the document, I am creating fields the key for which is being generated at client side.
Now, the problem that I am facing is that user can overwrite the existing field/request in the document if the key matches which I don't want to happen.
What I tried was to use this rule which unfortunately does not work
resource.data.keys().hasAny(request.resource.data.key();
So how can I achieve this?
Below are the screen shot of the firestore data and the current security rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /roles/{userId}{
allow read: if isSignedIn() && hasId(userId);
}
match /requests/{email} {
allow read, update: if isSignedIn() && hasMail(email)
}
//functions//
function hasMail (email) {
return request.auth.token.email == email;
}
function hasId (userId) {
return request.auth.uid == userId;
}
function isSignedIn () {
return request.auth != null;
}
function getUserRole () {
return get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.role
}
}
}
You can check if a resource already exists. Here an example:
allow write: if resource == null // Can create, not update
Use that to restrict any edit or update of the data. If you have additional rules you can granulate them to update, delete and create.

How to grant collection access to user based in other collection?

Let's assume that there is 3 collections and they are at the same hierarchy level:
User
UserAndOtherCollectionRelationship
OtherCollection
I desire to grant access on "OtherCollection" records to the users that own that record or are related to it (only read access).
Understand "UserAndOtherCollectionRelationship" as
UserAndOtherCollectionRelationship: {
'userId': uid, //user id provided by Firebase Auth Service
'otherCollectionId': 000,
'roles': ['owner', 'reader', ...]
}
This is what I have:
match /databases/{database}/documents {
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
match /user/{userId} {
allow read, write: if(isSignedIn() && isUserOwner(userId));
}
match /userAndOtherCollectionRelationship/{userAndOtherCollectionRelationshipId} {
allow read: if(resource.data.userId == request.auth.uid && isSignedIn());
allow create: if(isSignedIn());
allow update, delete: if(resource.data.userId == request.auth.uid);
}
// Functions
function isSignedIn() {
return request.auth != null;
}
function isUserOwner(userId) {
return request.auth.uid == userId;
}
function isOtherCollectionOwner() {
return isUserOwner(getUserAndOtherCollectionRelationshipData().userId) && getOtherCollectionData().roles.hasAll(['owner']);
}
//This is the function that I believe that it's not working propertly
function getuserAndOtherCollectionRelationshipData() {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/{document=**}).data;
}
}
Considering that the client (the app) must create a filter (where clause) to get only the desired records, I could not find a way to do that with this schema too.
So I put the user roles as a field on the "otherCollection" record:
otherCollection: {
...,
'userAndRoles': {
'replaceByUID': ['owner', ...]
},
}
updated the security rule function to:
function isOtherCollectionOwner() {
return get(/databases/$(database)/documents/OtherCollection/$(otherCollectionId)).data.roles[request.auth.uid].hasAll(['owner']);
}
Here is the client call:
final querySnapshot = await firestore.collection('otherCollection')
.where('user.$userId', arrayContains: 'owner')
.where('otherCollectionId', whereIn: otherCollectionIdList)
.get();
What is the best solution?
Change the data model to...
Set a different security rule as...
When a user wants to access an another collection, we have to set a rule under that collection.
So when you create a document in a collection, you have to create the same id in the other collection and create a field called owner.
This field owner contains the uid of the person who created the document.
userAndOtherCollectionRelationshipId === otherCollectionId
UserAndOtherCollectionRelationship: {
'owner': uid
}
In this way, when a user try to read the document, we check if he is owner or not with isOwner(otherCollectionId, request.auth.uid) function with Collectionid, and the userId. In the function you check if the owner who created the document is the same who is trying to read the document.
You can do create a rule as following:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if isOwner(otherCollectionId, request.auth.uid);
allow create: if isSignedIn();
}
function isOwner(docId, userId) {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/$(docId)).data.owner == userId;
}
To solve the issue, I updated the data model removing the userAndOtherCollectionRelationship collection and add the owner attribute to the otherCollection.
Any other relationship would be added as an attribute to otherCollection.
So the otherCollection looks like this now:
otherCollection: {
owner: ["user_uid", "other_user_id"],
..., //other atributes
}
The security rules were updated to:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
function isOtherCollectionOwner() {
return ([request.auth.uid] in (resource.data.owner));
}
The security rules tests were updated to:
const myAuth = {uid: 'my_user_uid', email: 'my#mail.com'};
const MY_PROJECT_ID = "my_project_id";
function getAdminFirestore() {
return firebase.initializeAdminApp({projectId: MY_PROJECT_ID, auth: myAuth}).firestore();
}
function getFirestore(auth) {
return firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth}).firestore();
}
describe("MyApp", () => {
it("Can list if is owner", async () => {
const admin = getAdminFirestore();
const setupOtherCollection = admin.collection('otherCollection').doc('otherCollectionId');
await setupOtherCollection.set({'name': 'myOtherCollection', 'owner': [myAuth.uid]});
const db = getFirestore(myAuth);
const otherCollectionCollection = db.collection("otherCollection").where("owner", "array-contains", [myAuth.uid]);
await firebase.assertSucceeds(otherCollectionCollection.get());
});
});

Missing or Insufficient permissions on Firestore Security Rule with get()

Pretty much no matter what I use for the get() request, getting Missing or insufficent permissions when logged in with a userID that is a "member":
function isSelf(userID) {
return request.auth != null && request.auth.uid != null && request.auth.uid == userID
}
function isMember(userID) {
return request.auth != null && request.auth.uid != null && get(/databases/$(database)/documents/'members'/$(request.auth.uid)).data.parent == userID
}
match /templates/{userID} {
allow read, write: if false
match /templates/{templateID} {
allow read: if isSelf(userID) || isMember(userID)
allow write: if isSelf(userID)
allow delete: if false
}
allow read: if isSelf(userID) || isMember(userID)
allow write: if isSelf(userID)
}
Have tried using get() with .data.parent and with .parent The member doc looks like this:
{
parent: 'USER_ID_OF_PARENT'
}
Call from the client app is:
export const getTemplate = async ({ userID, form }) => {
db.collection('templates').doc(userID).collection('templates').doc(form).get()
.then((doc) => {
})
.catch((err) => {
console.error(err)
})
}
Database structure is:
/templates/{userID} is a collection of docs with ids as userIDs that correspond to a doc with matching userID in /users/{userID}
/members/{memberID} is a collection of docs with ids as memberIDs, with a parent field with a string value SOME_USER_ID which matches a doc with userID SOME_USER_ID in /users/{userID}
Example:
/members/'MEMBER_1' doc:
{
name: 'Member 1',
parent: 'OWNING_USER_1'
}
/users/'OWNING_USER_1' doc:
{
name: 'Owning User 1',
parent: 'OWNING_USER_1'
}
/templates/'OWNING_USER_1' doc:
{
// no fields
}
/templates/'OWNING_USER_1'/templates/'FORM_1' doc:
{
name: 'Form 1'
}
With the following call:
getTemplate({
userID: 'OWNING_USER_1',
form: 'FORM_1'
})
When the authenticated user is OWNING_USER_1, the above call is successful (the isSelf() rule returns as true) and the found template document is returned
When the authenticated user is MEMBER_1, the above call gets Missing or insufficient permissions (the isMember() rule returns false)
Removed the quotes from around 'members' and this is now working correctly:
Replaced:
get(/databases/$(database)/documents/'members'/$(request.auth.uid)).data.parent
with:
get(/databases/$(database)/documents/members/$(request.auth.uid)).data.parent

How to update Firestore Rules with custom Roles

How to configure RULES to control Reads/Writes and Deletes on a collection (RECORDS) based on custom user roles defined in another collection (USERS)?
---- User Collection ----
USERS: {
<RandomID1> : { uid: 234, email: "abc#xyz.com", role: "ENDUSER" },
<RandomID2> : { uid: 100, email: "def#xyz.com", role: "ADMIN" }
}
---- Records Collection ----
RECORDS: {
<RandomID1> : { uid: "234", name: "Record 123" },
<RandomID2> : { uid: "234", name: "Record 456" },
<RandomID3> : { uid: "999", name: "Record 999" } /* another user's record */
}
---- Current Rules ----
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
Assuming the user is logged in the web application and using client-side Firebase SDK, how to achieve the below cases?
IFF: USERS.RandomID1.role = 'ENDUSER'
How to restrict the user to READ only their records from RECORDS,
but not somebody else's?
How to restrict UPDATEs to only their
records in RECORDS?
How to restrict DELETEs on all of their records in RECORDS?
How to restrict all CRUD operations on the rest of collections(/COLLECTION**), except RECORDS?
IFF: USERS.RandomID1.role = 'ADMIN'
How to enable this (Admin) user to perform all CRUD operations in
RECORDS?
So, how to rewrite or update rules to control these operations? If not, are there better designs or alternatives?
Note: We need to handle these cases to block some users/hackers who may try to open browser console/inspect window, and execute firestore queries with or without any conditions.
I appreciate your help!
For 5 you will need to define custom claims for your admins, below I assume a field isadmin is set to true for admins.
The following rules should be a good start:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 4: Will restrict all others access
match /records/{recordid} {
// 1+5(read): restrict reads
allow read: if request.auth != null
&& (resource.data.uid == request.auth.uid || request.auth.token.isadmin);
// 2+3+5(write): +create as a bonus
allow write: if request.auth != null
&& (request.resource.data.uid == request.auth.uid || request.auth.token.isadmin);
}
}
}
Here is my final answer:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
//Rule on a Collection Groups, instead of simple Collection
match /{NestedSubCollections=**}/<COMMON_COLLECTION-NAME>/{doc} {
allow create: if isSignedIn() && isAdmin();
allow read: if isSignedIn() && (isAdmin() || isThisUserRecord(resource.data.<CUST_USER-ID-FIELD>));
allow update: if isSignedIn() && (isAdmin() || isThisUserRecord(resource.data.<CUST_USER-ID-FIELD>));
allow delete: if isSignedIn() && isAdmin();
}
//Collection UserDB, to validate profile updates
match /UserDB/{userId=**} {
allow create: if isSignedIn() && isAdmin();
allow read: if isSignedIn() && (isAdmin() || hasUserProfile(resource.data.<CUST_USER-ID-FIELD>));
allow update: if isSignedIn() && (isAdmin() || hasUserProfile(resource.data.<CUST_USER-ID-FIELD>));
allow delete: if isSignedIn() && isAdmin();
}
//Checking if user is signed-in
function isSignedIn() {
return request.auth != null;
}
//Checking user's admin status, without the context of accessing collection, but directly
function isAdmin() {
return get(/databases/$(database)/documents/UserDB/$(request.auth.uid)).data.<CUST_ROLE-FIELD> == "<ADMIN-ROLE-NAME>";
}
//Check whether a current record belong to logged-in user; by <CUST_USER-ID-FIELD>, not oAuth-ID
function isThisUserRecord(custUserId) {
return get(/databases/$(database)/documents/UserDB/$(request.auth.uid)).data.<CUST_USER-ID-FIELD> == custUserId;
}
//Check whether a logged-in user has existing profile or not
function hasUserProfile(userOauthId) {
return userOauthId == request.auth.uid;
}
}
}
It is working perfectly fine in my production application and everything is secured without writing a single line of code in the application!
How to test it?
Updated a user record in UserDB with <CUST_ROLE_FIELD>, and <ADMIN_ROLE_NAME> as value
Then go to your frontend web application, go to browser Console, and trying to hit Firestore queries and validate the records and results
If it's a non-admin user, then he can only Read/Update as per our rules above, and the other queries will fail as expected.
Note, the create/delete is performed in the backend, via cloud function, for security reasons.
In sum, admin can perform any action on all records, but non-admin users can only touch his own records with limited Read/Update operations only.
You may have to tweak a bit based on other use cases. Let me know if anything.

Resources