Let's assume that there is 3 collections and they are at the same hierarchy level:
User
UserAndOtherCollectionRelationship
OtherCollection
I desire to grant access on "OtherCollection" records to the users that own that record or are related to it (only read access).
Understand "UserAndOtherCollectionRelationship" as
UserAndOtherCollectionRelationship: {
'userId': uid, //user id provided by Firebase Auth Service
'otherCollectionId': 000,
'roles': ['owner', 'reader', ...]
}
This is what I have:
match /databases/{database}/documents {
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
match /user/{userId} {
allow read, write: if(isSignedIn() && isUserOwner(userId));
}
match /userAndOtherCollectionRelationship/{userAndOtherCollectionRelationshipId} {
allow read: if(resource.data.userId == request.auth.uid && isSignedIn());
allow create: if(isSignedIn());
allow update, delete: if(resource.data.userId == request.auth.uid);
}
// Functions
function isSignedIn() {
return request.auth != null;
}
function isUserOwner(userId) {
return request.auth.uid == userId;
}
function isOtherCollectionOwner() {
return isUserOwner(getUserAndOtherCollectionRelationshipData().userId) && getOtherCollectionData().roles.hasAll(['owner']);
}
//This is the function that I believe that it's not working propertly
function getuserAndOtherCollectionRelationshipData() {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/{document=**}).data;
}
}
Considering that the client (the app) must create a filter (where clause) to get only the desired records, I could not find a way to do that with this schema too.
So I put the user roles as a field on the "otherCollection" record:
otherCollection: {
...,
'userAndRoles': {
'replaceByUID': ['owner', ...]
},
}
updated the security rule function to:
function isOtherCollectionOwner() {
return get(/databases/$(database)/documents/OtherCollection/$(otherCollectionId)).data.roles[request.auth.uid].hasAll(['owner']);
}
Here is the client call:
final querySnapshot = await firestore.collection('otherCollection')
.where('user.$userId', arrayContains: 'owner')
.where('otherCollectionId', whereIn: otherCollectionIdList)
.get();
What is the best solution?
Change the data model to...
Set a different security rule as...
When a user wants to access an another collection, we have to set a rule under that collection.
So when you create a document in a collection, you have to create the same id in the other collection and create a field called owner.
This field owner contains the uid of the person who created the document.
userAndOtherCollectionRelationshipId === otherCollectionId
UserAndOtherCollectionRelationship: {
'owner': uid
}
In this way, when a user try to read the document, we check if he is owner or not with isOwner(otherCollectionId, request.auth.uid) function with Collectionid, and the userId. In the function you check if the owner who created the document is the same who is trying to read the document.
You can do create a rule as following:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if isOwner(otherCollectionId, request.auth.uid);
allow create: if isSignedIn();
}
function isOwner(docId, userId) {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/$(docId)).data.owner == userId;
}
To solve the issue, I updated the data model removing the userAndOtherCollectionRelationship collection and add the owner attribute to the otherCollection.
Any other relationship would be added as an attribute to otherCollection.
So the otherCollection looks like this now:
otherCollection: {
owner: ["user_uid", "other_user_id"],
..., //other atributes
}
The security rules were updated to:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
function isOtherCollectionOwner() {
return ([request.auth.uid] in (resource.data.owner));
}
The security rules tests were updated to:
const myAuth = {uid: 'my_user_uid', email: 'my#mail.com'};
const MY_PROJECT_ID = "my_project_id";
function getAdminFirestore() {
return firebase.initializeAdminApp({projectId: MY_PROJECT_ID, auth: myAuth}).firestore();
}
function getFirestore(auth) {
return firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth}).firestore();
}
describe("MyApp", () => {
it("Can list if is owner", async () => {
const admin = getAdminFirestore();
const setupOtherCollection = admin.collection('otherCollection').doc('otherCollectionId');
await setupOtherCollection.set({'name': 'myOtherCollection', 'owner': [myAuth.uid]});
const db = getFirestore(myAuth);
const otherCollectionCollection = db.collection("otherCollection").where("owner", "array-contains", [myAuth.uid]);
await firebase.assertSucceeds(otherCollectionCollection.get());
});
});
Related
I have the following firestore security rules
match /users/{user} {
allow read: if request.auth != null;
allow create: if request.resource.id == request.auth.uid;
match /replies {
allow update: if request.auth != null;
}
}
my understanding from the firestore security rules doc is that the two rules are independent of each other. However, a logged in user gets a permission denied message when trying to update the /user/{user}/replies field. It doesn't matter if I nest the rule or not. It still gets denied. (it also doesn't work in Rules Playground in Firebase console)
what am I doing wrong?
my client code is as follows:
const processNewMessage = async (evt, newMessage) => {
myMessage.value = myMessage.value.trim();
if (evt.srcElement.textContent === "send") {
if (replying.value === true) {
const message = {
originalMessage: {
text: props.selectedMessage.text,
photoUrl: props.selectedMessage.photoUrl,
},
reply: {
user: uid,
userDisplayName: auth.currentUser.displayName,
userName: userName.value,
text: newMessage,
createdAt: Timestamp.now(),
g: { geohash: geohash.value, geopoint: myLocation },
photoUrl: photoUrl,
},
};
await updateDoc(doc(db, `users/${props.selectedMessage.user}`), {
replies: arrayUnion(message),
});
}
}
}
Security rules work on a document-level. Your code is trying to update the replies array in the users document, which is not allowed by any of your rules.
The match /replies in your rules applies to a replies subcollection under the users document. It has no effect on fields in the users document.
If you want the owner to update their entire document, and other signed in users to only update the replies field, you'll have to allow that in the rule on users.
Something like this:
match /users/{user} {
allow read: if request.auth != null;
allow create: if request.resource.id == request.auth.uid;
allow update: if request.auth != null &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["replies"])
;
}
For more on how this works, see the release notes for the map diff function.
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.
Pretty much no matter what I use for the get() request, getting Missing or insufficent permissions when logged in with a userID that is a "member":
function isSelf(userID) {
return request.auth != null && request.auth.uid != null && request.auth.uid == userID
}
function isMember(userID) {
return request.auth != null && request.auth.uid != null && get(/databases/$(database)/documents/'members'/$(request.auth.uid)).data.parent == userID
}
match /templates/{userID} {
allow read, write: if false
match /templates/{templateID} {
allow read: if isSelf(userID) || isMember(userID)
allow write: if isSelf(userID)
allow delete: if false
}
allow read: if isSelf(userID) || isMember(userID)
allow write: if isSelf(userID)
}
Have tried using get() with .data.parent and with .parent The member doc looks like this:
{
parent: 'USER_ID_OF_PARENT'
}
Call from the client app is:
export const getTemplate = async ({ userID, form }) => {
db.collection('templates').doc(userID).collection('templates').doc(form).get()
.then((doc) => {
})
.catch((err) => {
console.error(err)
})
}
Database structure is:
/templates/{userID} is a collection of docs with ids as userIDs that correspond to a doc with matching userID in /users/{userID}
/members/{memberID} is a collection of docs with ids as memberIDs, with a parent field with a string value SOME_USER_ID which matches a doc with userID SOME_USER_ID in /users/{userID}
Example:
/members/'MEMBER_1' doc:
{
name: 'Member 1',
parent: 'OWNING_USER_1'
}
/users/'OWNING_USER_1' doc:
{
name: 'Owning User 1',
parent: 'OWNING_USER_1'
}
/templates/'OWNING_USER_1' doc:
{
// no fields
}
/templates/'OWNING_USER_1'/templates/'FORM_1' doc:
{
name: 'Form 1'
}
With the following call:
getTemplate({
userID: 'OWNING_USER_1',
form: 'FORM_1'
})
When the authenticated user is OWNING_USER_1, the above call is successful (the isSelf() rule returns as true) and the found template document is returned
When the authenticated user is MEMBER_1, the above call gets Missing or insufficient permissions (the isMember() rule returns false)
Removed the quotes from around 'members' and this is now working correctly:
Replaced:
get(/databases/$(database)/documents/'members'/$(request.auth.uid)).data.parent
with:
get(/databases/$(database)/documents/members/$(request.auth.uid)).data.parent
I'm new to firestore and I try to get all resources from a group of users.
In my case I use the auth token to keep track of user groups (I will use this on cloud storage too).
The group object is similar to:
{"managers": { "user1_ID": "owner", "user2_ID": "client" } }
Then in auth token I set the group UID:
{ "groups": { "group1_ID": "owner", "group2_ID": "client" } }
Then in firestore rules I want to set something like this:
match /groups/{groupId} {
allow create: if request.auth.uid != null;
allow read: request.auth.token[groupId] != null;
allow delete, update: if request.auth.token[groupId] == 'owner';
}
But now i can just get or update a document when I have the group ID and the rule to read the docs don't allow me to find all groups that user is in to list then.
The code that I tryed to run is:
this.unsubscribe = db.collection("groups").onSnapshot(querySnapshot => {
var groups = [];
querySnapshot.forEach(function(doc) {
groups.push({uid: doc.id, ...doc.data()});
});
this.setState({sites})
})
the only solution that I think (but not tested yet) is get every group from auth token and make a request for avery one, but I think this is probably not a good one.
I already test using firestore resource object name and id and even inserting the id inside the document.
Finally I got this.
For who is trying some like this, here is my code.
The rules:
match /groups/{groupId} {
allow create: if request.auth.uid != null;
allow read: if resource.data.managers[request.auth.uid] != null;
allow delete, update: if resource.data.managers[request.auth.uid] == "owner";
}
The client JS:
this.unsubscribe = db.collection("groups").where(`managers.${firebase.auth().currentUser.uid}`, ">", "").onSnapshot(querySnapshot => {
var groups= [];
querySnapshot.forEach(function(doc) {
groups.push({uid: doc.id, ...doc.data()});
});
this.setState({groups})
})
I'm self studying firestore and I could not figure out a way to only allow a user to update, delete or read only the collections added by them.
This is the structure I'm using:
I use firebase auth for user handing. I save the currentUser.uid as user_id in the database for each collection.
These are the rules I'm using
service cloud.firestore {
match /databases/{database}/documents {
match /tasks{
allow read, update, delete: if request.auth.uid == resource.data.user_id;
allow create: if request.auth.uid != null;
}
}
When I try to read/get the data I get Missing or insufficient permissions error.
I'm using the web api (JavaScript) for firestore. This is the code I'm using to read data.
function read() {
db.collection("tasks").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
var newLI = document.createElement('li');
newLI.appendChild(document.createTextNode(doc.data().task));
dataList.appendChild(newLI);
});
});
}
the error was in my JavaScript I was getting all without filtering by user
function read() {
let taskColletion = db.collection("tasks");
taskColletion.where("user_id", "==", firebase.auth().currentUser.uid).get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
var newLI = document.createElement('li');
newLI.appendChild(document.createTextNode(doc.data().task));
dataList.appendChild(newLI);
});
});
}
This is actually explained on the Firestore Documentation(I recommend reading it).
You're missing a wildcard after /tasks:
service cloud.firestore {
match /databases/{database}/documents {
match /tasks/{task} {
allow read, update, delete: if request.auth.uid == resource.data.user_id;
allow create: if request.auth.uid != null;
}
}
}