Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I prevent duplicate voting while incrementing the vote count on Firebase?

I'm building a real-time poll in Firebase. Each vote is stored in a list field. To prevent having to pull every vote down to the client in order to count them, I cache the tallies for each choice in counter fields.

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

I'm currently 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 to atomically add the user to the voters list, ideally in the same transaction. Unfortunately, the way I have it now, I'm having to add the user after the transaction commits successfully. This is a huge security issue since it can be easily disabled in the browser by editing the script.

Is there any way to both update the counter and add a user to the list of voters without having to download the entire object in the transaction?

like image 999
GinoF Avatar asked Apr 09 '14 18:04

GinoF


1 Answers

The simplest answer here is to have a neutral party (a server script) monitor the list of voters and increment the counter. Then one only need make sure users add themselves by their uid and can only do so once.

I'm sure there are also some brilliant ways to do this entirely with security rules. I'm, unfortunately, not that brilliant, but here's a brute force approach you can improve on if you really want the pain of a client-only solution.

The plan:

  • force the user to write an audit record first
  • force them to add their name to the list of voters second
  • allow them to update the counter when both of these records exist and match the vote number

The schema:

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

We prevent user from modifying audit/ if they have already voted (voters/$user_id exists), or if the audit record already exists (someone has already claimed that count), or if the vote is not incremented by exactly 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 would update audit in a transaction, essentially trying to "claim" each increment until successful and cancelling the transaction (by returning undefined) any time the record to be added is not null (someone has already claimed it). This gives you your unique vote number.

To prevent any funny business, we store a list of voters, which forces each voter to only write into audit/ once. I can only write to voters if I've never voted before and only if an audit record has already been created with my unique vote number:

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

Last, but not least, we update the counter to match our claimed vote id. It must match my vote number and it may only increase. This prevents a race condition where one user creates an audit record and voters record, but someone else has already increased the total before I finished my 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, like adding the initial audit record, would be done in a transaction. If the current value of the total is greater than my assigned vote number, then I just cancel the transaction with undefined because someone else has already voted after me and updated it higher. Not a problem.

like image 73
Kato Avatar answered Oct 15 '22 00:10

Kato