I'm using firebase-bolt to set my rules.
My bolt:
// ######## CONTENTS
path /contents {
read() = true;
index() = ["dt_created"];
}
path /contents/$id is Timestamped<Contents> {
write() = isSignedIn() && isAllowEdit(this);
}
type Contents {
text : String,
address : String,
organization: String | Null,
location: String | Null,
map_lat : String,
map_lng : String,
num_favorite: Number,
num_comment: Number,
num_denounce: Number,
removed: Boolean,
category_id : String,
user_id : String,
photos: String[]
}
//
// Helper Functions
//
isSignedIn() = auth != null;
isAllowEdit(value) = (prior(value) == null || newData.child('user_id').val() == auth.uid);
I would like the only the owner the post can edit, but anyone can update the counters.
I think :
"contents": {
"$id": {
"num_favorite": {
".write": true
....
Not sure if it's possible. But can I create rules to edit only field with contains ?
This in regular Firebase Security Rules:
"contents": {
"$id": {
"num_favorite": {
".write": true
}
Translates to this on Bolt:
path /contents/$id/num_favorite {
write() = true;
}
This will work, because you're adding a new permission and not trying to remove an existing permission (which is not possible in Firebase's security rules language).
But I would consider separating the vote out into its own higher-level node:
path /favorite_counts/$id {
write() = true;
}
This keeps your security rules simpler and more isolated from each other.
Related
I'm working on a chat module using fiebase , I have a structure as below .
My main node is mychatexperiment
inside it I have another node ChatListing and some other chat keys.
Today I set up setValues in my structure and when I passed my url without any node it deletes all of my data inside the parent node.
What i want is to set up the rules
One can create the node in any case
One can update the node in any case
One can not delete the node in any case
One Can only update or set the data inside ChatListing
I was trying using this but it does not work . any idea how to implement these things .
{
"rules": {
".write": "!data.exists() || !newData.exists()",
"read" : true
}
}
Note : I'm not using any authentication so need to implement rules without any authentication
Revise Requirements :
I have a structure where I have a single parent node and inside it I have multiple chat keys that is generated through firebase , Inside chat keys I have a node of individual message inside that chat .
The second thing which is most important is that I have a node called chatlisting in which I am storing my userids in a unique format like If My userid is 5 so inside chatlisting I am storing 5 and inside that 5 I have all the chat keys nodes which are related to me .
{
"ChatListing": {
"1126_2": { //userUnique key
"MjmhpHb6qR7VSkYzbjI": { // 1126 userid has this chat key and inside this chat last msg was welcome and its unread count is 0
"UnReadCount": 0,
"lastmessage": "Welcome",
"lastmessagetime": 1631870264251
}
},
"4184_1": {
"MjmhpHb6qR7VSkYzbjI": { // 4184 userid has this chat key as well and inside this chat last msg was welcome and its unread count is 1
"UnReadCount": 1,
"lastmessage": "Welcome",
"lastmessagetime": 1.6318646965369204E12
}
}
},
"MjmhpHb6qR7VSkYzbjI": { // chat key
"-MjmhpQbBaL7EbHPHayA": { // mesg key
"data": "Thankyou",
"time": 1.6318646965369204E12,
"type": 0,
"user": 4184 // the msg is sent by this user
},
"-Mjn21A4067dT4emYe05": { // another msg in the same chat
"data": "Welcome",
"time": 1631870264251,
"type": 0,
"user": 1126 // the msg is sent by this user
}
}
}
What I want is to setup the rules in which no one can run update , set or delete inside parent node (except ChatList node) . Any one can create chat keys and read chat keys inside parent node , nothing else they can do .
but inside chatlist they can perform create read , set and update(not delete) as I need to update the last message in this node against user chat .
So reusing the points as covered by my other answer, you would apply those rules using:
{
"rules": {
"ChatListing": {
"$userid": { // the user's unique ID
// anyone who knows this user ID can read their messages
".read": true,
"$chatid": { // a chatroom the user is in
// data stored here MUST have this shape (with nothing else)
// {
// UnReadCount: number,
// lastmessage: string,
// lastmessagetime: number
// }
// Data may be created or updated, but not deleted
".validate": "newData.child('UnReadCount').isNumber() && newData.child('lastmessage').isString() && newData.child('lastmessagetime').isNumber()",
"UnReadCount": { ".write": "newData.exists()" },
"lastmessage": { ".write": "newData.exists()" },
"lastmessagetime": { ".write": "newData.exists()" }
}
}
},
// when using $ keys at the same level as defined keys,
// this rule will catch everything that doesn't match
// the above rules
"$chatId": { // a chatroom where messages can be sent
// anyone who knows this chat ID can read its messages
".read": true,
"$msgId": { // a message in this chatroom
// Data stored here MUST have this shape (with nothing else)
// {
// data: string,
// time: number
// type: number,
// user: string, // see note
// }
// Data may be created, but not edited or deleted
// change like above if needed
".validate": "newData.child('data').isString() && newData.child('time').isNumber() && newData.child('type').isNumber() && newData.child('user').isString()",
"data": { ".write": "!data.exists()" },
"time": { ".write": "!data.exists()" },
"type": { ".write": "!data.exists()" },
"user": { ".write": "!data.exists()" }
}
}
}
}
Notes:
Don't use numeric user IDs as they are easily guessable, generate something random. You could even use const userId = push(getReference(getDatabase())).key. Consider securing the data with anonymous authentication.
Unlike your requirements, I have made the messages in the chat immutable. Once sent, no one can edit them. This prevents someone other than the sender from coming in and changing the message. With authentication, edits could be allowed because it's more secure.
Take note how unlike my /cars example, I haven't put ".read": true at the root of the database or at /ChatListing. This prevents someone coming along and running either of the below pieces of code to pull all of your stored data or pull all stored user IDs at once which will then allow them to find messages not meant for them. It does not prevent brute-forcing the data though.
const rootRef = ref(getDatabase());
rootRef
.then((snapshot) => {
console.log(snapshot.val()) // all data in database!
});
const chatListingRef = ref(getDatabase(), "ChatListing");
chatListingRef
.then((snapshot) => {
const usersArray = [];
const chatIdSet = new Set();
snapshot.forEach(userData => {
usersArray.push(userData.key)
userData.forEach(lastChatData => chatIdSet.add(lastChatData.key));
});
// logs all user IDs in the database!
console.log("User IDs:", usersArray)
// logs all chatroom IDs in the database!
console.log("Chatroom IDs:", [...chatIdSet])
});
I have a field in my Firestore document called lastApproval that should be a timestamp if it has a value.
this is my simplified rules
match /users/{userID} {
allow create: if isValidUserStructure(incomingData())
allow update: if isValidUserStructure(incomingData())
}
function incomingData() {
return request.resource.data;
}
function isValidUserStructure(user) {
return user.email is string
&& user.fullname is string
&& user.lastApproval is timestamp // error in here
}
as you can see, isValidUserStructure function will be used to check when creating and updating user document.
when creating user document, that lastApproval field will be null like this
const data = {
fullname: 'John Doe',
email: 'my#email.com',
lastApproval: null
};
await db.collection('users').doc('userIDHere').set(data);
but when updating document, I only want timestamp.
const data = {
lastApproval: new Date()
};
await db.collection('users').doc('userIDHere').update(data);
and also, I want to pass the security rules if lastApproval is not available, for example, if the user only want to update the fullname like this
const data = {
fullname: "My New Name"
};
await db.collection('users').doc('userIDHere').update(data);
so I want my security rules to accept timestamp, null and undefined value for lastApproval field
I have tried but error
please help :)
There isn't any type null as far as I know. You can find all available types in the documentation.
If you want to check if the value is null then try user.lastApproval == null instead. To check if the lastApproval property exists at first place, try this:
match /collection/{doc} {
allow write: if 'lastApproval' in request.resource.data && (..otherLogic)
}
So you can write your function as:
function isValidUserStructure(user) {
return user.email is string
&& user.fullname is string
&& (('lastApproval' in user && user.lastApproval is timestamp) || !('lastApproval' in user))
}
I am doing a batch write like so:
const batch = this.afs.firestore.batch();
const studentID = this.afs.createId();
const classID = this.afs.createId();
const studentRef = this.afs.doc(`students/${studentID}`).ref;
batch.set(studentRef, {
name: 'tom',
classes: firebase.firestore.FieldValue.arrayUnion(classID)
});
const classRef = this.afs.doc(`classes/${classID}`).ref;
batch.set(classRef, {
name: 'calculus',
students: firebase.firestore.FieldValue.arrayUnion(studentID)
});
await batch.commit();
And I want to ensure that either:
students/studentID/classes array can only be changed if classes/classID/students array also changed with the correct IDs
this is not enforced if that field is not updated
one field cannot be deleted unless the other field is deleted
So, I am thinking this:
match /students/{studentID} {
allow read;
allow write: if noChange('classes') ||
matchWrite(studentID, 'classes', 'students');
}
match /classes/{classID} {
allow read;
allow write: if noChange('students') ||
matchWrite(classID, 'students', 'classes');
}
function noChange(field) {
return !(field in request.writeFields);
}
function getVal(field) {
return resource.data[field].removeAll(request.resource.data[field])[0];
}
function matchWrite(VAL1, VAL2, VAL3) {
return VAL1 in
getAfter(/databases/$(database)/documents/$(VAL2)/$(getVal(VAL2))).data[VAL3];
}
delete - delete for references...
// allow delete: if noChange('classes') || matchDelete('classes', 'students');
// allow delete: if noChange('students') || matchDelete('students', 'classes');
// function matchDelete(VAL1, VAL2) {
// students/STUDENTID/classes array CLASSID (being removed)
// must eq classes/CLASSID/students array STUDENTID (being removed)
// and the other way around
// }
I am getting boggled down on the last part. Since the batches are atomic, I would think I can use getAfter() somehow.
How would I ensure atomic rules or nothing?
J
I want to create two documents
Account/{uid} {
consumerId: ... //client generated id
}
Consumer/{consumerId} {
...
}
and I have a security rule for the consumer collection
match /Consumer/{consumerId} {
allow create: if (consumerId == get(/databases/$(database)/documents/Account/$(request.auth.uid)).data['consumerId'];
}
I need to ensure that an account can only add a consumer document with a consumerId corresponding to the one in their Account document. Both documents should be created together. I've been trying to do this with transactions but I keep getting the error "Transaction failed all retries.". Whats going wrong and how do I fix it?
The data variable is an object and not an array, so you should use data.consumerId instead of data['consumerId']:
match /Consumer/{consumerId} {
allow create: if consumerId == get(/databases/$(database)/documents/Account/$(request.auth.uid)).data.consumerId;
}
I ended up accomplishing this with a batch write and security rules.
match /consumer/{cid} {
function isNewResource() { return resource == null; }
allow create: if isRegistered();
allow read, update: if isNewResource();
}
And then client side with something along the lines of
createThing() {
const db = firebase.firestore();
const { uid, displayName } = this.auth.currentUser;
const batch = this.db.batch();
// Essentially generating a uuid
const newConsumerRef = db.collection("consumer").doc();
// Update the user doc
batch.update(
db.collection('/users').doc(uid),
{ consumerID: newConsuemrRef.id }
);
// Update the admins field in the new doc
batch.set(newConsumerRef, {
admins: {
[uid]: displayName,
},
});
return batch.commit();
}
My problem was the same, but the write to the field in the collections actually needed to be to an object key, so it looked a little funkier
batch.update(
db.collection('/users').doc(uid),
{ [`adminOf.${newRef.id}`]: 'some special name' }
);
I'm having a hard time figuring out how to validate a multi-location update where the updates depend on each other.
Consider the following structure:
"votes": {
"$post_id": {
"$answer_id": {
"$user_id": {
".write": "...",
".validate": "..."
}
}
}
}
"users": {
"$user_id": {
"votes": {
"up": {
".write": "...",
".validate": "..."
},
"down": {
".write": "...",
".validate": "..."
}
}
}
}
The users can vote on posts' answers with -1 / +1 (or remove their votes, so with null as well). So far so good, I can validate that no problem. My problem comes when I want to validate the user's up/down vote counter as well.
Example scenario: user A votes on an answer with +1, which would also increment user B's up counter with 1. How can I validate the up field so that it only gets incremented (or decremented) when there's an actual new vote for that.
Also there are scenarios like when a user has already voted +1 and then changes it directly to -1. I'm having a really hard time validating updates like this.
Should I just consider adding a server layer and do every single updates through the server? Or is my approach totally wrong here (or the data structure?). Adding a server layer would pretty much solve every validation issue, but also would add one more point of failure so I'm trying to avoid that.
Edit:
Update function
function vote(postID: string, answerID: string, author: string, oldVal: number, newVal: number): firebase.Promise<void> {
let voteValue: number = newVal == 0 ? null : newVal; // -1, 0, 1, could be changed to boolean
return this.ref.child(`users/${author}/votes`).once('value', count => {
let updates = {};
updates[`votes/${postID}/${answerID}/${this.authService.current.$key}`] = voteValue;
if (voteValue == 1) {
updates[`users/${author}/votes/up`] = ++count.val().up;
if (oldVal == -1) {
updates[`users/${author}/votes/down`] = --count.val().down;
}
}
if (voteValue == -1) {
updates[`users/${author}/votes/down`] = ++count.val().down;
if (oldVal == 1) {
updates[`users/${author}/votes/up`] = --count.val().up;
}
}
if (voteValue == null && oldVal == -1) {
updates[`users/${author}/votes/down`] = --count.val().down;
}
if (voteValue == null && oldVal == 1) {
updates[`users/${author}/votes/up`] = --count.val().up;
}
this.ref.update(updates);
});
}
When an answer's author's current votes are 0/0 and another user upvotes one of his answers it would create an update like:
"votes/-KM0CMCIQuBsGWQAjhRQ/-KM0CVmhK_7JQcxtdixl/fxpxj1Ky4yVpEeeB5PZviMjqNZv1": 1
"users/spAnCEKTTjX1GgdybQZIlXRI9IG2/votes/up": 1