the file on firebase storage is not accessible if metadata updated - firebase

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.

Related

Cannot retrieve field from a document in 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.

Random behaviour while deleting bucket from S3

I am working on the code that interacts with Aws s3 to perform various operations like create bucket, Delete bucket, upload and download files and so on.
An issue is occurring while trying to delete the bucket; Access Denied
At present, I am using Root user credentials to create and delete the bucket. No versioning is enabled and could not see any bucket Policy in AWS Console attached to this bucket.
It is showing strange behaviour; sometimes gives access denied error while trying to delete the empty bucket , sometime it just gets delete effortlessly.
I am able to delete the bucket via AWs s3 console without any trouble. It is just through the code it is behaving random.
Can please somebody explain; what could be the reason?
here is my code
public string DeleteBucket(string bucketName, string S3Region)
{
string sts = "";
Chilkat.Http http = new Chilkat.Http();
// Insert your access key here:
http.AwsAccessKey = "AccessKey";
http.AwsSecretKey = "SecretKey"; //root user
http.AwsRegion = S3Region;
bool success = http.S3_DeleteBucket(bucketName);
if (success != true)
{
return sts = "{\"Status\":\"Failed\",\"Message\":\""http.lastErrorText"\"}";
}
else
{
return sts = "{\"Status\":\"Success\",\"Message\":\"Bucket deleted!\"}";
}
}
You should examine the HTTP response body to see the error message from AWS.
For example:
http.KeepResponseBody = true;
bool success = http.S3_DeleteBucket(bucketName);
if (success != true) {
Debug.WriteLine(http.LastErrorText);
// Also examine the error response body from AWS:
Debug.WriteLine(http.LastResponseBody);
}
else {
Debug.WriteLine("Bucket created.");
}

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'

Xamarin.Forms how to backup a SQLite database in SD card

I have a SQLite database in my project and I'd like to add a backup function to save that db in the SD card.
Is there any simple way to do this for both Android and IOS using Xamarin.Forms?
I searched among the old (some of them are veeery old) questions but I couldn't find a clear answer.
iOS:
There is no "sdcard" on iOS, but you can copy (File.Copy) the db to the App's Documents directory so it is accessible via the iTunes app so you can copy to your PC/Mac:
Use this directory to store user-generated content. The contents of this directory can be made available to the user through file sharing; therefore, his directory should only contain files that you may wish to expose to the user.
The contents of this directory are backed up by iTunes and iCloud.
So assuming you are using App Support directory for your DB, which you should be:
public string GetDBPath(string dbFileName)
{
// If you are not using App Support directory for your DBs you are doing it wrong on iOS ;-)
var supportDir = NSSearchPath.GetDirectories(NSSearchPathDirectory.ApplicationSupportDirectory, NSSearchPathDomain.User, true)[0];
var dbDir = Path.Combine(supportDir, "database");
if (!Directory.Exists(dbDir))
Directory.CreateDirectory(dbDir);
return Path.Combine(supportDir, dbDir, dbFileName);
}
Copying to your App's doc directory is as simple as:
public void CopyDBToDocs(string dbFileName)
{
var docDir = NSSearchPath.GetDirectories(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User)[0];
File.Copy(GetDBPath(dbFileName), Path.Combine(docDir, dbFileName));
}
Android:
On Android, it can be really simple, or a pain, depending upon the API of the device and what your Android API target of the app that you are developing.
As simple as:
public void CopyDBToSdCard(string dbName)
{
File.Copy(GetDBPath(dbName), Path.Combine(Android.OS.Environment.DirectoryDownloads, dbName);
}
GetDBPath in this case is actually using the "true" database directory which is the subdir from your app's sandboxed "FileDir" location:
public string GetDBPath(string dbFileName)
{
// FYI: GetDatabasePath("") fails on some APIs &| devices &|emulators so hack it... (Some API 23 devices, but not API 21, 27, 28, ...)
var dbPath = context.GetDatabasePath("ZZZ").AbsolutePath.Replace("/ZZZ", "");
if (!Directory.Exists(dbPath))
Directory.CreateDirectory(dbPath);
return context.GetDatabasePath(dbFileName).AbsolutePath;
}
Now to actually perform that file copy to the "external" location from the app, there is the app's manifest permission that is required:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Also if your app has "targeted" and is running on API-23 (Android 6.0) or above, you need to have the user consent to that permission at runtime.
Using the Android.Support.Compat is the easiest way. Basically you need to check if the permission has already been granted, if not show the user why you need permission and then let the OS ask the user to accept/deny the request. You will then be notified on those perm results.
Note: There is the Forms' Perm plugin from JamesM for the non-bold coders ;-) PermissionsPlugin
void GetExternalPerms()
{
const string writePermission = Manifest.Permission.WriteExternalStorage;
const string readPermission = Manifest.Permission.ReadExternalStorage;
string[] PermissionsLocation =
{
writePermission, readPermission
};
if ((ContextCompat.CheckSelfPermission(this, writePermission) == Permission.Granted) && (ContextCompat.CheckSelfPermission(this, readPermission) == Permission.Granted))
{
return;
}
if (ActivityCompat.ShouldShowRequestPermissionRationale(this, writePermission))
// Displaying a dialog would make sense at this point...
}
ActivityCompat.RequestPermissions(this, PermissionsLocation, 999);
}
Then the results of the user accepting or denying that request will to return via OnRequestPermissionsResult:
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
switch (requestCode)
{
case 999: // This is the code that we supply via RequestPermissions
if (grantResults[0] == Permission.Granted)
{
// you have permission, so defer to your DB copy routine now
}
break;
default:
break;
}
}

