Cannot retrieve field from a document in Firebase - firebase

My app tries to retrieve the value of a field in a document in Firebase.
The structure of the Firebase database is very simple: The root collection is called "Users". It contains a single document, called "vfS6yOAJsjR49eWcXOeIvZCulJl2" (i.e. the userID automatically generated by Firebase). This document contains a single field, which is called "stockList". This field contains a JSON String (all verified through Firebase Console).
The code I'm using to retrieve the value of the field "stockList" is:
private FirebaseFirestore db;
private DocumentReference documentRef;
private FirebaseAuth auth;
private FirebaseUser currentUser;
private String json;
auth = FirebaseAuth.getInstance();
currentUser = auth.getCurrentUser();
Log.d(TAG, "UserID: "+ currentUser.getUid() );
if(currentUser != null){
documentRef = db.collection("Users").document(currentUser.getUid());
documentRef.get().addOnSuccessListener(documentSnapshot -> {
Log.d(TAG, "onSuccess: OK");
json = documentSnapshot.getString("stockList");
}
.addOnFailureListener(e -> {
Log.d(TAG, "Download Failure" );
}
}
The Logcat always says "Download Failure" (json remains indeed empty), though the user is properly authenticated by Firebase (currentUser.getUid() == "vfS6yOAJsjR49eWcXOeIvZCulJl2").
My Firebase security rules are:
service cloud.firestore {
match /Users/{userID} {
allow read, write : if request.auth != null && request.auth.uid == userId;
}
}
Your advice is much appreciated.
PS: I'm test-running the app through the Emulator in Android Studio. So internet connectivity shouldn't be the cause.

Related

the file on firebase storage is not accessible if metadata updated

I'm not sure if this is a bug. It works last month and runs into issues a couple of weeks later. I will post a bug report if this issue cannot be resolved.
I have an Android app that allows users to share files with another person via email address. When the file was uploaded to the Firebase Storage successfully, the app pops up a dialog to allow users to type in the address of the recipient for file sharing. And the email address will be written into custom metadata as a key.
In Firebase Storage, each user uploads files to their own folder(email address as folder name). The Storage rules are listed below. The idea is users only can access the files in their own folders, and has read permission for shared files.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// read and write permission for owners
match /users/{userEmail}/{allPaths=**} {
allow read, write: if request.auth.token.email == userEmail && request.auth.token.email_verified;
}
// read permission for shared files
match /users/{userEmail}/{allPaths=**} {
allow read: if request.auth != null && request.auth.token.email != userEmail && request.auth.token.email in resource.metadata.keys() && request.auth.token.email_verified;
}
// samples are public to read
match /samples/{allPaths=**} {
allow read;
}
}
}
The rules were modified from this thread.
Firebase rules: dynamically give access to a specific user
To work with the shared files, the app writes the recipient's email address to the file as a key of custom metadata. The Android code for updating metadata is listed below.
private void updateMetadataForSharing(String fileLocation, String documentId, String recipientEmail) {
// write file metadata
StorageMetadata metadata = new StorageMetadata.Builder()
.setCustomMetadata(recipientEmail,"")
.build();
// Update metadata properties
StorageReference storageRef = storage.getReference();
StorageReference fileRef = storageRef.child(fileLocation);
fileRef.updateMetadata(metadata)
.addOnSuccessListener(new OnSuccessListener<StorageMetadata>() {
#Override
public void onSuccess(StorageMetadata storageMetadata) {
// Updated metadata is in storageMetadata
Toast.makeText(ReviewActivity.this, "The file has been shared to "+recipientEmail+", please paste the sharable link from clipboard.", Toast.LENGTH_LONG).show();
String sharableLink = "https://web.app.com/?u="+documentId;
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("sharable link", sharableLink);
clipboard.setPrimaryClip(clip);
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
// Uh-oh, an error occurred!
Toast.makeText(ReviewActivity.this, "Error occurred attempting to share the file to "+recipientEmail, Toast.LENGTH_LONG).show();
}
});
}
But the file is not accessible after metadata updated. It was fine if the no metadata written to the file. The web app showed the errors as the picture shown.
web app error message for failing to download the file
I assume it may associate with the access token of file. It has nothing to do with the rules, because it's still not working when I grant all permissions temporarily.
Please advise. Thanks.
I ran into the same problem today with an uploaded file not being accessible after the metadata was updated. It seems like the file becomes inaccessible if the metadata key contains the # character. For some reason the key cannot contain the character but its fine in the value.

Flutter firebase user problem trying to link to a model

