Firestore rules: unable to verify role in subcollection - flutter

I'm having difficulties to setting up my security rules
Data Structure
-- books (collection)
-- bookId (autogenerated Id - doc)
-- {chapter: text}, {chapter: text},
userBooks
-- email (email of user logged in - doc)
-- books (sub collection)
-- bookId (referencing bookId doc on collection book)
-- role: admin or read
The use case is as follow, a user with admin role is the only one permitted to share a book, allowing him to add an entry in books of another user.
created the function trying to achieve that but it does not pass successfully
I'm am logged in as user with email userWantsToShare#gmail.com. When user tries to share his book to addUser#gmail.com I make the following request:
userWantsToShare#gmail.com making the request to write under addUser#gmail.com collection.
final role = {'role': 'edit'};
await FirebaseFirestore.instance
.collection('userBooks')
.doc("addUser#gmail.com")
.collection('books')
.doc('9KHYZJVBY3BNAlYPYYoA')
.set(role);
When coming to firebase
this should translate to /userBooks/addUser#gmail.com/books/9KHYZJVBY3BNAlYPYYoA
Data {'role': 'edit'}.
match /userBooks/{emailId}/books/{bookId} {
allow write: if isSharedEmail(bookId);
}
//here im verifying the user that wants to share has admin role and therefore is authorised to write in another user book subcollection
function isSharedEmail(bookId){
//this should translate to /userBooks/userWantToShare#gmail.com/books/9KHYZJVBY3BNAlYPYYoA
get(/databases/$(database)/documents/userBooks/$(request.auth.token.email)/books/$(bookId)).data.role == "admin" ;
}
As userWantToShare#gmail.com has under that path admin role it should allow the insertion. But most likely I'm omitting something as it does not work at all.
What m I missing?
Thanks in advance

Ok, for anyone making the same dumb mistake as I did. When making the call to the function isSharedEmail was outside the context.
Meaning that by moving the function to the scope inside the context was enough to make it work
Before: not working
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /userBooks/{email} {
allow write: if ( isAppShared())
allow read: if true;
}
}
}
function isAppShared() {
return get(/databases/$(database)/documents/userBooks/$(request.auth.token.email)).data.bookId == request.resource.data.bookId;
}
After: Working
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /userBooks/{email} {
allow write: if ( isAppShared())
allow read: if true;
}
function isAppShared() {
return get(/databases/$(database)/documents/userBooks/$(request.auth.token.email)).data.bookId == request.resource.data.bookId;
}
}
}

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.

How to lock updating/deleting a collection created by a user to the same user who created the collection in firestore (without using cloud Functions)?

I have the following model and rules... the first part of the equation is working fine a user can CRUD his own collection that he created, and it has the the same id as the creator.
the issue lies in the other collection that created by that user since it has a different id (an autogenerated id by firestore and I want to lock this collection to by modified only by the user that created it) any idea how to do this ?
Collections Model
// Data // doc id === uid
subDataId: null,
createdAt: Timestamp.now(),
// Sub Data // doc id === generated automatically by fb refrenced in the data collection.
online: false,
createdAt: Timestamp.now(),
// Firebase rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /Data/{userId} {
allow read , create: if request.auth != null
allow update, delete : if request.auth.uid == userId
}
match /SubData/{SubDataId} {
allow read , create: if request.auth != null
// allow update : if request.auth.uid == ?????????
}
}
}
Write a rule that requires the user to provide their own user ID as a field of the document being created (see the documentation about requiring that a field is provided). Make sure the code in your app does exactly that. Also write a rule that checks if the user id of the user updating the document also matches the field that was previously written by that user.
You might want to read this other question as well.

How do I achieve this security logic in Firebase FireStore?

