Firestore Security Rules get() call on list operation - firebase

I'm having some trouble finding the right Firestore security rules to match my use case.
The collection type is called Party. There are subcollections that are mostly irrelevant. Here's an example top-level record:
{
partyName: "Foo",
members: {
uid123: {
member: true
}
}
}
I have the following simplified security rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
allow read, write: if false;
match /parties/{partyId} {
// Define a helper
function isPartyMember() {
return request.auth != null &&
get(/databases/$(database)/documents/parties/$(partyId))
.data.get(['members', request.auth.uid, 'member'], false) == true;
}
// Top level collection
allow read, write: if isPartyMember();
// Subcollection prevents using "resource" variable in shared helper.
match /subcollection/{subId} {
allow read: if isPartyMember();
}
}
}
}
I am issuing the following query on web v9:
const resp = await getDocs(
query(
collection(this.firestore, "parties"),
where(`members.${this.auth.currentUser.uid}.member`, "==", true)
)
);
As far as I can tell, I am following the rules:
The security rules exactly match the query.
The function has access to the partyId variable.
This should only query for valid documents.
Notes:
When I test the read rules via Rules Playground, it seems to work as I'd expect.
Replacing the full get(...) call with resource actually works (!), but I can't do this because of the subcollection. It has to be an explicit reference.
Unfortunately, when I run the query I get Missing or insufficient permissions. What am I missing? Can you not secure docs with a get() operation in a list query?

For top level collections, you don't have to use get() to read data of current document. Try refactoring the rules as shown below:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
allow read, write: if false;
match /parties/{partyId} {
// Define a helper
function isPartyMember() {
return request.auth != null &&
get(/databases/$(database)/documents/parties/$(partyId))
.data.get(['members', request.auth.uid, 'member'], false) == true;
}
// Top level collection
// Use resource.data instead of get();
allow read, write: if request.auth != null && resource.data.get(['members', request.auth.uid, 'member'], false) == true;
// Subcollection prevents using "resource" variable in shared helper.
match /subcollection/{subId} {
allow read: if isPartyMember();
}
}
}
}
I am not totally sure about this behaviour but using resource.data instead of get() for top level collection should work.
From what I can see, if you attempt to use get() to get data of document being evaluated in a list operation, the rule fails (though it works when you are fetching a single document by ID). For example:
match /subcollection/{subId} {
// Rule fails
allow read: if get(/databases/$(database)/documents/parties/$(partyId)/subcollection/$(subId)).data.field == 'value';
// Rule passes
allow read: if resource.data.field == 'value';
// Rule passes
allow read: if isPartyMember(); // reads parent document
}

Related

Firestore Security Rules - are rules cascading downward?

I am using the first version of Firestore security rules:
rules_version = '1';
Also, I have the following rule:
service cloud.firestore {
match /databases/{database}/documents {
match /chats/{chatId} {
allow read: if <condition1>
allow write: if false;
match /messages/{document=**} {
allow read: if <condition2>
// allow write: if false;
}
}
}
}
As you can see, I have commented the write operation. Is the chat doc's write operation rule being passed to the match of messages documents? Do I need to explicitly write the condition for the write operation in the nested match? If not, if the write rule is not explicitly declared... will it be false by default?
Update
I have read here that
Security rules apply only at the matched path
so, we have to explicitly define the rules for the nested stuff... but, in the case of write: if false, if it is not declared, will it be false by default?
If you comment some rule, it won't work. Rules works like in CSS for example. The last rule matters most. Here is a small example how you should secure dataase:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// {document=**} is equal to all collections and documents in whole database.
// including nested ones, so use it wise.
match /{document=**} {
allow read, write: if false;
}
// Above i restrict access to whole database but here users
// can do something in bookings collection.
// They can make documents in this collection but cannot
// make nested collections because of rule above.
match /bookings/{docId} {
allow read: if resource.data.uid == request.auth.uid || isAdmin()
allow update: if resource.data.uid == request.auth.uid || isAdmin()
allow create: if request.auth != null
}
match /app/{document} {
allow read: if true;
allow write: if false;
}
}
}
function isAdmin() {
return request.auth.token.admin == true;
}

Firestore allow read if collection name matches