Adding custom data to a firebase storage upload?

I'm uploading files to firebase storage like so:
var storageRef = firebase.storage();
var fileRef = storageRef.ref(file.name);
fileRef.put(file)
.then(function (snapshot) {
console.log('Uploaded a blob or file!');
window.URL.revokeObjectURL(file.preview);
})
After the upload I have a firebase storage trigger:
export const processUploadedFile = functions.storage.object().onChange(event => {
}
What I want to do is upload some additional information with the original upload so that the processUploadedFile knows what to do with it (for example extract the file, move it to a special directory, etc, etc).
I tried using metadata like so:
var newMetadata = {
customMetadata: {
"Test": "value"
}
}
fileRef.put(file, newMetadata)
But on the cloud storage trigger function I don't know how to get the metadata, I logged out fileMetaData like so:
file.getMetadata().then((metaData)=>console.log(metaData))
But did not see my metadata anywhere in there (or in fileMetaData[0].metadata which returned undefined)
Not sure how I can achieve this...
I think providing file meta info will do the trick. Here is the reference. Firebase Storage File metadata. You can pass custom parameters for the file with customMetadata. For instance :
customMetadata: {
'actionType': 'ACTION_CODE',
'action': 'do something info'
}
You can access this metadata with storage trigger and take the action accordingly. Here is how you can achieve that Automatically Extract Images Metadata
I believe there are some properties that cannot be changed as they are not writeable. However, if you indeed want to add a custom data to firebase storage, you can set custom metadata as an object containing String properties. For example:
var myCustomMetadata = {
customMetadata : {
'file_name': 'this is the file name'
}
}
In the case above, the file_name is the custom metadata that you want to create.
After creating a reference to the file in the firebase storage, you can then call the updateMetadata() method on the reference.
For example:
Get the reference to an image file using downloadUrl:
var getRef = firebase.storage().refFromURL(imageUrl);
Use the reference to update the metadata:
getRef.updateMetadata(myCustomMetadata).then(()=>{
//do other things
})
For me, I had to call to Firebase Storage 2x. I'm using Java on an Android device to edit the metadata. First time is to upload the image. Second time is to set the image's metadata.
Instructions to set the Metadata of a Storage file is here= https://firebase.google.com/docs/storage/android/file-metadata
"You can update file metadata at any time after the file upload completes by using the updateMetadata() method. "
Here's my functions:
private void uploadImageToFBStorageAndFS(byte[] profilePic, final StorageUrlEstablished_CL storageUrlCallback) {
String storage_directory = //You get this
StorageReference profileImageRef = FirebaseStorage.getInstance().getReference(storage_directory).child(final_filename);
//1st time, upload the image/bytes.
if (profilePic != null) {
profileImageRef.putBytes(profilePic).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
#Override
public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
Task<Uri> result = taskSnapshot.getMetadata().getReference().getDownloadUrl();
result.addOnSuccessListener(new OnSuccessListener<Uri>() {
#Override
public void onSuccess(Uri uri) {
updateImageMetadata(profileImageRef);
String urlWhereProfilePicIsStored = uri.toString();
}
});
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
//Error handling
}
});
}
}
private void updateImageMetadata(StorageReference profileImageRef){
//Some devices, like the Asus tablet, doesn't upload good meta-data with the image.
// Create file metadata including the content type
StorageMetadata metadata = new StorageMetadata.Builder()
.setContentType("image/png")
.setCustomMetadata("myCustomProperty", "Rent App")
.build();
// Update metadata properties
profileImageRef.updateMetadata(metadata);
}

Resources