I'm struggling with the Firestore security rules. This is the struct for my users collection and I also have structures for other collections: machines, logs, and photos. I want to set up the rule such that data can be accessed only when the request.auth.uid == user.user_UUID
I presume my syntax must not be correct in my rule because with what I have below, I'm not able to read or write any data after I log in with me app. thinking that maybe the brackets around the user_UUID were the problemI tried changing 'match users/{user_UUID}' to 'match users/user_UUID' but that didn't work. I also tried removing the 'match /{document=**} '
each of my collections have a user_UUID field and I want security to protect such that only the respective authenticated user can access that data.
import Foundation
struct User: Codable {
var email: String?
var userUUID: String?
}
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
match /users/{user_UUID} {
allow read, update, delete: if request.auth != null && request.auth.uid == user_UUID;
allow create: if request.auth != null;
}
}
}
}
I think I am seeing my problem. When I create a new authenticated user, in my user collection the UUID is being captured as a field and the documentID of the user itself is getting it's own identifier which I'm capturing separately in the user as property userDocumentID.
let db = Firestore.firestore()
let usersRef = db.collection("users")
let newUser = usersRef.document()
let docID = newUser.documentID ///*** THIS MUST BE WHERE PROBLEM LIES
let userData = [
//"date": "",
"user_UUID": result!.user.uid,
"userDocumentID": docID,
"nameLast": lastName,
"nameFirst": firstName,
"email": email,
"unlimited": false,
"password": password] as [String : Any]
newUser.setData(userData)
self.user_UUID = result!.user.uid
}
I've been doing more reading and am wondering if my database structure is inherently wrong. I have 4 different collections none of which are nested within the other. I am using a key value from each to link them together.
So I have machine.user_UUID to link the document to the correct record.
But I'm seeing that perhaps I should have a path such as user/machines
Could that be the source of my problems?
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.
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.
The Problem
I have seen this question several times (also in the context of the Firebase Real-Time Database), but I haven't seen a convincing answer to it. The problem statement is fairly simple:
How can (authenticated) users choose a username that hasn't been taken yet?
First of all, the why: After a user authenticates, they have a unique user ID. Many web-apps, however, let the user choose a "display name" (how the user wants to appear on the website), in order to protect the users personal data (like real name).
The Users Collection
Given a data structure like the following it is possible to store a username along with other data for each user:
/users (collection)
/{uid} (document)
- name: "<the username>"
- foo: "<other data>"
However, nothing prevents another user (with a different {uid}) to store the same name in their record. As far as I know, there is no "security rule" that allows us to check if the name has already been by another user.
Note: A client side check is possible, but unsafe as a malicious client could omit the check.
The Reverse Mapping
Popular solutions are creating a collection with a reverse mapping:
/usernames (collection)
/{name} (document)
- uid: "<the auth {uid} field>"
Given this reverse mapping, it is possible to write a security rule to enforce that a username is not already taken:
match /users/{userId} {
allow read: if true;
allow create, update: if
request.auth.uid == userId &&
request.resource.data.name is string &&
request.resource.data.name.size() >= 3 &&
get(/PATH/usernames/$(request.resource.data.name)).data.uid == userId;
}
and to force a user to create a usernames document first:
match /usernames/{name} {
allow read: if true;
allow create: if
request.resource.data.size() == 1 &&
request.resource.data.uid is string &&
request.resource.data.uid == request.auth.uid;
}
I believe the solution is half-way there. However, there are still a few unsolved issues.
Remaining Issues / Questions
This implementation is quite involved already but it doesn't even solve the problem of users that want to change their user name (requires record deletion or update rules, etc.)
Another issue is, nothing prevents a user from adding multiple records in the usernames collection, effectively snatching all good usernames to sabotage the system.
So to the questions:
Is there a simpler solution to enforce unique usernames?
How can spamming the usernames collection be prevented?
How can the username checks be made case-insensitive?
I tried also enforcing existence of the users, with another exists() rule for the /usernames collection and then committing a batch write operation, however, this doesn't seem to work ("Missing or insufficient permissions" error).
Another note: I have seen solutions with client-side checks. BUT THESE ARE UNSAFE. Any malicious client can modify the code, and omit checks.
#asciimike on twitter is a firebase security rules developer.
He says there is currently no way to enforce uniqueness on a key on a document. https://twitter.com/asciimike/status/937032291511025664
Since firestore is based on Google Cloud datastore it inherits this issue. It's been a long standing request since 2008.
https://issuetracker.google.com/issues/35875869#c14
However, you can achieve your goal by using firebase functions and some strict security rules.
You can view my entire proposed solution on medium.
https://medium.com/#jqualls/firebase-firestore-unique-constraints-d0673b7a4952
Created another, pretty simple solution for me.
I have usernames collection to storing unique values. username is available if the document doesn't exist, so it is easy to check on front-end.
Also, I added the pattern ^([a-z0-9_.]){5,30}$ to valide a key value.
Checking everything with Firestore rules:
function isValidUserName(username){
return username.matches('^([a-z0-9_.]){5,30}$');
}
function isUserNameAvailable(username){
return isValidUserName(username) && !exists(/databases/$(database)/documents/usernames/$(username));
}
match /users/{userID} {
allow update: if request.auth.uid == userID
&& (request.resource.data.username == resource.data.username
|| isUserNameAvailable(request.resource.data.username)
);
}
match /usernames/{username} {
allow get: if isValidUserName(username);
}
Firestore rules will not allow updating user's document in case if the username already exists or have an invalid value.
So, Cloud Functions will be handling only in case if the username has a valid value and doesn't exist yet. So, your server will have much less work.
Everything you need with cloud functions is to update usernames collection:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
exports.onUserUpdate = functions.firestore
.document("users/{userID}")
.onUpdate((change, context) => {
const { before, after } = change;
const { userID } = context.params;
const db = admin.firestore();
if (before.get("username") !== after.get('username')) {
const batch = db.batch()
// delete the old username document from the `usernames` collection
if (before.get('username')) {
// new users may not have a username value
batch.delete(db.collection('usernames')
.doc(before.get('username')));
}
// add a new username document
batch.set(db.collection('usernames')
.doc(after.get('username')), { userID });
return batch.commit();
}
return true;
});
Create a series of cloud functions that are triggered whenever a document is added, updated, or deleted in the users table. The cloud functions will maintain a separate lookup table named usernames, with document ids set to the usernames. Your front-end app can then query the usernames collection to see if a username is available.
Here is TypeScript code for the cloud functions:
/* Whenever a user document is added, if it contains a username, add that
to the usernames collection. */
export const userCreated = functions.firestore
.document('users/{userId}')
.onCreate((event) => {
const data = event.data();
const username = data.username.toLowerCase().trim();
if (username !== '') {
const db = admin.firestore();
/* just create an empty doc. We don't need any data - just the presence
or absence of the document is all we need */
return db.doc(`/usernames/${username}`).set({});
} else {
return true;
}
});
/* Whenever a user document is deleted, if it contained a username, delete
that from the usernames collection. */
export const userDeleted = functions.firestore
.document('users/{userId}')
.onDelete((event) => {
const data = event.data();
const username = data.username.toLowerCase().trim();
if (username !== '') {
const db = admin.firestore();
return db.doc(`/usernames/${username}`).delete();
}
return true;
});
/* Whenever a user document is modified, if the username changed, set and
delete documents to change it in the usernames collection. */
export const userUpdated = functions.firestore
.document('users/{userId}')
.onUpdate((event, context) => {
const oldData = event.before.data();
const newData = event.after.data();
if ( oldData.username === newData.username ) {
// if the username didn't change, we don't need to do anything
return true;
}
const oldUsername = oldData.username.toLowerCase().trim();
const newUsername = newData.username.toLowerCase().trim();
const db = admin.firestore();
const batch = db.batch();
if ( oldUsername !== '' ) {
const oldRef = db.collection("usernames").doc(oldUsername);
batch.delete(oldRef);
}
if ( newUsername !== '' ) {
const newRef = db.collection("usernames").doc(newUsername);
batch.set(newRef,{});
}
return batch.commit();
});
This works for me efficiently whereby username must be unique. I am able to add and edit usernames without duplicates.
NOTE: username must be in lowercase always, this eliminates duplicates caused by case sensitivity.
Create users collection:
/users (collection)
/{uid} (document)
- name "the username"
Create usernames collection:
/usernames (collection)
/{name} (document)
- uid "the auth {uid} field"
Then in firestore use the following rules:
match /databases/{database}/documents {
match /usernames/{name} {
allow read,create: if request.auth != null;
allow update: if
request.auth.uid == resource.data.uid;
}
match /users/{userId}{
allow read: if true;
allow create, update: if
request.auth.uid == userId &&
request.resource.data.name is string &&
request.resource.data.name.size() >=3 &&
get(/databases/$(database)/documents/usernames/$(request.resource.data.name)).data.uid == userId;
}
}
I store the usernames in the same collection where each username occupies a unique document ID. That way the username which already exists will not be created in the database.
One possible solution is to store all usernames in a single document's usernames field and then permit only additions to that document using sets in Rules:
match /users/allUsernames {
function validateNewUsername() {
// Variables in functions are allowed.
let existingUsernames = resource.data.usernames;
let newUsernames = request.resource.data.usernames;
let usernameToAdd = newUsernames[newUsernames.size() - 1];
// Sets are a thing too.
let noRemovals = existingUsernames.toSet().difference(newUsernames.toSet()).size() == 0;
let usernameDoesntExistYet = !(usernameToAdd in existingUsernames.toSet());
let exactlyOneAddition = newUsernames.size() == existingUsernames.size() + 1;
return noRemovals && usernameDoesntExistYet && exactlyOneAddition;
}
allow update: if request.resource.data.keys().hasOnly(['usernames']) && validateNewUsername();
}
If you wanted to make a mapping from username -> uid (for validating other parts of the ruleset) this is possible in a single document too. You can just take the keyset of the document and do the same set operations as above.
This answer addresses your second concern about adding multiple records in the usernames collection. I'm not sure if this is the best method, but I believe a possible approach to prevent a given user from creating multiple username documents is writing an onCreate cloud function which checks if the user has an existing username document when a new username document is created. If the user does, then the cloud function can delete this document to prevent any malicious username parking.
Store the max integer user id used in the database in another collection. Query that collection everytime to find the max user id. You can even store other max ids in this collection. It can look something like this:
MaxIDCollection:
maxStudentIDDocument={ maxID: 55 } //lets say the max user id in db is 55
maxCourseIDDocument={ maxID: 77 }
Make sure to update the maxIDs everytime you add a new Student or Course.
If in future you add a new Student then by querying this collection you can know "if 55 is max then the new Student should get 56 as id."
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
}
}
}
}