I'm trying to implement what I thought was a basic security rule in Cloud Firestore, namely to allow read access to a specific collection.
service cloud.firestore {
match /databases/{collectionName}/documents {
match /{document=**}{
allow read : if collectionName=="metadata";
}
}
}
so in the rules playground, the query for /metadata/status gets denied, however, if I switch the operator to != instead of ==, it allows any query for any collection, not just the ones that aren't metadata. Help?
The placement of that wildcard is incorrect. The collectionName would be name of the database which is (default) for default database and hence "(default)" == "metadata" returned false. Try the following rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{collectionName}/{doc}{
allow read : if collectionName == "metadata";
}
}
}
Here collectionName would be name of collection being accessed.
This rule however will be applied for all collections. If you want to add that rule for 'metadata' collection only then you can add a separate rule for that:
match /metadata/{doc} {
allow read: if true;
}
if you want to set a rule on only a specific document, E.g: Inbox:
service cloud.firestore {
match /databases/{database}/documents {
// Allow public read access, but only authorized users can write
match /{document=**} {
allow read: if true
allow write: if (request.auth.uid != null);
}
match /Inbox/{document=**} {
allow read,write: if true
}
}
}

How to check if document exists in firestore security rules list request?

I have two root collections, users and workspaces. A user document has a workspaces array with the workspace document IDs the user is a part of. Below is the security rule I try to use. I had two approaches, one is to get the user document from the root collection and check its workspaces array for the workspaceId the other is to check for existance of the userId in the members subcollection. Both end up throwing the same error: FirebaseError: Null value error. for 'list' # L15
If I separate the allow read into allow get and allow list and just write true for list it fixes the error but obviously I want to restrict the access so users can only get their own workspaces (where they are members).
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /workspaces/{workspaceId} {
allow read: if isLoggedIn() && userIsInWorkspace(workspaceId);
}
function isLoggedIn() {
return request.auth != null && request.auth.uid != null;
}
function userIsInWorkspace(workspaceId) {
return exists(/databases/$(database)/documents/workspaces/$(workspaceId)/members/$(request.auth.uid));
}
}
}
Approach with get:
function userIsInWorkspace(workspaceId) {
let workspacesOfUser = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.workspaces;
return workspaceId in workspacesOfUser;
}
UPDATE:
I tried implementing it with custom claims like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if isLoggedIn() && request.auth.uid == userId;
}
match /workspaces/{workspaceId}/{document=**} {
allow read, write: if isLoggedIn() && userIsInWorkspace(workspaceId);
}
function isLoggedIn() {
return request.auth != null && request.auth.uid != null;
}
function userIsInWorkspace(workspaceId) {
return workspaceId in request.auth.token.workspaces;
}
}
}
So my concept was that I have users and workspaces as rootcollections and workspaces has subcollections such as teams and reports and so on. I still get the same null value error. When this comes up is in the above mentioned error so list requests does the error. My use case is that when you are logged out from the app the workspace slug that is stored on every workspace entry in firestore gets added az a query parameter so the user can be redirected back to the exact workspace. To make this happen I do a list request so basically I query the workspaces collection where the slug is the given slug from the url.
const workspaceSnapshot = await db
.collection('workspaces')
.where('slug', '==', this.$route.query.slug)
.limit(1)
.get()
This request creates my error but from this I cannot make out anything. I would suppose that when I give a condition for reads and writes that includes get and list as well.
Queries are case-sensitive. You said you had two root collections, namely Users and Workspaces, but you are querying against users and workspaces. This will not yield results. Try changing your query:
function userIsInWorkspace(workspaceId) {
let workspacesOfUser = get(/databases/$(database)/documents/Users/$(request.auth.uid)).data.workspaces;
return workspaceId in workspacesOfUser;
}

Firestore rules access to parent document

