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
}
}
}
Related
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
}
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).
I have a firestore collection which follows the structure outlined below
service cloud.firestore {
match /databases/{database}/documents {
//allow any authenticated user to read from ComicBook
match /MainCollection/{document} {
allow read: if request.auth != null;
allow create:.....
match/subcollection/{document} {
//here i would like to use something like a get to match a field in the doc above
allow write: if request.auth.uid == {document from above}.data.ownerID
}
}
How can i achieve this if possible?
You will have to use get() as described in the documentation to fetch any other document other than the one that's been matched by your rule. You will also have to give you inner and outer nested rules different wildcard names:
match /MainCollection/{maindoc} {
match/subcollection/{subdoc} {
allow write: if request.auth.uid ==
get(/databases/$(database)/documents/MainCollection/$(maindoc)).data.ownerID
}
}
Notice I used different wildcard names maindoc and subdoc so one variable does not mask the other.
I use a collection called "admin" in Firestore to define which users can write new documents (image below).
At moment, it is controled just by software. I would like to add rules to Firestore. I tried the rule below but it didn't work. What would be the correct rules in that case ?
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if request.auth != null;
allow write: if get(/admin/{anyDocument}).data.userId == request.auth.uid;
}
}
}
I'd recommend instead having a users collection with an admin field that can be set to true/false. Then you can do something like:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if request.auth != null;
allow write: if get(/users/${request.auth.uid}).data.admin == true;
}
}
}
As far i know this is not possible with your current database structure. Because the push key is not accessible in firestore rules unless it is with in the admin node.
One way is to save the admin with their uid as key like admin/userID/data...
now you can access it
allow write: if get(/databases/$(database)/documents/admin/$(request.auth.uid)).data.userId == request.auth.uid;;
I have the next firestore rule:
service cloud.firestore {
match /databases/{database}/documents {
match /{usuarios=**} {
allow write: if get(/usuarios/$(request.auth.uid)).data.level == 0 || get(/usuarios/$(request.auth.uid)).level == 0;
}
}
}
And I get "permission-denied" when I tried this query:
firebase.firestore().collection('usuarios').doc(uid).set({...});
This is my DB actually:
pd: I want to add info about new users in my db (by his UID)
That's going to be very expensive and inefficient since you pay per get request. Instead, you should write your rules as if you're defining the database's structure. Here's what I mean:
service cloud.firestore {
match /databases/{database}/documents {
match /usuarios/{uid} {
// Give write access to any field within the document who's id is the uid
// Add other validation as needed.
allow write: if uid == request.auth.uid
// Give admin access to anyone who's `level == 0`
// Make sure to add the `databases...` boilerplate
|| get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.level == 0;
// If necessary, give access to sub-collections.
// (We can't do it for uid or it will become a path object that we can't use as a string.)
/{sub=**} {
allow write: if uid == request.auth.uid;
}
}
}
}
If you'd like to see fully flushed-out rules that are doing something very similar, I have an open source exemple. Cheers!