Firebase Security Rules: Allow Read + Write to everything except a single collection - firebase

I need to restrict read access to a single collection, keys, in firestore to only authenticated users with certain uids. All other authenticated users can view any other collection/document in the database.
First try is this set
service cloud.firestore {
match /databases/{database}/documents {
match /{col}/{doc}/keys/{key} {
allow read: if request.auth.uid == "A" ||
request.auth.uid == "B" ||
request.auth.uid == "C";
}
match /{document=**} {
allow read, write: if request.auth.uid != null && !("key" in resource.data);
}
}
}
Unfortunately users get this error message when trying to create a new collection in /{col}/{doc} - i.e. a collection sibling to 'keys'
io.grpc.StatusException: PERMISSION_DENIED: Missing or insufficient permissions.
If this were Java I would just run an indexOf on the path, deny if it contained 'keys' and allow otherwise. However, according to Firebase Rules Reference, a Path object does not support indexOf.
Here is the code the results in the error. In this case, the "boxes", and "CFI" collections do not yet exist in FireStore, so they would normally be created when the code runs.
final DocumentReference firstDocRef = firestore.document("test/laksdfjasdf/programCounters/hjg65ffgjj");
CF cfNew = Utils.getCF();
firestore.runTransaction(transaction -> {
DocumentSnapshot snapshot = transaction.get(firstDocRef);
HashMap<String, Object> updatedMap = new HashMap<>();
updatedMap.put("num", cfNew.getNum());
transaction.update(firstDocRef, updatedMap);
Box newBox = createNewBox(cfNew);
final DocumentReference newBoxRef = firestore.collection(
"test/laksdfjasdf/boxes")
.document("" + newBox.getBoxNumber());
transaction.set(newBoxRef, newBox);
final DocumentReference newCFIRef = firestore.collection("test/laksdfjasdf/CFI")
.document("" + cfNew.getCreated().hashCode());
transaction.set(newCFIRef, cfNew);
return newBox;
}).addOnSuccessListener(box -> {
Log.d(TAG, "SUCCESS");
}).addOnFailureListener(e -> {
Log.d(TAG, "Failure");
});

Related

Flutter Firebase Upload with Authentication

I am trying to upload a profile photo as a logged in user with the following however I get error
E/flutter (32619): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: [firebase_storage/unauthorized] User is not authorized to perform the desired action.
My code
User? user = await authenticationService.getCurrentUser();
// Create a Reference to the file
Reference ref =
FirebaseStorage.instance.ref().child(user!.uid).child('/profile.jpg');
final metadata = SettableMetadata(
contentType: 'image/jpeg',
customMetadata: {'picked-file-path': s!.path});
UploadTask uploadTask = ref.putFile(File(s.path), metadata);
var result = await uploadTask;
print(result);
My rule
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read;
allow write: if request.auth.uid == userId
&& request.resource.contentType.matches('image/.+');
}
}
}
What am I missing here ?
I understand that you want to upload a file to the following reference
FirebaseStorage.instance.ref().child(user!.uid).child('/profile.jpg');
and check, in the security rules, that the id of the user executing the upload corresponds to the value of user!.uid.
In your security rules, you have a check as request.auth.uid == userId but nowhere in the rules you define what is userId. You need to use a wildcard, as follows:
service firebase.storage {
// The {bucket} wildcard indicates we match files in all Cloud Storage buckets
match /b/{bucket}/o {
match /{userId}/{imageId} {
allow read;
allow write: if request.auth.uid == userId
&& request.resource.contentType.matches('image/.+');
}
}
}
If you only upload images named profile.jpg in this "folder", you could add a check like imageId == 'profile.jpg' but you could also define the path as:
match /{userId}/profile.jpg {
allow read;
allow write: if request.auth.uid == userId
&& request.resource.contentType.matches('image/.+');
}

How to check if document exists in firestore security rules list request?