So i'm making an app with a friends system and trying to set up rules for firebase to handle reads & writes if the users is friends or not.
I'm very stuck at a particular call that i just don't have any idea on how to make.
My firestore is structured as follows:
users/userUID/places/documentsofplaces
each userdocument have some fields of the usual information, name, username, etc. and an array of friendsUID.
I have managed to get the first part down, that a user can only read and write if it's UID matches the documentUID, and looking in the friendslist a friend can only read but not write.
The next part, in the places collection, just throws me off, how can i get the parent document and compare the userUID to a UID in the friendslist?
This is what i have so far:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
// Allow write and read if user, and read if friend
allow write: if isUser(userId);
allow read: if isUser(userId) || isFriend();
function isUser(userId) {
return (request.auth.uid == userId);
}
function isFriend() {
return (request.auth.uid in resource.data.friendsList);
}
}
match /users/{userId}/places/{documents} {
allow write: if isUser(userId);
allow read: if isUser(userId) || isFriend(userId);
function isUser(userId) {
return (request.auth.uid == userId);
}
function isFriend(userId) {
return (request.auth.uid in get(/databases/$(database)/documents/users/userId.resource.data.friendsList));
}
}
}
}
Any help is greatly appreciated!
Your document get() should look more like this:
get(/databases/$(database)/documents/users/$(userId)).data.friendsList
You have to use variables with $(var) notation inside the document path. get() returns a Resoruce object with a data property. I suggest reading over the documentation for accessing other documents for more details.

Making a client-side request to satisfy a Firestore permissions rule that uses ACLs as arrays with hasAny()

Consider the following :
A Firebase auth user has an ACL (access control list)
user.roles = [ a, b, c ]
Each record in a collection must be secured with a list of roles that are permitted to perform read/write operations on the document:
resource.access.roles = [ c, d, e ]
If there is an intersection between the two arrays, the operation should be permitted.
function userHasAccess (resource) {
return getUser().roles.hasAny(resource.data.access.roles)
}
match /{collection}/{id} {
allow read: if userHasAccess(resource)
}
Note: in the Firebase console, getUser().roles is an array and resource.data.access.roles is an array and there IS an intersection between them. Consequently, IN THE CONSOLE, the request is permitted.
Question: how to query this collection from the client whilst satisfying the rule.
What I had expected is that this would work.
ref
.collection(collection)
.where('access.roles', 'array-contains-any', user.roles)
... would satisfy the rule and allow the read operation, but it does not. It throws standard permissions error, even though I can read the ref.collection(collection).doc(id) just fine.
FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
Would appreciate an example or better a ref to the docs where it shows how to formulate a query in the client that satisfies the hasAny() rule that the framework provides.
** EDIT: ** Added full details per Doug's recommendation:
The database is structured as follows:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAuthUser(userId) {
return request.auth.uid == userId;
}
function isTenantUser(env, tenantId) {
return get(/databases/$(database)/documents/envs/$(env)/usersMap/$(request.auth.uid)).data.tenant == tenantId
}
function getUser(env, tenantId) {
return get(/databases/$(database)/documents/envs/$(env)/tenants/$(tenantId)/users/$(request.auth.uid)).data
}
function userHasAccess (env, tenantId, resource) {
return resource.data.access.role in getUser(env, tenantId).roles
}
// note if set env here, have to pass through all fns to match path
match /envs/{env} {
match /usersMap/{userId} {
allow read: if isAuthUser(userId)
}
match /tenants/{tenantId} {
allow read: if isTenantUser(env, tenantId)
allow update: if getUser(env, tenantId).account.isAdmin
match /lists/{settingId} {
allow read, update: if isTenantUser(env, tenantId)
}
match /settings/{settingId} {
allow read: if true
allow update: if getUser(env, tenantId).account.isAdmin
}
// todo: users can't change admin flag or email
match /users/{userId} {
allow read: if true
allow create: if getUser(env, tenantId).account.isAdmin
allow update: if getUser(env, tenantId).account.isAdmin || request.auth.uid == userId
}
// todo: permissions on read/update
match /{collection}/{id} {
allow read, update, delete: if userHasAccess(env, tenantId, resource)
allow create: if true
}
}
}
}
}
Based on this, I want to secure access to the following url:
/databases/$(database)/documents/envs/$(env)/tenants/$(tenantId)/{collection}/{documentId}
eg:
/databases/$(database)/documents/envs/devleopment/tenants/shalom-shul-20AZ/cases/case-one
with ACLs where the user record is:
and the case record is:
Under these conditions, the rules evaluate as allowed in the firebase console, but the client-side query as posted returns denied error (even though i can read any individual doc just fine).

Resources