When implementing security rules to ensure a user can only fetch/update their own documents in Firestore, where should I put the logic?
(Before you say 'in Firestore Security Rules file', please note that I am using the Firebase Admin SDK which bypasses these security rules rather than the frontend Firebase SDK.)
For more context:
But where should I put the logic to check, for example, that a document being fetched has a 'userId' property set to the ID of the currently logged in user.
It seems I have 3 options:
In middleware. Before passing responsibly to controller to do the work, middleware should perform a 'get' operation then check the userId attribute of the document returned matches the logged in user. Downsides of this approach is that I am fetching the document twice - once to check permissions and then again in the controller.
In controller. Once the data has been fetched from the database I do the userId check. Downsides of this are code replication as I'll have to write this logic in each controller function.
In service. Perform the check in the FirestoreService class once the data has been fetched. Downside of this approach is that it feels like I'm adding additional logic to a service which should remain generic. The service get method would need to be called with details about what attributes to check and know the userId.
What's the best approach to this puzzle in terms of architecture?
Bear in mind it should work for all CRUD operations, not just the 'read' operation.
Any help much appreciated!
I started with the logic in the service but it feels clunky.
You have not provided much details on what queries you are running, or what resources can be deleted so I'll take a general CRUD example of TODOs.
First, you'll need the user ID in every controller/service before querying so it makes sense to verify user's token in a middleware and add it to request context. Then you just have to ensure you query that user's data. For example, if you have a list TODOs API, your query will be:
const snap = await todoCol.where('userId', '==', ctx.user.id).get();
If you don't add the query constraint, then Firestore SDK will return all the documents in the collection.
The same applies for update and delete operations. If you have a DELETE /todos/:todo_id
API, it'll be best to query the document first and check if the request user ID matches the user ID in the document data. If not, return an error.