Search code examples
androidauthenticationfirebase-realtime-databasefirebase-securityrules

How to make Firebase database values only visible to their respective paired UIDs?


This is my database structure: database structure

I want the values under the node of user 1 only readable and writable to user 1, and the values under the node of user 2 only readable and writable to user 2, and so on for other new users in the future. That means user 1 cannot read and write values under the nodes of other users and vise versa.For that I had to set up a rule for my database.


The rule below is what I tried. So far, User 1 can only write values under his own node and does not write under the nodes of other users, and vise versa, which is exactly what I want. Problem is user 1 can still read values under the nodes of other users, which is undesirable. I read some documents explaining the database rules but this is very hard for me to understand. I tried multiple ways to rewrite my rule but still failed. I need help to fix this.

{
  "rules": {
    ".read": "auth.uid != null",
    ".write":"auth.uid != null",
    "Users' Input History": {
      "$user": {
      ".validate":"$user === auth.uid"
      }
    },

    "Users' Vocabulary List": {
      "$user": {
      ".validate":"$user === auth.uid"
      }
    }
  }
}

For the record, I used the rule simulator, as in the following images. I toggeled on the "validated" option, selected "Google" as the provider, set simulated UID as "Kad06bqeNChhjaksxgP9cVtoFMh1" (which is user 1), and hit the execute button. The result was both "read" and "set" permitted at line 3 and 4 respectively.

enter image description here

enter image description here


Below is my code for user authentication and submitting values to the database:

GoogleSignInActivity:

public class GoogleSignInActivity extends BaseActivity implements
        View.OnClickListener {

    private static final String TAG = "GoogleActivity";
    private static final int RC_SIGN_IN = 9001;

    // [START declare_auth]
    private FirebaseAuth mAuth;
    private FirebaseAuth.AuthStateListener mAuthListener;
    // [END declare_auth]

    private GoogleSignInClient mGoogleSignInClient;
    private TextView mStatusTextView; // For displaying user's email
    private TextView mDetailTextView; // For displaying user's UID
    private TextView mScreenNameTextView; // For displaying user's display name (user.getDisplayName())


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_google_sign_in);

        // Views
        mStatusTextView = findViewById(R.id.status);
        mDetailTextView = findViewById(R.id.detail);
        mScreenNameTextView = findViewById(R.id.screen_name_textView);

        setProgressBar(R.id.progressBar);

        // Button listeners
        findViewById(R.id.signInButton).setOnClickListener(this);


        // [START config_signin]
        // Configure Google Sign In
        GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestEmail()
                .build();
        // [END config_signin]

        mGoogleSignInClient = GoogleSignIn.getClient(this, gso);

        // [START initialize_auth]
        // Initialize Firebase Auth
        mAuth = FirebaseAuth.getInstance();
        // [END initialize_auth]



        mAuthListener = new FirebaseAuth.AuthStateListener() {
            @Override
            public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
                FirebaseUser user = firebaseAuth.getCurrentUser();
                if (user != null) {
                    // User is signed in
                    Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid());
                    toastMessage("Successfully signed in with: " + user.getEmail());
                    mStatusTextView.setText(getString(R.string.Google_status_fmt, user.getEmail()));
                    mDetailTextView.setText(getString(R.string.Firebase_status_fmt, user.getUid()));


                            username = mDetailTextView.getText().toString();

                }
            }
        };


    }



    // [START on_start_check_user]
    @Override
    public void onStart() {
        super.onStart();
        mAuth.addAuthStateListener(mAuthListener);
        // Check if user is signed in (non-null).
        FirebaseUser currentUser = mAuth.getCurrentUser();
    }
    // [END on_start_check_user]


    @Override
    public void onStop() {
        super.onStop();
        if (mAuthListener != null) {
            mAuth.removeAuthStateListener(mAuthListener);
        }
    }


    // [START onActivityResult]
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
        if (requestCode == RC_SIGN_IN) {
            Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
            try {
                // Google Sign In was successful, authenticate with Firebase
                GoogleSignInAccount account = task.getResult(ApiException.class);
                firebaseAuthWithGoogle(account);

                Toast.makeText(getApplicationContext(), getResources().getString(R.string.Login_successful), Toast.LENGTH_LONG).show();


                        username = mDetailTextView.getText().toString();


            } catch (ApiException e) {
                // Google Sign In failed, update UI appropriately
                Log.w(TAG, "Google sign in failed", e);
            }
        }
    }
    // [END onActivityResult]

    // [START auth_with_google]
    private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
        Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());
        // [START_EXCLUDE silent]
        showProgressBar();
        // [END_EXCLUDE]

        AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
        mAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if (task.isSuccessful()) {
                            // Sign in success, update UI with the signed-in user's information
                            Log.d(TAG, "signInWithCredential:success");
                            FirebaseUser user = mAuth.getCurrentUser();
                        } else {
                            // If sign in fails, display a message to the user.
                            Log.w(TAG, "signInWithCredential:failure", task.getException());
                            Snackbar.make(findViewById(R.id.main_layout), "Authentication Failed.", Snackbar.LENGTH_SHORT).show();
                        }

                        // [START_EXCLUDE]
                        hideProgressBar();
                        // [END_EXCLUDE]
                    }
                });
    }
    // [END auth_with_google]

    // [START signin]
    private void signIn() {
        Intent signInIntent = mGoogleSignInClient.getSignInIntent();
        startActivityForResult(signInIntent, RC_SIGN_IN);
    }
    // [END signin]


    @Override
    public void onClick(View v) {
        int i = v.getId();
        if (i == R.id.signInButton) {
            signIn();
        } 

    }


}