The illustration below shows the logic of security rules that I need to apply for my firebase firestore.
Description
My app allows user authentication, and I manually activate client accounts. Upon activation, they can add users to their database collection, as well as perform operations on their database. I have illustrated each client as 'Organization' below, and each has multiple users which should only be able to access specific parts of the database collections/documents.
Each organization has an admin user who has full access over the database collection of that particular organization. Each user (from each organization) can access the node 'Elements', as well as their own UID-generated docs and collections only.
It seems like I need custom claim auths to achieve that. I wonder if some alternatives exist, like adding some specific security rules in my fireStore to make this work, or any other alternatives besides the firebase admin sdk tools, as it's time consuming and I'm not an expert backend developer.
Other Details
I use flutter. My app allows clients to authenticate and create their database collection. Clients add users (as team members) with different roles (which affect what collection/document they can access)
This security rules logic is the main thing I'm stuck on right now.
I highly appreciate suggestions and examples that might shed light on my way of achieving that.
illustration
My FireStore security rules right now
One possible solution:
Each organisation should contain a list of strings (userIds), and only users with userId in this list can access the Organisation collection and docs.
Database structure:
organisation_1:
userIds (field containing list of user ids - []<String>):
adminId (field containing admin id - String):
admin (collection):
users (collection):
elements (collection):
premium (collection):
organisation_2:
Security rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isLoggedIn() {
// only true if user is logged in
return request.auth != null;
}
match /organisation/{organisationId} {
function prefix() {
return /databases/$(database)/documents/organisation/$(organisationId);
}
function isAdmin() {
// only true if admin
return isLoggedIn() && request.auth.uid == get(/$(prefix())).data.adminId;
}
function isUser() {
// only true if user
return isLoggedIn() && request.auth.uid in get(/$(prefix())).data.usersId;
}
function isDataOwner(dataId) {
// only true if user is admin or userId is the document id.
// this rule should allow each user access to their own UID-
// generated docs and collections only
return isLoggedIn() && (isAdmin() || dataId == request.auth.uid);
}
// since userIds list is organisation data, we should prevent any
// user from editing it (or only allow admin to edit it).
// if you are using cloud function to update userIds list, set this
// to false. Cloud function does not need access.
allow write: if isAdmin();
allow read: if true;
match /Elements/{elementsId=**} {
// allow access to the entire Elements collection and
// subcollections if isAdmin or isUser.
allow read, write: if isAdmin() || isUser();
}
match /settings/{userId} {
// allow access only if document id is your userId
allow read, write: if isDataOwner(userId);
}
match /adminDocs/{docId} {
// only allow admin
allow read, write: if isAdmin();
}
}
}
}
Then you can use a cloud function to keep your userIds list up to date. Example:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const db = admin.firestore();
exports.onCreate = functions.firestore
.document("/organisation/{organisationId}/users/{userId}")
.onCreate((_, context) => {
const params = context.params;
const organisationId = params.organisationId;
const userId = params.userId;
const data = {
userIds: admin.firestore.FieldValue.arrayUnion(userId),
};
return db.doc(`/organisation/${organisationId}`)
.set(data, { merge: true });
});
exports.onDelete = functions.firestore
.document("/organisation/{organisationId}/users/{userId}")
onDelete((_, context) => {
const params = context.params;
const organisationId = params.organisationId;
const userId = params.userId;
const data = {
userIds: admin.firestore.FieldValue.arrayRemove(userId),
};
return db.doc(`/organisation/${organisationId}`)
.set(data, { merge: true });
});
You can avoid this cloud function by simply adding userid to userId list when admin creates new user. But cloud function is cleaner (Use it).
UPDATE
$(database) is the name of your firestore database.
{database} (line 3 in my security rules) tells rules to save the actual name of database into database variable.
prefix() returns the path to the organisation document.
If a user tries to access his document in this path organisation/12345/users/67890, then $(database) is default and prefix() returns /databases/default/documents/organisation/12345/
You can go to firestore docs to see how $(database) and path (prefix()) is being used.

Get role in every rule Firebase security

Hello so I have a role in my user collection and I wanted to write the rules depending on the role so if the role is the teacher you can have access to a little more stuff than the parent role. Now my question is there a possibility that I can access the role and use it for every collection, not only the user collection. Like a function that just checks every time what your role is?
I'm doing this for the first time and I'm not pretty sure if I understand everything right, so far.
This is what I have in my rules so far:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && ((getRole() in array) || rsc.data.openWorld == true);
}
function getRole() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'pädagoge';
}
match /posts/{userPosts} {
allow read: if isSignedIn();
allow create: if isOneOfRoles(resource, ['pädagoge']);
}
match /messages/{messages} {
allow read, write: if isSignedIn();
}
}
}
UPDATE
I've tried your security rules in the Firestore "Rules playground". You can see below that you need to do isOneOfRoles(request.resource, ['pädagoge']);: with only resource, the rule engine cannot check the value of the field openWorld beacause the future state of the document is contained in the request.resource variable, not in the resource one. See the doc for more details.
You also need to have a corresponding user in the users collection with a role field with the value pädagoge: in my example the user's UID is A1 (i.e. the ID of the Firestore doc in the users collection). See on the second and third screenshots below how we use this value in the Firebase UID field in the "Rules playground" simulator.
(same screenshot as above, only the left pane was scrolled down to show the Firebase UID field)

Why firebase_firestore security rules not worked for document read

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.