I have two root collections, users and workspaces. A user document has a workspaces array with the workspace document IDs the user is a part of. Below is the security rule I try to use. I had two approaches, one is to get the user document from the root collection and check its workspaces array for the workspaceId the other is to check for existance of the userId in the members subcollection. Both end up throwing the same error: FirebaseError: Null value error. for 'list' # L15
If I separate the allow read into allow get and allow list and just write true for list it fixes the error but obviously I want to restrict the access so users can only get their own workspaces (where they are members).
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /workspaces/{workspaceId} {
allow read: if isLoggedIn() && userIsInWorkspace(workspaceId);
}
function isLoggedIn() {
return request.auth != null && request.auth.uid != null;
}
function userIsInWorkspace(workspaceId) {
return exists(/databases/$(database)/documents/workspaces/$(workspaceId)/members/$(request.auth.uid));
}
}
}
Approach with get:
function userIsInWorkspace(workspaceId) {
let workspacesOfUser = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.workspaces;
return workspaceId in workspacesOfUser;
}
UPDATE:
I tried implementing it with custom claims like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if isLoggedIn() && request.auth.uid == userId;
}
match /workspaces/{workspaceId}/{document=**} {
allow read, write: if isLoggedIn() && userIsInWorkspace(workspaceId);
}
function isLoggedIn() {
return request.auth != null && request.auth.uid != null;
}
function userIsInWorkspace(workspaceId) {
return workspaceId in request.auth.token.workspaces;
}
}
}
So my concept was that I have users and workspaces as rootcollections and workspaces has subcollections such as teams and reports and so on. I still get the same null value error. When this comes up is in the above mentioned error so list requests does the error. My use case is that when you are logged out from the app the workspace slug that is stored on every workspace entry in firestore gets added az a query parameter so the user can be redirected back to the exact workspace. To make this happen I do a list request so basically I query the workspaces collection where the slug is the given slug from the url.
const workspaceSnapshot = await db
.collection('workspaces')
.where('slug', '==', this.$route.query.slug)
.limit(1)
.get()
This request creates my error but from this I cannot make out anything. I would suppose that when I give a condition for reads and writes that includes get and list as well.
Queries are case-sensitive. You said you had two root collections, namely Users and Workspaces, but you are querying against users and workspaces. This will not yield results. Try changing your query:
function userIsInWorkspace(workspaceId) {
let workspacesOfUser = get(/databases/$(database)/documents/Users/$(request.auth.uid)).data.workspaces;
return workspaceId in workspacesOfUser;
}

How to manage rights in firebase to allow differents users to read/upate/write?

I've a firestore database and I now need to add a new collection.
Each entry of this collection should contain:
Which userId is the owner(field admin)
Which userId has been allowed to edit this element(field writer)
Which userId has been allowed to only read(field reader).
I'm currently only at the first step, and already strugling:
I was hoping to be able to query my collection( /trips/) and get only the one that I'm allowed to access, but I get an error:
FirebaseError: Missing or insufficient permissions.
Here is my rules file:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
match /trips/{trip} {
allow read, update, delete: if request.auth != null && request.auth.uid == resource.data.admin;
allow create: if request.auth != null;
}
}
}
So my questions:
Is this the correct way of managing resource that must be acceeded by multiple people(meaning, I cannot just have the userId in the path since there are multiple users)
How should I query only the documents list that I'm allowed to see?
Thank you very much for your help
As you will read in the doc, "All match statements should point to documents, not collections".
With
service cloud.firestore {
match /databases/{database}/documents {
match /trips {
// ....
}
}
}
you don't point to a document. You should use a wildcard to point to any document in the specified path, as follows:
service cloud.firestore {
match /databases/{database}/documents {
match /trips/{trip} {
// ....
}
}
}
Therefore the following should correctly implement your requirements:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /trips/{trip} {
allow read: if request.auth != null &&
(request.auth.uid == resource.data.admin
|| request.auth.uid == resource.data.writer
|| request.auth.uid == resource.data.reader
);
allow update: if request.auth != null &&
(request.auth.uid == resource.data.admin
|| request.auth.uid == resource.data.writer
);
allow create: if request.auth != null;
}
}
}
Then, for the two questions:
Is this the correct way of managing resource that must be acceeded by multiple people (meaning, I cannot just have the userId in the path
since there are multiple users)
If the admin, writer and reader are specific for each document, yes this is the correct way. If those roles would be more global (e.g. all the trips to Europe can be edited by the same user), you could use a role based approach with Custom Claims.
How should I query only the documents list that I'm allowed to see?
It is important to note that rules are not filter. So your query for getting docs needs to be aligned with the rules. In your specific case, you could have an additional field of type Array which contains three values; the uids of the admin, writer and reader, and use the array-contains operator. Something like:
const user = firebase.auth().currentUser;
const query = db.collection("trips").where("authorizedReaders", "array-contains", user.uid);
match /{document=**} {
allow read, write: if false;
}
You don't need the above code as it will apply to all routes of the database, because of the above line you are getting the below error as it does not allow you to read and write to the database
FirebaseError: Missing or insufficient permissions.
Now, if you want to assign privileges to users then you should add the Role field to users collections which would have a value such as Admin, Editor, Reader
Then, you can check in routes something like below
match /users/{userId}/trips/{tripId} {
allow read, delete: if request.resource.data.role == "Admin";
allow create, update: if request.resource.data.role == "Admin || request.resource.data.role == "Editor";
}
If you want to know more about how to create a route check out this video for the best explanation

