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})
})
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.
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());
});
});
I have a simple security rule set up which uses a function to determine if a user has access to a container in the system, every container and every user has a owner field on them, the following comparison is not working however:
query:
await firebase.firestore().collection('containers').where('owner', '==', owner)
.get()
.then(({ docs }) => {
const containers = getters.get_containers;
const containersArray = docs.map(doc => ({ id: doc.id, ...doc.data() }));
docs.forEach(doc => (containers[doc.id] = doc.data()));
commit('set_containers', containers);
commit('set_containers_array', containersArray);
})
function getUser() {
return get(/databases/$(database)/documents/users/$(request.auth.uid));
}
function canAccessContainer(container_id) {
return (getUser().data.owner == get(/databases/$(database)/documents/containers/$(container_id)).data.owner);
}
match /containers/{container_id} {
allow read: if isAuthenticated() && canAccessContainer(container_id) <------- not working
allow update: if isAuthenticated() && isUserInOrg();
allow create: if isAuthenticated() && isUserInOrg() && !docExists(container_id, 'containers');
allow delete: if isAuthenticated() && isUserInOrg();
}
So after awhile of research, I've realized that I am using a lot of triggers on the back end to perform data transformation/manipulation in a lot of places.
These triggers are causing the "chain" of commands to silently fail, and really don't need security rules, basically setting the security rules to "isAuthenticated", "isUserInOrg", and matching the ownerId to the documents is enough to secure my app.
I have a users collection where each document contains a name field and and an access map.
"users" :[
{
"mHbVq5TUY7brlleejClKm71NBGI2": {
"name": "Bob Johnson",
"access": {
"X0w1VaVIljR1Nc5u3Sbo" : true
}
}
]
I would like the Firestore rules to allow creation of a new document only if it doesn't already exist and only if the person performing the action has had their email verified. For the update, only the user owning that node can perform the update, the name must be a string, and the access map should not be able to be changed. I tested my update and create rules in the simulator and they worked as expected. However, when I run a .set() it completely overwrites my entire node and removes the access map which I cannot have happen. I assume that a .set() is actually performing an update and thus meeting my update criteria. So, how do I prevent someone from completely overwriting my node. Thanks in advance...code below.
---CODE PERFORMING OVERWRITE
db.collection("users").doc("mHbVq5TUY7brlleejClKm71NBGI2").set(
{
name: "Bill Swanson",
}
).catch(err => {
console.log(err.message)
})
---RULES
function incomingData() {
return request.resource.data
}
function emailVerified() {
return request.auth.token.email_verified;
}
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
function userExists(user_Id) {
return exists(/databases/$(database)/documents/users/$(user_Id));
}
allow create: if !userExists(userId) && emailVerified();
allow update: if request.auth.uid == userId
&& !('access' in incomingData())
&& request.resource.data.name is string;
allow read: if request.auth.uid != null;
}
}
}
When using set(), If you're not sure whether the document exists, pass the option to merge the new data with any existing document to avoid overwriting entire documents.
Here's how to pass the option to merge the update with the existing document.
db.collection("users")
.doc("mHbVq5TUY7brlleejClKm71NBGI2")
.set(
{
name: "Bill Swanson"
},
{
merge: true
}
).catch(err => {
console.log(err.message)
});
Hope this helps.
Using get() in Firestore rules on a newly created document causes the return value to be false. If you wait a few seconds and hit a security rule that calls get() on that same new document, get() will then return the expected value. Am I missing something in my rules and/or code, or is this a bug with Firestore?
service cloud.firestore {
match /databases/{database}/documents {
match /budgets/{budgetId} {
allow read: if resource.data.userId == request.auth.uid;
allow create: if request.auth.uid == request.resource.data.userId;
match /accounts/{accountId} {
allow read, create, update: if userOwnsBudget(); // <--- failing for newly created budget documents
}
function userOwnsBudget() {
return get(/databases/$(database)/documents/budgets/$(budgetId)).data.userId == request.auth.uid;
}
}
}
}
const data: Budget = {
userId: userId,
budgetName: budgetName,
currencyType: currencyType
};
try {
const newBudget = await this.afs.collection<Budget>('budgets').add(data);
const accountsCollection: AngularFirestoreCollection<BudgetAccount> = this.afs.collection<BudgetAccount>('budgets/' + newBudget.id + '/accounts');
//Insufficient permission, but occasionally succeeds
accountsCollection.valueChanges().subscribe(accounts => {
console.log(accounts);
});
setTimeout(() => {
accountsCollection.valueChanges().subscribe(accounts => {
console.log(accounts)
});
}, someArbitaryTime) // Typically waiting 5 seconds is enough, but occasionally that will still fail
} catch(error) {
console.error(error);
}
EDIT: This bug has been resolved.
This is unfortunately a known issue at the moment. We're working on a fix and will update here once it's resolved. Thanks and sorry!