How to prevent duplication of voting while increasing the number of votes on Firebase?

I am collecting a real-time survey in Firebase. Each vote is stored in a list box. In order to prevent everyone from voting for the client, to count them, I cache the counts for each choice in the counter fields.

poll1 counts choice1: 5 choice2: 2 choice3: 10 choice4: 252 voters uid1 : choice1 uid6 : choice3 uid25: choice2 uid31: choice1 

Now I am updating the counter with the following transaction:

 var fireCountPush = new Firebase(self.fireCountUrl+node+id); fireCountPush.transaction(function(current_value) { return current_value + 1; }, function(error, committed, snapshot) { if(committed) { var fireVote = new Firebase(self.fireVotesUrl); fireVote.child(self.user.uid).set(id); } }); 

However, I want atomic to add the user to the voters list, ideally in the same transaction. Unfortunately, now I have this, I have to add the user after a successful transaction. This is a huge security issue because it can be easily disabled in the browser by editing the script.

Is there a way to update the counter and add the user to the voters list without loading the entire object into the transaction?

+6
source share
2 answers

The simplest answer: the neutral side (server script) controls the voter list and increases the counter. Then you just need to make sure that users add themselves through their uid and can do this only once.

I am sure there are also brilliant ways to do this completely with security rules. Unfortunately, I’m not so brilliant, but here the brute-force approach can improve if you really want the pain of a client-only solution.

Plan:

  • force the user to record the audit record first.
  • make them add their name to the voter list second
  • allow them to update the counter when both of these entries exist and match the voting number

Scheme:

 /audit/<$x>/<$user_id> /voters/$user_id/<$x> /total/<$x> 

We do not allow the user to change the audit / if they already voted (there is a voter / $ user_id), or if the audit record already exists (someone already claimed that the count), or if the vote does not increase by one:

 "audit": { "$x": { ".write": "newData.exists() && !data.exists()", // no delete, no overwrite ".validate": "!root.child('voters/'+auth.uid).exists() && $x === root.child('total')+1" } } 

You should update audit in the transaction, essentially trying to β€œrequire” each increment until you succeed and cancel the transaction (returning undefined) at any time when the added record is not null (someone has already stated this). This gives you a unique voting number.

To prevent a funny business, we keep a voter list that makes each voter record only once at a time. I can only write to voters if I have never voted before, and only if an audit record has already been created with my unique voting number:

 "voters": { "$user_id": { ".write": "newData.exists() && !data.exists()", // no delete, no replace ".validate": "newData.isNumber() && root.child('audit/'+newData.val()).val() === $user_id" } } 

Finally, but not least, we update the counter to match our declared id. It must match my number of votes, and it can only increase. This prevents the race condition when one user creates an audit record and a voter record, but someone else has already increased the total amount before I finish the three steps.

 "total": { ".write": "newData.exists()", // no delete ".validate": "newData.isNumber() && newData.val() === root.child('audit/'+auth.uid).val() && newData.val() > data.val()" } 

Updating the total amount, such as adding an initial audit record, will be performed in the transaction. If the current value of the total amount is more than my assigned number of votes, I just canceled the transaction using undefined because someone else voted for me and updated it above. No problem.

+6
source

Following my second comment - why keep a separate account?

Keep the account in the unique record that you click (increment for each new record) - and instead of viewing the changes in the counter, watch for "add_child" on your UniqeID record with a limit of 1. This will return the current total amount.

/ user id / = counter

The task is completed - in one update.

change

If you are still considering this ... I will give you a design template to mitigate its lack of atomicity ...

"Spray and Stencil Update"

We have a check that can’t manage updates, but knows what the update should do (the account should go up by one and only one) - this is a passive stencil, just filtering out what should not pass.

We have a client that can manage updates, but cannot be absolutely sure what an update is. He wants to update the account by one, but the account may increase from the moment of his last reading! This will not lead to validation, and he will have to try again, but the same thing can happen, and we have a race condition ...

Trying one update and checking the result is similar to using a thin brush with sencil, it is redundant. Therefore, let's do an update in the "spray" cycle, there is no need to check the results, just call the update - try to update the account by 1, 2, 3, 4, 5, 6 ... One of them be true and update! The rest will fail either because the user has already voted (the previous update is working), or because the index is incorrect! While the size of the spray (the number of record attempts) is greater than the number of other voters between reading the last index and spraying updates, it will definitely turn out 100%.

Old school purists don't like this idea, but they prefer SVN to GIT and will use SQL not firebase :)

0
source

All Articles