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.
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
}
I have a collection in which I am storing user requests in documents having documents ID as user's email. In the document, I am creating fields the key for which is being generated at client side.
Now, the problem that I am facing is that user can overwrite the existing field/request in the document if the key matches which I don't want to happen.
What I tried was to use this rule which unfortunately does not work
resource.data.keys().hasAny(request.resource.data.key();
So how can I achieve this?
Below are the screen shot of the firestore data and the current security rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /roles/{userId}{
allow read: if isSignedIn() && hasId(userId);
}
match /requests/{email} {
allow read, update: if isSignedIn() && hasMail(email)
}
//functions//
function hasMail (email) {
return request.auth.token.email == email;
}
function hasId (userId) {
return request.auth.uid == userId;
}
function isSignedIn () {
return request.auth != null;
}
function getUserRole () {
return get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.role
}
}
}
You can check if a resource already exists. Here an example:
allow write: if resource == null // Can create, not update
Use that to restrict any edit or update of the data. If you have additional rules you can granulate them to update, delete and create.
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;
}
I am struggling to access document references in the firestore rules. My database looks like this. (Simplified for brevity):
curriculum
session1
roles
admin
--- canEditContent
user
--- canEditContent
users
userid
--- role
roles/admin <document reference>
I want to access the admin permissions based on the document reference.
I have tried several ways however can't seem to get anywhere. This is my code so far
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function getUser() {
return get(/databases/$(database)/documents/users/$(request.auth.uid));
}
function getUserRole() {
let role = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role;
return get(role);
}
match /curriculum/{curriculum} {
allow write: if isSignedIn() && getUserRole().data.canEditContent;
}
match /users/{userId} {
allow read, update, delete, write: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
}
}
I have tried many ways and can't seem to solve it.
Thanks in advance
EDIT
Added screenshots below of collections
Users collection showing role as a document ref to a role document in the roles collection.
Roles collection
I can see two issues in your rules:
get needs the full document path, so your function getUserRole wont work. Try this instead:
function getUserRole() {
let role = getUser().data.role;
return get(path("/databases/" + database + "/documents/" + role));
}
Your rule uses the role canEditContent but the data you show uses editContent, is that on purpose?
As already mentioned please provide the complete set of data & query & rules, here we cant see the query you are using. Also note that you can use the Firestore emulator to get information on what rule is failing and where.
have one application where we have multiple communities and user that have roles to write message to it.
Each community has users under its own wing ({communityId}/meta_users/{uid}) I have written a function that access the roles under for user, I tried to go through the document but could not file any help.
I simulation I check that userRoleLevel will not allow to read even if the user has a role('admin') assigned to him.
please help me to correct it. I have pasted snippet of the rule below. let me know if need more info
function userRoleLevel(community){
return exists(/databases/$(database)/documents/communities/$(community)/meta_users/$(request.auth.uid)/roles/level)
&& get(/databases/$(database)/documents/communities/$(community)/meta_users/$(request.auth.uid)/roles/level).data.role;
}
service cloud.firestore {
match /databases/{database}/documents {
// All read is allowed for now.
match /{document=**} {
allow read;
}
match /communities/{community} {
allow write: if owner();
match /{category}/{message}{
allow write: if loggedIn()
&& (userRoleLevel(community) in ['owner', 'admin', 'writer']);
}
You have the function userRoleLevel() returning a Boolean instead of a String containing the role. You won't be able to use userRoleLevel(community) in ['owner', 'admin', 'writer'] unless you do the following.
function userRoleLevel(community){
if( exists(/databases/$(database)/documents/communities/$(community)/meta_users/$(request.auth.uid)/roles/level) ) {
return get(/databases/$(database)/documents/communities/$(community)/meta_users/$(request.auth.uid)/roles/level).data.role;
} else {
return false; // return something that won't match your roles
}
}
Finally, I found the solution to this problem.
Thing is the location of the function it is outside the scope of the database schema and it is using segment variable for the database scope.
The solution is to move this function inside the database schema.
service cloud.firestore {
match /databases/{database}/documents {
// All read is allowed for now.
match /{document=**} {
allow read;
}
// function should be located here <--------
function userRoleLevel(community){
return get(/databases/$(database)/documents/communities/$(community)/meta_users/$(request.auth.uid)/roles/level).data.role;
}
...
}
Hope, this helps others too.