Firestore security rules: get() returns different result than the expected one - google-cloud-firestore

I have a firestore collection of usernames where each individual username acts as a document id. Each individual document has two fields only - uid (the uid of the owner) and createdAt. Thats all. I want to write a security rue, where I say "You can delete username ony if you own it". So here is my security rule:
match /usernames/{username} {
function userOwnsUsername() {
let unused = debug("does user owns username?");
let uid = get(/databases/$(database)/documents/usernames/$(username)).data.uid;
return debug(request.auth.uid == uid);
}
allow delete: if isUserAuthenticated() && userOwnsUsername();
}
function isUserAuthenticated() {
return request.auth.uid != null;
}
When I remove the rule userOwnsUsername the operation is executed successfully. Can someone tell me what I am doing wrong?

You don't have to use get() when trying to read data from the document being accessed/updated. Try using resource.data instead:
match /usernames/{username} {
function userOwnsUsername() {
return request.auth.uid == resource.data.uid;
}
allow delete: if isUserAuthenticated() && userOwnsUsername();
}

Related

How to make a user read a collection of all documents where documents.uid == user.uid in firebase flutter

Basically I have 2 collections 'Bookings' and 'Users'. The 'Bookings' collection contains all bookings created by every user, and the 'Users' collection displays information about the user.
User: {
name:
uid:
}
Bookings: {
location:
time:
uid:
etc:
}
I have a GetBookings() function that retrieves the 'Bookings' collection and display it for an admin account. However, I am currently stuck on how to approach displaying a user his bookings.
getBookings() {
var bookings = FirebaseFirestore.instance.collection('bookings');
return bookings.get();
}
I thought about creating another 'Bookings' collection under each user but am unsure on how to link this new 'Bookings' collection with the previous collection in order to preserve the same bookings id. I had a go with security rules as mentioned by #Renaud Tarnec, however I might be getting the syntax wrong, or during looping through the bookings collection and receiving a permission denied on our request it preemptively stops my fetchBookings() function, or a user might be able to access the entire 'Bookings' collection regardless of whether each booking has his uid or not.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Allows users to view their bookings
match /bookings/{booking} {
allow read: if request.auth != null && request.auth.uid == booking.uid;
allow write: if true;
}
}
}
Future<List<BookingModel>> fetchBookings() async {
var bookings = await _bookingRepository.fetchAllBookings();
return bookings.map((snapshot) {
var bookingMap = snapshot.data();
return BookingModel(bookingMap['email'], bookingMap['location'], bookingMap['phoneNumber'],
bookingMap['dateTime'], bookingMap['uid'], bookingMap['dateCreated']);
}).toList();
}
I'd like to know what would be professional/industrially accepted way in tackling this problem.
Like I said, in my opinion, the best solution for you is to set correct rules in database and create correct queries to get that data.
Rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /bookings/{docId} {
allow read: if resource.data.uid == request.auth.uid || isAdmin()
// bellow you can use second part after && but im not sure are it will be null or unassigned this is overenginered so you can just not use condition after &&.
allow update: if resource.data.uid == request.auth.uid && request.resource.data.uid == null || isAdmin()
allow create: if request.auth != null && request.resource.data.uid == request.auth.uid || isAdmin()
allow delete: if isAdmin()
}
}
}
function isAdmin() {
return request.auth.token.admin == true;
}
Queries you need to make for users:
getBookings() {
// Im not sure are it will work like that in flutter im not a flutter programmer.
// You need to specify using where() method that you want documents with your uid or rules will not allow you to get eny data.
var bookings = FirebaseFirestore.instance.collection('bookings').where('uid', '==', user.uid);
return bookings.get();
}
It would be better if: While you adding the booking data to the "Booking" collection, you also need to add it also to the user.booking collection.
Since the bookings collection can only be accessed by an admin account, a classical solution in your case (denormalization in a NoSQL Database) is to use a Cloud Function to create the Booking document in the users/{userID}/bookings subcollection when a new Booking is created in the bookings collection.
Something along the following lines:
exports.duplicateBooking = functions
.firestore
.document('bookings/{docId}')
.onCreate((snap, context) => {
const userId = ....; // Not clear from your question how you define that. You should probably add it to the booking doc.
const bookingData = snap.data();
return admin
.firestore()
.collection(`users/${userId}/bookings)
.add({
'location': bookingData.location,
'time': bookingData.time,
'email': bookingData.email,
'phoneNumber': bookingData.phoneNumber
});
});
Another possibilities would be to keep a unique bookings collection with a set of Security Rules that allows a user to read his own bookings. In this case, remember that rules are not filters when you write the corresponding query.

Firestore Collection List operation fails with "missing or insufficient permission"; Works for individual documents

I'm trying to construct rules that allow a user to access all sub-collections and documents of a given account, if they are listed as a user of that account.
This works perfectly for retrieving individual documents, event nested ones. However, it fails when trying to list documents in a sub-collection.
This does not appear to me to be an instance of "rules are not filters": my query should categorically pass for every possible item queried, as it's based on their root ancestor document.
I've also read here (although I couldn't find it in the documents) that list operations also fail if you try and perform a get() for each queried document. I don't believe my rules violate this, either, as the get() command only needs to be run once, and will categorically return the same document for each queried document, as, again, it's based on a common ancestor.
Is my reasoning on the above rules wrong, or is there something else I'm not doing right?
My rules are as follows:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function hasAccountPermission(request, path, permission) {
let accountId = path[0];
let account = get(/databases/$(database)/documents/accounts/$(accountId));
let role = account.data.users[request.auth.uid];
return (
request.auth != null
&& role != null
&& (permission == null || permission in account.data.roles[role])
);
}
match /users/{user} {
allow read, update, delete: if request.auth != null && request.auth.uid == user;
allow create: if request.auth != null;
}
match /accounts/{account=**} {
allow read: if hasAccountPermission(request, account, null)
allow write: if hasAccountPermission(request, account, 'all')
}
}
}
Example database is as follows:
/accounts/bobstuff
roles: {
admin: ['all']
}
users: {
bob: 'admin'
}
/docs/adocument
field: value
A get operation on /accounts/bobstuff/docs/adocument succeeds
A list operation on /accounts/bobstuff/docs/ fails
Ok. It turns out it was was violating the second principle - or at least, Firestore thought it was.
By changing the rules to capture the first path segment, instead of capturing the whole path, and extracting the first segment manually, it appears Firestore was able to determine that only a single get() would be required, and therefore, the rules were valid.
This required splitting the rule into {account} and {account}/{doc=**}. The side-effect of this is that I could also make the rules more efficient by avoiding a get() call for the root case.
Complete (working) rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function hasAccountPermission(request, account, permission) {
let role = account.data.users[request.auth.uid];
return (
request.auth != null
&& role != null
&& (permission == null || permission in account.data.roles[role])
);
}
match /users/{user} {
allow read, update, delete: if request.auth != null && request.auth.uid == user;
allow create: if request.auth != null;
}
match /accounts/{account} {
allow read: if hasAccountPermission(request, resource, null)
allow write: if hasAccountPermission(request, resource, 'all')
allow create: if request.auth != null
}
match /accounts/{account}/{doc=**} {
allow read: if hasAccountPermission(request, get(/databases/$(database)/documents/accounts/$(account)), null)
allow write: if hasAccountPermission(request, get(/databases/$(database)/documents/accounts/$(account)), 'all')
}
}
}

Having problems with Firebase access rules

This is mostly me playing with various cloud storage mechanisms, so I came with some test code. In this one, I wanted to have users and group them into households. The data structures I have in Firestore are:
Users/{user}/
name (string)
email (string)
admin (bool)
Households/{household}/
name (string)
users (array of string)
The identifier for {user} is the user ID from the User api (I'm using Swift for my code); the identifier for {household} is a UUID.
The rules I have for the database are:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /Users/{uid} {
allow create: if request.auth.uid != null;
allow read, write: if request.auth.uid != null && (request.auth.uid == uid || isAdmin());
allow delete: if isAdmin();
}
match /Households/{household} {
allow create: if request.auth.uid != null;
allow read, write: if hasAccess(household);
}
function hasAccess(household) {
let id = (request.auth != null) ? string(request.auth.uid) : "";
let users = id == "" ? [] : get(/databases/$(database)/documents/Households/$(household)).data.users;
return id != null && ((id in users) || isAdmin());
}
function isAdmin() {
let id = request.auth.uid;
return get(/databases/$(database)/documents/Users/$(id)).data.admin == true;
}
}
}
The Playground works with my UID; however, my code does not -- it gets an access denied error. (If I set my UID to have admin set to true, it works, so I know that part of the rules is working.)
A different problem on stackoverflow I found yesterday (63621376) showed the same problem, and it was fixed by converting a value to a string, which you can see I try there.
I have been unable to get the CLI emulator working, primarily because I use Macs, and I haven't been able to get the 1.8 version of Java installed in a way that it can work with.
ETA the client code:
let ref = self.dbHouseholds!
ref
.whereField("users", arrayContains: self.user?.id ?? "")
.getDocuments { snapshots, err in
print("snapshots = \(snapshots), err = \(err)")
}
It also fails if I don't have the .whereField query. The errors are
snapshots = nil, err = Optional(Error Domain=FIRFirestoreErrorDomain Code=7 "Missing or insufficient permissions." UserInfo={NSLocalizedDescription=Missing or insufficient permissions.})
The rule is denying your query because Firebase security rules are not filters. Please be sure to read and understand that documentation thoroughly.
The playground allows you to perform a request for a single document, but what you're showing here is a collection query, which you can't simulate in the console. When you perform a collection query, the rules will reject any query where there is any possible document that might not allow access. Rules will not scan every single document to pick out the ones that match - that does not scale at all.
Your function hasAccess depends on the value of a variable "household" containing an individual document ID being accessed. Since you are querying for many documents, you can't use that variable to check each document.
If you want to write a rule that requires that users can only query documents that have their UID in the users field, you'll have to write that condition like this instead:
request.auth.uid in resource.data.users
This will enforce the where clause in your query.

Firestore rule: match top level collection

Setup:
My top collection is named users
Each user is named for their unique ID, uid
I want to make a rule so that no matter what document or sub-collection is being accessed, if it will compare uid to the name of current user in users to allow
Current attempt:
Note that this WORKS for top level documents, but as soon as I try to work with a sub-collection within that user, it fails
If it matters, there will be 7 named sub-collections that are always the same between users
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{user} {
function isSignedIn() {
return request.auth.uid != null;
}
allow read, write: if isSignedIn() && request.auth.uid == user
}
}
}
Any help would be appreciated. I think I need to add some ** somewhere?
You will want to use a recursive wildcard to match all documents in all subcollection under the top-level collection.
match /users/{user}/{everything=**} {
function isSignedIn() {
return request.auth.uid != null;
}
allow read, write: if isSignedIn() && request.auth.uid == user
}
In rules version 2, recursive wildcards match 0 or more path segments.

Firestore: How to set security on object types with users as field names

As I understand it, Firestore does not allow queries within fields of type array, which is a shame. Therefore, if you want to be able to query the contents of an array you have to set up a field as an object type and then set the fields like a map, this is called a nested map. I want to have a map where the key is the ID of another user. Therefore, the database structure is:
database
users
{userId}
friends
userId1: true
userId2: true
The 'userId1' and 'userId2' field names will vary depending on the userId of the person listed as a friend.
The question is, how do I write my security rule so I can find my documents (via my {userId}) and the documents of other users where my {userId} is a field in the 'friends' object of the other user's document?
I guess it needs to be something like..
match /users/{userId} {
allow read, update, delete: if resource.data.userId == request.auth.uid;
allow read: if resource.data.friends.{userId} == true;
}
But of course this does not work because you cannot seem to use the variable {userId} to name the field that you want to perform a test on. So, if this cannot be done, what is a way to search for documents and have my {userId} stored somehow in someone else's document?
Edit
Well, I think I have the rules determined (see below). However, when trying to test these rules I can't seem to write a Swift call to retrieve data based on that friends object. My Swift call is:
db.collection("users").whereField(FieldPath(["friends.\(userId)"]), isEqualTo: true)
So, my questions are:
Are the rules below correct?
How do I make a Swift call to find the people with a certain userId in the field name of an object type?
service cloud.firestore {
match /databases/{database}/documents {
match /users/{documentId} {
allow read, write: if isOwner();
allow read: if getFriend(request.auth.uid) == true;
function isOwner() {
return request.auth.uid == resource.auth.uid;
}
function getFriend(userId) {
return getUserData().friends[userId]
}
function getUserData() {
return get(/databases/$(database)/documents/rooms/{documentId}).data
}
}
}
}
I still have not resolved the problem of accessing fields in an object, but it is noted that my Security Rules where generally invalid. You cannot have multiple lines with the same rule type in it, you cannot have multiple lines with 'allow: read' for example. You must use && and ||. For example, the correct definition for the basic rules if you want to check two things are:
// Database rules
service cloud.firestore {
// Any Cloud Firestore database in the project.
match /databases/{database}/documents {
// Handle users
match /users/{documentId} {
// The owner can do anything, you can access public data
allow read: if (isOwner() && isEmailVerified()) || isPublic();
allow write: if isOwner() && isEmailVerified();
// Functions //
function isSignedIn() {
return request.auth != null;
}
function isOwner() {
return request.auth.uid == resource.data.userId;
}
function isPublic() {
return resource.data.userId == "public";
}
function isEmailVerified() {
return request.auth.token.email_verified
}
}
}
}