Restricting a user to save more than 10 documents - firebase

I am not sure what would be the best way to handle this. I want to restrict user to save more than 10 documents. I have collection like
DataCollection > {item-documents}
structure of item-document
{
id: auto-generated
uid: user-uid
.... // other data
}
Option 1:
Before saving a document, I can get the count of total items saved by this user and can restrict. But I think it can be cheated by mocking API response
Option 2:
Is there any way to restrict a write operation in Rules?

NEW ANSWER following below comment from #FrankvanPuffelen:
You can maintain one counter document per user. More precisely, in a specific collection (e.g. userDocumentCounters), you create one document per user that you update each time a main document is created by a user. For that you can use a Batched Write together with the increment() method.
In order to avoid the user to cheat, you assign these create and update security rules to the userDocumentCounters collection (the field containing the counter value is named count in this example):
match /userDocumentCounters/{docId} {
// Applies to writes to nonexistent documents
allow create: request.resource.data.count == 1;
// Applies to writes to existing documents
allow update: if request.resource.data.count < 11 && request.resource.data.count > resource.data.count;
// Applies to delete operations
allow delete: if <condition>;
}
OLD ANSWER:
One possibility would be to have a Cloud Function that updates a counter by user each time a document is created (and deleted?).
This counter is saved in one document by user that you can read in the security rules with the get() method.
The advantage of using a Cloud Function is that you can deny any access to the collection containing the counter docs since the Cloud Function uses the Admin SDK which bypasses the security rules.
An important(?) drawback is the fact that the counter update will not be instantaneous, even if you configure the Cloud Function to have one or more instances always available.

Related

How to keep an item counter in a collection when deleting with the Admin SDK

The function of deleting an item in firestore, returns correctly even if the item to be deleted don't exist.
If we delete a number of elements in a batch and we have a counter of elements of the collection that we want to update, in case some element that we are going to delete no longer exists, the counter would give a smaller number of the real one.
To avoid this, we use the following firestore rule:
       allow delete: if exists (/ databases / $ (database) / documents / ...);
The problem is that if we run the batch on the server, the Admin SDK ignores the firestore rules.
Any solution that does not involve transactions?
How about using a cloud function that listens for deletes?
The way this would work is you set up a listener that waits on specific collections to have a document deleted. This would be a great way to guarantee a delete leads to a function being run, and the function can do anything you'd like (e.g. increment/decrement, etc.).
This could very simply look like this:
exports.deletedItem = functions.firestore
.document('items/{itemId}')
.onDelete((snap, context) => {
const deletedValue = snap.data();
// Increment/decrement anything you'd like here.
// Perform any number of operations on a confirmed delete
});

Firestore: Round-robin access to document in collection

I want to do something very simple, but not sure the best way to do this with Firestore.
I have an ads collection.
Each time an ad is accessed, I want to update the accessed property timestamp so I can just show the ad that hasn't been shown in the longest amount of time.
My security rules only allow users that carry a token with a payload of admin:true to create/modify ads.
So, from within the app, I can't update the timestamp each time an ad is accessed because the users aren't admins.
I looked at creating a function for this but realized that there is no onGet function that would allow me to do this (https://firebase.google.com/docs/functions/firestore-events)
I don't see anyway to allow a single property to be modified by any user.
What would be an appropriate way to do this with Firestore?
You could solve this either by creating a quite comprehensive rules validation where you make a check that all fields except accessed are unchanged. You can implement the admin role concept with custom claims as described in the answer on this post.
Checking that all fields except accessed are unchanged requires you to list and check all fields one by one.
service cloud.firestore {
match /databases/{database}/documents {
match /ads/{id} {
allow read: if true;
allow write: if request.auth.token.admin == true
|| (request.resource.data.someField == resource.data.someField
&& request.resource.data.anotherField == resource.data.anotherField);
}
}
}
Another way, you could do it is to create a callable cloud function that works similar to the Unix touch command. You simply call it from your client for every time your read an ad and you can safely update the accessed field on the post within that function.
export const touchAd = functions.https.onCall((data, context) => {
const adId = data.id;
return admin.firestore().collection('ads').doc(adId).update({
accessed: firebase.firestore.FieldValue.serverTimestamp(),
}));
});

Firestore default users data initialization

I am creating a game and I want to store completed game levels on firestore for each user.
Now my problem is that I will have to initalize this data once - I want to add a document for new user and a pojo that containts map of level ids and boolean for completed/uncompleted.
So I need to execute some kind of logic like "if document with this id doesnt exist, then add that document and add default data that means user hasnt completed any levels". Is there some way that would guarantee Id have to execute this logic only once? I want to avoid some kind of repeating/re-try if something fails and so on, thanks for your suggestion
That's what a transaction is for (definitely read the linked docs). In your transaction, you can read the document to find out if it exists, then write the document if it does not.
Alternatively, you may be able to get away with a set() with merge. A merged set operation will create the document if it doesn't exist, then update the document with the data you specify.
The typical approach to create-a-document-if-it-doesn't-exist-yet is to use a transaction. Based on the sample code in the documentation on transactions:
// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");
return db.runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(sfDocRef).then(function(sfDoc) {
if (!sfDoc.exists) {
transaction.set(sfDocRef, { count: 1 });
}
});
}).then(function() {
console.log("Transaction successfully committed!");
}).catch(function(error) {
console.log("Transaction failed: ", error);
});
Also see:
Firestore create document if it doesn't exist security rule, which shows security rules that allow a document to be create-but-not-updated.
Create a document only if it doesn't exist in Firebase Firestore, which shows how to allow a document only to be created by a UID identified in the data.
the documentation on transactions
the reference docs for the Transaction class