My code for pushing values to the database:

EditText wordInputView;
String searchKeyword; 
String username;

public static DatabaseReference mRootReference = FirebaseDatabase.getInstance().getReference();
    public static DatabaseReference mChildReferenceForInputHistory = mRootReference.child("Users' Input History");
    public static DatabaseReference mChildReferenceForVocabularyList = mRootReference.child("Users' Vocabulary List");

searchKeyword = wordInputView.getText().toString();


Query query = mChildReferenceForInputHistory.child(username).orderByValue().equalTo(searchKeyword);

                query.addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                        for (DataSnapshot snapshot: dataSnapshot.getChildren()) {
                            //If a duplicate value (searchKeyword) is found, remove it.
                            snapshot.getRef().setValue(null);  
                        }
                    }

                    @Override
                    public void onCancelled(@NonNull DatabaseError databaseError) {
                        throw databaseError.toException();
                    }
                });

                mChildReferenceForInputHistory.child(username).push().setValue(searchKeyword);



//Initialize the adapter
        userInputArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, userInputArraylist);
        userInputListview.setAdapter(userInputArrayAdapter);
        mChildReferenceForInputHistory.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String previousChildKey) {

                for (DataSnapshot snapshot : dataSnapshot.getChildren()){
                    String value = snapshot.getValue(String.class);

                    userInputArraylist.add(value);


                    HashSet<String> myVocabularyArraylistHashSet = new HashSet<>();
                    myVocabularyArraylistHashSet.addAll(userInputArraylist);
                    userInputArraylist.clear();
                    userInputArraylist.addAll(myVocabularyArraylistHashSet);

                    //Alphabetic sorting
                    Collections.sort(userInputArraylist);

                    userInputArrayAdapter.notifyDataSetChanged();

                }
            }

            @Override
            public void onChildChanged(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {
            }

            @Override
            public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) {
            }

            @Override
            public void onChildMoved(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {
            }

            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {
            }
        });

Solution

  • From the Firebase documentation:

    If a rule grants read or write permissions at a particular path, then it also grants access to all child nodes under it.

    In your case you are giving .read permissions to all users that are authenticated, because this part:

      "rules": {
    ".read": "auth.uid != null",
    ".write":"auth.uid != null",
    

    ... gives read permissions to everyone that is authenticated and because this is your route directory they also give them permissions to read "Users' Input History" and "Users' Vocabulary List" because this are under your route.

    The .validate rules are used only when you are writing data and they define what the data should be. Back to your question, in your usecase you should reconsider restructuring your database and setting new rules. One possible solution is setting these rules:

    {
      "rules": {
        "Users' Input History": {
          "$user": {
                ".read": "auth.uid  === $user",
                  ".write": "auth.uid  === $user"
          }
        },
    
        "Users' Vocabulary List": {
          "$user": {
                ".read": "auth.uid  === $user",
                  ".write": "auth.uid  === $user"
          }
        }
      }
    }
    

    In this case you give permissions to the user 111 to read and write only "/Users' Input History/111" and "/Users' Vocabulary List/111" so if you have more childs under root you must define rules for each one of them.