Firebase rule to only allow one update in android studio

2020-03-27 android firebase firebase-realtime-database

I'm creating an android studio voting application. It is using recyclerview to render candidates information from the database. Once the voter clicks on a vote button, the candidate is added a vote on the firebase realtime database. I wanted to make sure that a voter can only vote once. Is there a firebase rule I can use or do I have to do it in code? My firebase database.

The android interface

public void onBindViewHolder(@NonNull final MyViewHolder holder, final int position) {
        holder.name.setText(candidates.get(position).getFirstname());
        holder.party.setText(candidates.get(position).getParty());
        holder.category.setText(candidates.get(position).getCategory());
        Picasso.get().load(candidates.get(position).getImageurl()).into(holder.profilepic);

        holder.vote.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                updateTotalVotes("increaseTotalVotes", candidates.get(position).getImageurl());
            }
        });

    }

    public static void updateTotalVotes(final String operation, String key) {
        System.out.println("Inside updateTotalVotes");
        DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();

        DatabaseReference totalVotesRef = rootRef.child("candidates").child(key).child("totalVotes");
        totalVotesRef.runTransaction(new Transaction.Handler() {
            @Override
            public Transaction.Result doTransaction(MutableData mutableData) {
                System.out.println("Inside Transactions");
                Integer votes = mutableData.getValue(Integer.class);
                if (votes == null) {
                    System.out.println("Inside first if statement = null");
                    return Transaction.success(mutableData);
                }

                if (operation.equals("increaseTotalVotes")) {
                    System.out.println("Inside update Votes by adding 1");
                    mutableData.setValue(votes + 1);
                } else if (operation.equals("decreaseTotalVotes")){
                    mutableData.setValue(votes - 1);
                }

                return Transaction.success(mutableData);
            }

            @Override
            public void onComplete(DatabaseError databaseError, boolean b, DataSnapshot dataSnapshot) {
                // Log.d(TAG, databaseError.getMessage()); //Don't ignore errors!
            }
        });
    }

Answers

Short answer is No, you can't by the rules of db.

But you can do it with help of authentication, which user can make an account and his vote record in a sub tree of the candidate. So that the number of sub tree children is the number of votes for this candidate.

Notice: my solution could be broken if fake accounts were made, my advice using phone authentication too, to approve the account is not fake.

Firebase's security rules cannot enforce unique value in a specific property under a single node. But (as is often the case with NoSQL databases) you can use a specific data model to implement the use-case.

The usual solution for this is to use the UID of the voter as the key.

votes
  uid1: "candidate A"
  uid2: "candidate B"
  uid3: "candidate A"

Since keys must be unique in a JSON object, this structure ensures by definition that each UID can only vote once.

This is separate from keeping the total votes for a candidate. For that you can either use security rules, or Cloud Functions.

Doing this is in security is appealing, since it means you won't need any server-side code. But the rules can become quite complex. For an example of this, see my answer to this question: Is the way the Firebase database quickstart handles counts secure?

The simpler, and these days more common, approach is to do this with a Cloud Function. From a recent project I worked on, I have this Cloud Function:

exports.countVote = functions.database.ref('/votes/{uid}').onCreate((snapshot, context) => {
  let value = snapshot.val();
  let countRef = snapshot.ref.parent.parent.parent.child(`totals/${value}`);
  return countRef.transaction(function(current) {
    return (current || 0) + 1;
  })
});

So this tallies the votes for each unique value. It then ensures that users can't change their existing vote with:

{
  "rules": {
    "votes": {
      "$uid": {
        ".write": "auth.uid === $uid && !data.exists()"
      }
    }
  }
}

So a user can only vote if they user their own UID (the auth.uid variable is prepopulated and can't be spoofed), and if they haven't voted yet.

Related