How is it possible that listening to Firestore collection allows to access data that I shouldn't have access to? - flutter

Given the below rule:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Nobody except superuser and member can see member data
match /Member/{id} {
allow get: if ((request.auth != null) && (id == request.auth.uid));
allow list: if ((request.auth != null) && (id == request.auth.uid));
//allow read: if ((request.auth != null) && (id == request.auth.uid));
allow update: if ((request.auth != null) && (id == request.auth.uid));
allow delete: if ((request.auth != null) && (id == request.auth.uid));
allow create: if ((request.auth != null) && (id == request.auth.uid));
}
}
}
And my data: Member collection.
document ID: PIijFvYPXQPSlheQxotvFvXW4VI2 which has some fields
document ID: QM6tGSkA2SRSPjHGIurDiARd6zv1 which has some fields
My observations that make sense:
1) Using the Rules playground
Simulation type: get
Location: /Member/PIijFvYPXQPSlheQxotvFvXW4VI2
Authenticate: false
Result:
Simulated read denied, due to : "allow get: if ((request.auth != null) && (id == request.auth.uid));" This makes total sense.
2) Using the Rules playground
Simulation type: get
Location: /Member/PIijFvYPXQPSlheQxotvFvXW4VI2
Authenticate: true
Provider: Google
Firebase UID: QM6tGSkA2SRSPjHGIurDiARd6zv1
Result:
Simulated read denied, due to : "allow get: if ((request.auth != null) && (id == request.auth.uid));" This makes total sense.
3) Using the Rules playground
Simulation type: get
Location: /Member/PIijFvYPXQPSlheQxotvFvXW4VI2
Authenticate: true
Provider: Google
Firebase UID: PIijFvYPXQPSlheQxotvFvXW4VI2
Result:
Simulated read allowed. This makes total sense.
4) In flutter,
Codesnippet 1:
await AbstractRepositoryS
ingleton.singleton
.userRepository()
.signInWithGoogle()
.then((value) async {
await AbstractRepositorySingleton.singleton
.memberRepository().valuesList().then((event) {
print("Success");
});
Codesnippet 2 (implementation of AbstractRepositorySingleton.singleton.memberRepository().valuesList())
Future<List<MemberModel>> valuesList() async {
return await MemberCollection.getDocuments().then((value) {
var list = value.documents;
return list.map((doc) => _populateDoc(doc)).toList();
});
}
Codesnippet 3:
final CollectionReference MemberCollection = Firestore.instance.collection('Member');
Action:
I run code snippet 1, which requests me to login onto my phone. I login with firebase UID = PIijFvYPXQPSlheQxotvFvXW4VI2, then a query for ALL members is run.
Result:
This fails, which ones again all makes sense.
My observations that make NO sense:
Codesnippet 1:
await AbstractRepositorySingleton.singleton
.userRepository()
.signInWithGoogle()
.then((value) async {
await AbstractRepositorySingleton.singleton
.memberRepository().values().listen((event) {
print("Success");
});
Codesnippet 2 (implementation of AbstractRepositorySingleton.singleton.memberRepository().values())
Stream<List<MemberModel>> values() {
return MemberCollection.snapshots().map((snapshot) {
return snapshot.documents
.map((doc) => _populateDoc(doc)).toList();
});
}
Action:
I run code snippet 1, which requests me to login onto my phone, I login with user with firebase ID = PIijFvYPXQPSlheQxotvFvXW4VI2,
then a LISTEN for ALL members is executed.
Result
This succeeds and the _populateDoc allows me to read all data fields from all members.
Basically it seems this mechanism allows me to read the entire collection, even for data where I should have no access to.
However, I'm sure I'm doing something wrong... but what.
Please advise. Many thanks.

One thing I noticed is an error occurring during the listen() method:
com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
Still for some reason I had 4 hits in the breakpoint in _populateDoc where I could happily read the member data from the doc.
After given this a bit extra thoughts, I've come to the conclusion this might have to do with local caching of the firestore data on my phone.
So I tried another phone, one where this particular app never ran before, I got the same error and I had 1 hit in the breakpoint in _populateDoc.
It's starting to make a bit sense to me. However, if someone could confirm this. Is this somewhere documented?

Related

Inconsistent Behaviour of once()

I've been testing my code on Firebase Emulator. My code is supposed to retrieve the most recent posts from my RT database while paginating the data:
Future<Map<String, Map<String, dynamic>>?> fetchData({
int? startAfter,
int? endBefore,
bool syncData = false,
}) async {
Query query = _ref.child('posts').orderByChild('date');
if (syncData) {
await query.keepSynced(true);
}
if (startAfter != null) {
query = query.startAfter(startAfter);
}
if (endBefore != null) {
query = query.endBefore(endBefore);
}
query = query.limitToLast(15);
final event = await query.once(DatabaseEventType.value);
...
}
Also, I have a listener attached to the same location:
void listenChanges(int startAfter) {
final reference = _ref
.child('posts')
.orderByChild('date')
.startAfter(startAfter)
.limitToLast(1);
reference.onChildAdded.listen((event) {
...
});
}
The problem is the inconsistent behavior of once(). I know it's supposed to read the data from the local cache immediately, and only if the data is not available there, it will check on the server. However, I get different results every time. Sometimes, I'm able to receive the most recent data even though I do not enable keepSynced. Sometimes, I'm not even when I enable it.
Does once() retrieve the updated value if a listener is already attached to the same location? If so, the odd thing is that I call the listener after calling once(), but it still "sometimes" gets the updated value. Is that because the local cache expires after some time?
Edit:
Here is my security rules on Firebase Emulator:
"posts": {
".read": "auth != null && (query.limitToLast <= 15 || query.limitToFirst <= 15)",
"$id": {
".read": "auth != null",
".write": "auth != null"
}
}
Can query-based security rules be the reason behind the inconsistent behavior of once()?
As I commented, I'm not sure what the exact problem is, but if you think the problem is caused by the way once() and local persistence interact, consider using the newer get() method. This method reverses the logic and (if there is a connection to the database) it first gets the value from the server, and only then calls your callback.

How to display categories based on user role, Firebase Flutter

I have 2 kinds of users "d" and "p".
For them, there is a special collection - roles, which stores the user's uid and his role.
I'm trying to display a role and, based on the role, display the corresponding categories.
However, I am getting the following error.
The argument type 'Future<QuerySnapshot<Object?>>? Function(QuerySnapshot<Map<String, dynamic>>)' can't be assigned to the parameter type 'FutureOr<QuerySnapshot<Object?>> Function(QuerySnapshot<Map<String, dynamic>>)' because 'Future<QuerySnapshot<Object?>>?' is nullable and 'Future<QuerySnapshot<Object?>>' isn't.
What alternatives do I have to implement the same? The action takes place in a function that is then called and processed in the FutureBuilder.
getCategoryData(String _collection, {bool forMap = false}) {
FirebaseFirestore.instance.collection('roles').where('uid', isEqualTo: FirebaseAuth.instance.currentUser!.uid).get().then((value){
for (var element in value.docs) {
if(forMap == false && element.data()['role'] == 'd'){
Future<QuerySnapshot> _snapshot = FirebaseFirestore.instance.collection(_collection).where('for_d', isEqualTo: true).get();
return _snapshot;
}else if((forMap == false && element.data()['role'] == 'p') || (forMap == true)){
Future<QuerySnapshot> _snapshot = FirebaseFirestore.instance.collection(_collection).get();
return _snapshot;
}
}
});
}
The problem here is that, there is a chance your getCategoryData function might return a null object.
If it goes over all the docs and none of them match the given two if conditions, a null value is returned by default.
Your database design might be such that this might never occur but the compiler is unaware of that.
You can fix this by returning an empty future after the for loop. The compiler won’t show this error anymore. Although I hope you’ve designed your database well that this condition will never occur.

Preventing multiple spurious calls to backend in Flutter

I have a service class in Flutter that fetches objects from the server using Rest API calls. It caches those objects and returns cached objects if the UI tries to fetch the same object again. Like this:
Future<User?> getUser(int userid) async{
User? u = _userCache.get(userid);
if(u == null) {
ApiResponse<User> ar = await _restapi.getUser(userid);
if (ar.successful && ar.data != null) {
u = ar.data!;
_userCache.add(userid,u);
}
}
return u;
}
Now, the problem that I am having is as follows: The UI requires the same User object multiple times while building a screen and so, it invokes getUser(id) multiple times. Ideally, it should return the cached User for the second and subsequent requests but the first request to the server hasn't completed yet and the cache does not have the User object (even though it will have the object a few seconds later). This causes the app to invoke the Rest API calls multiple times.
I want to basically have just one active/pending call to the RestAPI. Something like this:
Future<User?> getUser(int userid) async{
>>>> CHECK whether a call to backend is pending. If so, then wait here.<<<<
User? u = _userCache.get(userid); <--This call should now succeed
...same as before
}
Is this how it should be done? What is the right way to achieve this?
====== UPDATE =================
Based on #dangngocduc suggestion my getUser method now looks like this:
Future<User?> getUser(int userid) async{
User? u = _userCache.get(userid);
if(u == null) {
await userLock.synchronized( () async {
u = _userCache.get(userid);
if(u == null) {
ApiResponse<User> ar = await _uome.getUser(userid);
if (ar.successful && ar.data != null) {
u = ar.data!;
_userCache.add(userid, u!);
}
}});
}
return u;
}
Your issue is on first time, when we don't have cache and have multiple request for a user.
I think you can try with this package:
https://pub.dev/packages/synchronized
Let add your logic on synchronized.

Firestore emulator and request.auth.email generating a 'Property email is undefined on object' error

Thanks to the https://www.youtube.com/watch?v=VDulvfBpzZE great resource, I installed firestore emulator and my testing environment, everything went fine except when I came to test the following rule:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function invitationIsForMe(destEmail) {
return request.auth.email == destEmail;
}
match /invitations/{inviteId} {
allow read, delete: if invitationIsForMe(resource.destEmail);
}
}
Which generate an error in the test:
FirebaseError:
Property email is undefined on object.
The test is as follow:
const me = {uid: 'myuserid', email: 'me#foo.bar'};
const getDb = (auth) => {
return firebase.initializeTestApp({projectId: YORGA_PROJECT_ID, auth}).firestore();
}
describe('firestore structure and rules', () => {
it("Can read an invitation sent to me", async () => {
const db = getDb(me);
const testDoc = db.collection('invitations').where('destEmail', '==', me.email);
await firebase.assertSucceeds(testDoc.get());
})
}
If I test it with request.auth.uid, it's working (but that's not the logic I'm looking for).
So my question: is it a firestore emulator limitation ? I hope it's not and it's just me, but if it's the case I think the example given with a uid and an email for auth in initializing the testing base is a bit confusing so it's worth a warning.

Firestore rules - update blocks create?

I have the following rule set on my firestore:
match /names/{nameId} {
allow read: if request.auth.uid != null;
allow update: if request.auth.token.creator == true && request.writeFields.size() == 1 && ('images' in request.writeFields);
allow create: if request.auth.token.creator == true;
}
However, when I'm trying to create a new object using java(kotlin) client:
val data = hashMapOf(
"name" to name
)
mFirestore.collection(COLLECTION_USERS).add(data)
.addOnSuccessListener { doc ->
Log.i(TAG, "On New Document: " + doc.id)
}.addOnFailureListener { ex ->
Log.w(TAG, "Failed adding document: ", ex)
}
If I change the rules to make update same as create:
match /names/{nameId} {
allow read: if request.auth.uid != null;
allow update: if request.auth.token.creator == true;
allow create: if request.auth.token.creator == true;
}
The java code will now work.
What am I missing here? why is update blocking creates?
p.s.
Just to make sure, I'm removing all documents from my collection in firestore before running the code. Either way though, "add" method generates a random id, so it shouldn't matter.