the error I'm getting:
A value of type 'UserA?' can't be returned from the method
'_userFromFirebaseUser' because it has a return type of 'User'
I'm following along this tutorial: Flutter & Firebase App Tutorial #6 - Custom User Model
At about 5:35 into the video he talks about returning the Firebase user object and linking it to the model. Now the video is about 2 years old, so Firebase has moved on a bit. FirebaseUser is now just called User, but I think in my changes I've made a mistake. Here is my code:
class AuthService{
final FirebaseAuth _auth = FirebaseAuth.instance;
//create user object based on firebaseuser
User _userFromFirebaseUser(User? user) {
return user != null ? UserA(uid: user.uid) : null;
}
This is my model:
class UserA {
final String uid;
UserA({ required this.uid });
}
Since FirebaseUser is now just called User I've changed the model to be UserA since I was getting confused.
Any suggestion on what I'm doing wrong?
You're mixing up the two types of users in your code.
The video has two types: FirebaseUser and User, which you have mapped to Users and UserA respectively.
With your types, the function should be:
UserA? _userFromFirebaseUser(User? user) {
return user != null ? UserA(uid: user.uid) : null;
}
Aside from the type change, the ? is needed in Dart nowadays to indicate that you may return either a UserA object or null.

The getter 'uid' was called on null in flutter

I'm implementing firebase authentication in flutter application but while trying to get user uid , it is crashing and show noSuchMethodError uid is null , if anyone could help , it is deeply appreciated
That's how i init my variables
class _UserRegistrationState extends State<UserRegistration> {
FirebaseAuth auth;
DocumentReference reference;
Reference storage;
PickedFile imageUri;
final ImagePicker _imagePicker = new ImagePicker();
#override
void initState() {
super.initState();
auth = FirebaseAuth.instance;
// the uid is where the logcat is pointing too and it is null
reference = FirebaseFirestore.instance.collection('users').doc(auth.currentUser.uid);
storage = firebase_storage.FirebaseStorage.instance.ref('avatar').child(auth.currentUser.uid);
}
When you sign in to Firebase Authentication, the SDK automatically persists the user's credentials in local storage. When you restart the app, the SDK tries to restore the user's authentication state from the stored credentials. This requires that it calls the servers to get a new ID token, and for example to check if the account was deleted/disabled in the meantime.
Depending on the platform where you run your code, the calls to the server may have completed before your auth.currentUser runs, or not. To safely respond to the user state, always use an auth state listener as shown in the FlutterFire documentation on responding to auth state changes:
#override
void initState() {
super.initState();
auth = FirebaseAuth.instance;
FirebaseAuth.instance.authStateChanges().listen((User user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
reference = FirebaseFirestore.instance.collection('users').doc(auth.currentUser.uid);
storage = firebase_storage.FirebaseStorage.instance.ref('avatar').child(auth.currentUser.uid);
}
});
}
currentUser → User?
Returns the current User if they are currently signed-in, or null if not.
So most likely thing is that you are not logged in and therefore passing null to reference

Firestore Security: Deny Update / Write If Field In Request Resource (Works in simulator, not IRL)

I have user profile data stored in Firestore. I also have some profile fields in Firestore that dictate user permission levels. I want to deny the user the ability to write or update to their Firestore profile if they include any changes that would impact their permission level.
Example fields in user's firestore doc for their profile
permissionLevel: 1
favoriteColor: red
Document ID is the same as the user's authentication uid (because only the user should be able to read / write / update their profile).
I want to deny updates or writes if the user's firestore update or write includes a permissionLevel field, to prevent the user from changing their own permission level.
Current Firestore Rules
This is working fine when I build an object in the simulator to test including or not including a field called "permissionLevel". But this is denying all update / write requests from my client-side web SDK.
service cloud.firestore {
match /databases/{database}/documents {
// Deny all access by default
match /{document=**} {
allow read, write: if false;
}
// Allow users to read, write, and update their own profiles only
match /users/{userId} {
// Allow users to read their own profile
allow read: if request.auth.uid == userId;
// Allow users to write / update their own profile as long as no "permissionLevel" field is trying to be added or updated
allow write, update: if request.auth.uid == userId &&
request.resource.data.keys().hasAny(["permissionLevel"]) == false;
}
}
}
Client-Side Function
For example, this function attempts to just update when the user was last active by updating a firestore field. This returns the error Error updating user refresh time: Error: Missing or insufficient permissions.
/**
* Update User Last Active
* Updates the user's firestore profile with their "last active" time
* #param {object} user is the user object from the login auth state
* #returns {*} dispatched action
*/
export const updateLastActive = (user) => {
return (dispatch) => {
firestore().settings({/* your settings... */ timestampsInSnapshots: true});
// Initialize Cloud Firestore through Firebase
var db = firestore();
// Get the user's ID from the user retrieved user object
var uid = firebaseAuth().currentUser["uid"];
// Get last activity time (last time token issued to user)
user.getIdTokenResult()
.then(res => {
let lastActive = res["issuedAtTime"];
// Get reference to this user's profile in firestore
var userRef = db.collection("users").doc(uid);
// Make the firestore update of the user's profile
console.log("Firestore write (updating last active)");
return userRef.update({
"lastActive": lastActive
})
})
.then(() => {
// console.log("User lastActive time successfully updated.");
})
.catch(err => {
console.log("Error updating user refresh time: ", err);
})
}
}
This same function works fine if I remove this line from the firestore rules. I don't see how they have anything to do with each other, and why it would work fine in the simulator.
request.resource.data.keys().hasAny(["permissionLevel"]) == false;
Update
I got a notice that writeFields is deprecated. I have another another answer to a similar question here which uses request.resource.data which may be an alternative that is useful to people who arrive here.
Original Answer
OK, I found a solution, but I can't find any official documentation in the firebase docs to support this. It doesn't work in the simulation, but it works IRL.
Replace (from my example above)
request.resource.data.keys().hasAny(["permissionLevel"]) == false
With This
!("permissionLevel" in request.writeFields);
Full Working Permissions Example
service cloud.firestore {
match /databases/{database}/documents {
// Deny all access by default
match /{document=**} {
allow read, write: if false;
}
// Allow users to read, write, and update their own profiles only
match /users/{userId} {
// Allow users to read their own profile
allow read: if request.auth.uid == userId;
// Allow users to write / update their own profile as long as no "admin" field is trying to be added or created
allow write, update: if request.auth.uid == userId &&
!("permissionLevel" in request.writeFields);
}
}
}
This successfully prevents an update or write whenever the key permissionLevel exists in the firestore request map object, and allows other updates as intended.
Documentation Help
Firestore Security Docs Index lists "rules.firestore.Request#writeFields" - but when you click it, the resulting page doesn't even mention "writeFields" at all.
I used the principles based on rules.Map for
k in x Check if key k exists in map x
Two other things you could consider doing for adding permission levels:
Create a separate subcollection for the user that will then contain a document with information you do not want the user to be able to change. That document can be given different permission controls.
Use Firebase Auth Tokens with Custom Claims. Bonus: this method will not trigger reads on the database. I recommend checking out these Firecasts:
Controlling Data Access Using Firebase Auth Custom Claims
Minting Custom Tokens with the Admin SDK for Java
Add the Firebase Admin SDK to Your Server guide is also very helpful.
I am new to the development game, but this what I use to manually create custom claims using ItelliJ IDEA:
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthException;
import com.google.firebase.auth.UserRecord;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class App {
//This will be the UID of the user we modify
private static final String UID = "[uid of the user you want to modify]";
//Different Roles
private static final int USER_ROLE_VALUE_BASIC = 0; //Lowest Level - new user
private static final int USER_ROLE_VALUE_COMMENTER = 1;
private static final int USER_ROLE_VALUE_EDITOR = 2;
private static final int USER_ROLE_VALUE_MODERATOR = 3;
private static final int USER_ROLE_VALUE_SUPERIOR = 4;
private static final int USER_ROLE_VALUE_ADMIN = 9;
private static final String FIELD_ROLE = "role";
//Used to Toggle Different Tasks - Currently only one task
private static final boolean SET_PRIVILEGES = true; //true to set User Role
//The privilege level being assigned to the uid.
private static final int SET_PRIVILEGES_USER_ROLE = USER_ROLE_VALUE_BASIC;
public static void main(String[] args) throws IOException {
// See https://firebase.google.com/docs/admin/setup for setting this up
FileInputStream serviceAccount = new FileInputStream("./ServiceAccountKey.json");
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.setDatabaseUrl("https://[YOUR_DATABASE_NAME].firebaseio.com")
.build();
FirebaseApp.initializeApp(options);
// Set privilege on the user corresponding to uid.
if (SET_PRIVILEGES){
Map<String, Object> claims = new HashMap<>();
claims.put(FIELD_ROLE, SET_PRIVILEGES_USER_ROLE);
try{
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
FirebaseAuth.getInstance().setCustomUserClaims(UID, claims);
// Lookup the user associated with the specified uid.
UserRecord user = FirebaseAuth.getInstance().getUser(
System.out.println(user.getCustomClaims().get(FIELD_ROLE));
}catch (FirebaseAuthException e){
System.out.println("FirebaseAuthException caught: " + e);
}
}
}
}
The build.gradle dependency is currently:
implementation 'com.google.firebase:firebase-admin:6.7.0'

Android Firebase Authentication - How to Link Existing User Account to Anonymous Account

I am having some trouble understanding how to link an existing email account to an anonymous firebase account. is this possible ? or does it only link if the email account is new ?
when i call the following code to link accounts both the anonymous account and existing account exist. but if its a new email account then i see that the new email account as the same uid as the anonymous account and the anonymous account is gone.
mAuth.getCurrentUser().linkWithCredential(credential)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
#Override
public void onComplete(#NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
Log.d(TAG, "linkWithCredential:success");
FirebaseUser user = task.getResult().getUser();
updateUI(user);
} else {
Log.w(TAG, "linkWithCredential:failure", task.getException());
Toast.makeText(AnonymousAuthActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
updateUI(null);
}
// ...
}
});
so my question is: am i able to link anonymous user account to EXISTING user account ? because then my firebase console is going to be filled with anonymous user entries.
UPDATE: Using the firebase mergService here how can i delete the anonymous account ? i dont see it returning a credential for me to delete.
the mergeService describe looks like this:
public class MyManualMergeService extends ManualMergeService {
private Iterable<DataSnapshot> mChatKeys;
#Override
public Task<Void> onLoadData() {
final TaskCompletionSource<Void> loadTask = new TaskCompletionSource<>();
FirebaseDatabase.getInstance()
.getReference()
.child("chatIndices")
.child(FirebaseAuth.getInstance().getCurrentUser().getUid())
.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
mChatKeys = snapshot.getChildren();
loadTask.setResult(null);
}
#Override
public void onCancelled(DatabaseError error) {
FirebaseCrash.report(error.toException());
}
});
return loadTask.getTask();
}
#Override
public Task<Void> onTransferData(IdpResponse response) {
String uid = FirebaseAuth.getInstance().getCurrentUser().getUid();
DatabaseReference chatIndices = FirebaseDatabase.getInstance()
.getReference()
.child("chatIndices")
.child(uid);
for (DataSnapshot snapshot : mChatKeys) {
chatIndices.child(snapshot.getKey()).setValue(true);
DatabaseReference chat = FirebaseDatabase.getInstance()
.getReference()
.child("chats")
.child(snapshot.getKey());
chat.child("uid").setValue(uid);
chat.child("name").setValue("User " + uid.substring(0, 6));
}
return null;
}
}
this gets called after user transitions from anonymous user to a real account. how can i then know the credential so i can delete the anonymous account ?
You can't link an existing credential to anonymous user.
You have to basically copy the data of the anonymous user to the existing credential user and then delete the anonymous user.
It is not possible for Firebase to handle this for you. You have 2 users with different uids and data saved on each users, not to mention different profile data on each. Firebase doesn't know which user to keep and how to merge the profile/data. In some cases, data could be saved outside of Firebase services.
FirebaseUI is currently doing a similar mechanism for upgrading anonymous users on sign in. If the credential is new, then linking will succeed without any additional action. If the credential already exists, linking will fail and the developer is expected to handle the merge conflict, copy the data from the non anonymous user and delete the anonymous user after.
This is the web flow in FirebaseUI-web: https://github.com/firebase/firebaseui-web#upgrading-anonymous-users
This is being implemented for FirebaseUI-android:
https://github.com/firebase/FirebaseUI-Android/pull/1185
Here is an example with web, given an authCredential and an anonymous user signed in.
Here is a simple web example how to handle merge conflicts.
let data;
// Default App with anonymous user.
const app = firebase.app();
// Anonymous user.
anonymousUser = app.auth().currentUser;
// Get anonymous user data.
app.database().ref('users/' + app.auth().currentUser.uid)
.once('value')
.then(snapshot => {
// Store anonymous user data.
data = snapshot.val();
// Sign in credential user.
return app.auth().signInWithCredential(authCredential);
})
.then(user => {
// Save the anonymous user's data to the credential user.
return app.database().ref('users/' + user.uid).set(data);
})
.then(() => {
// Delete anonymnous user.
return anonymousUser.delete();
})
})

Resources