I am trying to query some data from private devices. My rules are set as following:
match /private_devices/{device} {
function userHasKey() {
return request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid)/keys/$(device));
}
allow read: if (userHasKey())
}
My flutter code:
Stream<QuerySnapshot> getPrivateDevices(List<String> keyList) {
return private.where('UDID', whereIn: keyList).snapshots();
}
I use a list to import the keys from:
Stream<QuerySnapshot> getUserKeys(User user) {
return users.document(user.uid).collection('keys').snapshots();
}
And my user rules:
match /users/{userId} {
allow read, write: if (request.auth != null && request.auth.uid == userId)
}
match /users/{userId}/keys/{key} {
allow read, write: if (request.auth != null && request.auth.uid == userId)
}
Edit: my collection references:
final CollectionReference devices = Firestore.instance.collection("devices");
final CollectionReference users = Firestore.instance.collection("users");
final CollectionReference private = Firestore.instance.collection("private_devices");
I get a following permission error in my console:
Listen for Query(private_devices where UDID in [F8MXi2JzwYvIAoVRYG5f] order by __name__) failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
I/System.out(27625): com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
I also tested my rules in the rules playground, and everything worked fine. I know it has to do with querying, but have no clue how to progress forward.
I have also noticed that when i try to get a single document with private.document(keyList[0]).get(), I actually get back the data.
Any help is welcome, thank you in advance.
Your rules don't allow the query because security rules are not filters. Be sure to read that documentation carefully, as well as this blog.
Security rules are not going to cross-reference another document for each document in the query result. That just doesn't scale in the way that Firestore requires. You can do that for individual document gets, but not for queries.
What you will have to do instead is put all of the data necessary for the filter into documents in a single collection, and make sure the client is applying a filter that matches only the documents they are allowed to read by the rule.
Related
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.
I want to limit access to an entire app to a google group.
So my firestore rule could look like
function isGoodEmail() {
return request.auth != null &&
request.auth.uid != null &&
request.auth.token.email.matches('.*#example[.]com$') &&
request.auth.token.email_verified;
}
function isAllowedUser() {
return isGoodEmail() && request.auth.token.email in [
"user1#example.com",
"user2#example.com"
];
}
match /{document=**} {
allow read, write: if isAllowedUser();
}
}
However, I don't really want to hardcode the list in the rules file, because it's also used elsewhere (e.g. on the homepage to show unauthorized user some special home page). I would like a condition like:
request.auth.token.uid in "mygroup#example.com"
Does firestore have any such provision or am I SOL ?
Firebase security rules don't know anything at all about Google Groups. All you have really access to is an email address. If you don't want to hard code them, you could store them individually in documents and use a query to figure out if the email address exists. But you would have to keep the database in sync with the group somehow.
I am currently working on a a app and in that user needs to make a new account. Your Enters first name and last name then the app automatically suggest a username which is unique and it will be the document name of that user. I had set the firestore secutity rules as follows,
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
after user enters the username it checks that the username used or not before move to the next screen.
Future<bool> checkUsernameExist(String name)async{
bool usernameExistSate;
await firestore.collection('users').doc(name).get().then((docSnapShot){
if(docSnapShot.exists){
usernameExistSate = true;
}else{
usernameExistSate = false;
}
});
return usernameExistSate;
}
Currently above system works fine without any problem. But I have a problem, With the firebase security rules sets to below condition how users able to read the documents to check the similar document names are present?
allow read, write: if request.auth != null;
First, I would not use the usernames to store your data in firestore but the uid provided when you are authenicated with google auth. This will allow you much safer access to the database with security rules like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/{document=**} {
allow read, write, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
}
}
For your second problem I would just create a second collection in the root of the firebase project named for example usernames with all usernames stored in a big list so you can query them safely via the firebase API. For that to be possible you have to give the authenticated device access to this collection too via for example adding this under
match /users/...
match /usernames/{document=**} {
allow read, write, update, delete, create: if request.auth != null;
}
Of course then, you have to keep track of both lists when making changes. But this way an authenticated user has only access to his data and all usernames in the worst case.
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.
I'm having some problem with the READ rules of Firestore currently
Here is my data structure
{
email: example#gmail.com,
username: geekGi3L,
birthday: 1995/02/14,
photo: <firestore-download-url>
}
The rules currently I set is
service cloud.firestore {
match /databases/{database}/documents {
match /users/{user} {
allow read;
allow write: if request.auth.uid != null && request.auth.uid == user;
}
}
}
How could I set the rules to allow user to READ the specific fields like email and birthday only if request.auth.uid != null && request.auth.uid == uid while username and photo should be readable by every user?
Thank you <3
In Firstore, there is no per-field access control for reading fields of a document. The most granular unit of access is the document. A user either has full access to read a document in its entirety, or they don't have any access at all.
If you need to change access per field, you'll have to split the fields of the document into multiple collections, with each collection having access control appropriate for the fields of the documents within. It's very common to have a split between public and private data like this.