Firestore security rules to compare two databases and prevent duplication

In my app I want to be able to have books assigned to a reader without there being any duplicates. Can I assign a rule like that to firestore or is that something to handle in my swiftui code? The security rules so far is as follows:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match/users/{uid}{
allow read: if request.auth.uid == uid
allow create
}
match/books/{id}{//display library if user logged in
allow read: if request.auth != null
}
match/reader/{uid}{//check for readers assigned to user
allow read: if request.auth != null && request.resource.data.uid == request.auth.uid;
allow write: if request.auth != null;
match/reader_books/{id}{
allow read: if request.auth != null
allow write:if request.auth != null
}
}
}
}
EDIT: I tried to create a function in swiftui code to check for duplicates here:
func checkData(_ readerBooks: ReaderBooks){
print("reader ID : \(readerBooks.readerID)")
let checkBooks = db.collection("reader_books").document("\(readerBooks.readerID)")
checkBooks.getDocument{(document, error) in
if let document = document, document.exists{
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
}else{
print("Document does not exist")
self.postData(readerBooks)
}
}
}
When trying to add a book to a reader it always returns document does not exist in the console even when the readerID is present in the document's field. Is there an issue with the code somewhere?
You don't need security rules for this. In the reader_books subcollection, just use the same ID as the book from the books collection. Since a collection can't have two documents with the same ID, it will be impossible for one book to appear in that collection twice.

Setup Firestore User Security Rule

I am new to Firestore and I'm trying to setup simple security rules so that only someone who is signed in can create a new database entry and that users can only read and write their own entries.
service cloud.firestore {
match /databases/{database}/documents {
match /Users/{userID} {
// Can only create a new entry if signed in with a uid.
allow create: if request.auth.uid != null;
// Can only update an entry if signed in with uid and changing own information (saved under uid)
allow update: if request.auth.uid != null &&
userID == request.auth.uid;
// Can only read (get/list) an entry if signed in with uid and reading own information (saved under uid)
allow read: if request.auth.uid != null &&
resource.data.userID == request.auth.uid;
}
}
}
The create new entry case works fine, but I wonder if this is secure enough.
For updating and reading, I also want to check that the user is updating/reading their own entry. The document name is the uid (in other words UserID) from Firebase so simply checking that request.auth.uid is the same should do the trick, but something is off in the way I'm writing it. The call gets blocked and when I run it in the simulator I get the error: Missing or insufficient permissions. I can't figure out after reviewing the documentation and this tutorial video.
Something like below should be sufficient for your situation:
// True if the user is signed in or the requested data is 'public'
function signedInOrPublic() {
return request.auth.uid != null || resource.data.visibility == 'public';
}
// 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 signedInOrPublic();
}

Resources