An issue with preventing users from changing specific document fields in Firestore

I'm using firebase in my Android project, I have a users collection in the Cloud Firestore database, users can only update some fields, but can read all, I know it is not possible to protect a subset of a document from being updated, and security rules can be applied only to the entire document, so I searched about this and the only solution I found is to make a sub-collection inside the user document and create a new document inside this sub-collection and finally put fields I want to protect in there, then I can easily secure this document applying this code in security rules section:
match /users/{userId}/securedData/{securedData} {
allow write: if false;
allow read: if true;
}
So now no one can write these fields, but anyone can read, and this is exactly what I want, but later I found that I need to query users based on fields inside the sub-collection, what is known as a collection group query which is not supported at the moment, so I ended up with two solutions:
Retrieve all users and filter them at the client side, but this will increase the number of the document reads because I'm querying all users collection documents + an extra sub-collection document read for each user to get the needed field for filtering users.
Instead of making a sub-collection inside user document and incurring these problems, just keep all fields in the top level document(user document), allowing users to update any field.
match /users/{userId} {
allow update, read: if true;
allow create, remove: if false;
}
But make an (onUpdate) cloud function to listen on user document update and
detect changes in fields which aren't allowed to be modified by the user, so
if the user tried to change these fields I can detect this and return modified fields to their previous values like this:
export const updateUser = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
const previousValue = change.before.data().securedField;
const newValue = change.after.data().securedField;
if (previousValue !== newValue) {
return db.collection('users')
.doc(context.params.userId)
.update({ securedField: previousValue });
}
});
Is the second solution secure? which is the best solution for this? or any other solutions?
Your approach is definitely a possibility, and an interesting use of Cloud Functions. But there will be a delay between the write operation from the user, and the moment the Cloud Function detects and reverts the change.
I'd probably catch the situation in security rules. While you can't deny the user from writing the field in security rules, you can ensure that they can only write the same value to a field that currently has. That effectively also makes it impossible for them to change the value of a field. You do this by:
allow write: if request.resource.data.securedField == resource.data.securedField;
This rule ensures that the field in the updated document (request.resource.data.securedField) has the same value as that field in the current document(resource.data.securedField).

Firestore Access Rules that Rely on a Document Reference

Firestore has a DocumentReference type, which is a "pointer" to another firestore document. Using the firebase JavaScript client, you can access properties (e.g. document "id"), directly on the reference.
For example, if there is a document with a docRef property that is a firestore DocumentReference:
const retrievedDoc = await getFirestoreDocument();
console.log(retrievedDoc.docRef.id); // "jRmSeMYDMKiOPGsmkdaZ"
I am trying to accomplish the same thing within firestore rules. There is a custom function named isOwner. It uses the firestore rules get call on a document path, and then attempts to access the docRef.id just as if it were the JavaScript client above.
get(/databases/$(database)/documents/path/to/$(id)).data.docRef.id
The value of the document's id is compared against the current user's. But when I test this using the simulator and in real code, it fails. I feel like this should work, but it doesn't.
What does work is to store and use the id value directly as a string (e.g. get(/path/id).docId) instead of a DocumentReference.
Should I be able to access the id value of a DocumentReference within the firestore rules? Am I doing something wrong?
I want to avoid doing a second document get within the rule as described in this SO answer. That's a second "read" for each trigger of this rule. And I don't think the document id (which is what I need) will be available on the get call anyway.
Based on documentation:
https://firebase.google.com/docs/reference/rules/rules.firestore#.get
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource
get() method is supposed to returns a Resource object which is supposed to contains a .id property (as well as .data).
For example, to restrict write access to an authenticated user which is the author of a book document (authors documents are identified with the user uid), you would do:
service cloud.firestore {
match /databases/{database}/documents {
match /books/{document=**} {
allow write: if get(resource.data.authorReference).id == request.auth.uid;
}
}
}
Yet I'm always having the error property id is undefined on object on trying.
.data is accessible so I suppose there is an issue in the api.
Update
Actually, a reference is a Path object in Firestore rules as documented here. So you access the id by the index of the part of the path you need.
In this example I use the incoming document's data which has a reference object to lookup a property on another document from a get()
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[4]].canBeRelated
// the [4] assumes the path to be `databases/$(database)/documents/contacts/contactId`
// your exact index would vary for your data structure
}
}
First Answer
This only works in the Firestore dashboard rules simulator, it is not a working example for either the local emulation or production Firestore.
This is a year old but I encountered this same puzzling issue, but not on the data from a get(), just on the data of the request.resource.data. I'm not sure what ought to be available (not even __name__ is available) in the rules but if you're accessing a resource reference on the data and you have a predictable id size (say, 20 characters) you could simply get the range of the path on the resource to check against
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[9:29]].canBeRelated
// the [9:29] assumes the path to be `/contacts/20characterLongIdStr`
// your exact range would vary for your data structure
}
}
Feels like a resource reference object should have at least the id since the path is there. It appears Firestore won't support this for whatever reason.

Resources