Can Firebase Cloud Storage rules validate against Firestore data? - firebase

Can we use Firestore data to grant or restrict access to files hosted on Firebase Cloud Storage?
Exemple of what I would like to use as Firebase Security Rule
allow write: if get(/databases/mydbname/documents/guilds/$(guildID)).data.users[(request.auth.uid)] in ["Admin", "Member"];

Update (Oct 2022): it is now possible to access Cloud Firestore from within your Cloud Storage security rules with the new firestore.get() and firestore.exists() functions. See the blog post Announcing cross-service Security Rules and the documentation on enhancing Cloud Storage security rules with Cloud Firestore.
Previous answer below for reference:
There is currently no way to access different Firebase products from within the security rules of another product. See: is there a way to authenticate user role in firebase storage rules?
But it seems like you are trying to check group-membership of the user. Instead of looking that group-membership up in the database, I recommend that you model it as a so-called custom claim. Instead of (or in addition to) writing the membership to the database, you'll set the claim "user {uid} is a member of group {guild1}" into the user profile using the Firebase Admin SDK:
admin.auth().setCustomUserClaims(uid, {guild1: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
With that done, you can check the guild membership in the security rules:
allow read: if request.auth.token.guild1 == true;
(I'm not sure if/how you can model the guild membership as a map, I'll update this answer if that turns out to be the case.)

Firebase recently announced cross-service security rules that let's you access Firestore data in Firebase storage's security rules. You just need to use firestore. prefix before get() and exist() functions as shown below:
allow write: if firestore.get(/databases/(default)/documents/col/docId/).data.field == "value";
Firebase current supports only 1 database instance per project so the name must be (default) in path. It's not a wildcard as in Firestore rules so not $(database)

Update: As of 2022-09-28, Firebase introduced cross-service Security Rules, so the answer below is outdated. See #Dharmaraj's answer below for an example.
You can retroactively validate and delete the file after it's been uploaded using a cloud function trigger.
Warning: this technique is not bullet proof, as the invalid file will be stored in Cloud Storage temporarily or potentially forever if the Cloud Function trigger fails. My preference is to prevent the upload in the first place, but if the logic to determine permission resides in Firestore and can't be stuffed in custom claims, then this is currently the only way if you're uploading files using Firebase's Client SDKs. If you're building a mission critical system, you should upload the file to a Cloud Function and let the Cloud Function store the file in Cloud Storage instead.
When uploading a file, add some metadata indicating who's doing the upload:
const storageRef = ref(
storage,
`files/${fileName}`,
);
const uploadTask = uploadBytesResumable(storageRef, file, {
customMetadata: {
uploaderId: userId,
},
});
Set storage rule to ensure that the user identity metadata can be trusted:
match /files/{fileName} {
allow create: if request.auth != null &&
request.resource.metadata.uploaderId == request.auth.uid
}
Create a cloud function trigger that retroactively validates and deletes:
export const onFinalize = functions
.storage.object()
.onFinalize(async object => {
// We can trust object.metadata.uploaderId, so check Firestore if user is able to upload file
if (!(await canUploadFile(object.metadata.uploaderId, object.name))) {
await storage.bucket(object.bucket).file(object.name).delete();
throw new Error(
`Permission error: ${object.metadata.uploaderId} not allowed to upload ${object.name}`,
);
}
// Continue
});

Related

How to grant access to google cloud storage buckets based on users auth uid

I am running a cloud function that saves images like this:
//Pseudocode
const admin = await import("firebase-admin");
const bucket = admin.storage().bucket();
const file = bucket.file('myName');
const stream = file.createWriteStream({ resumable: false });
...
After the images are uploaded, I get the publicUrl like so
file.publicUrl()
and store it in an Object.
This object then gets stored to firestore.
When I now copy this url from the object(the url structure looks like this)
https://storage.googleapis.com/new-project.appspot.com/ZWHpYGSQWYXLlUcAwkRFQLC0u7s1/f2a48bdc-7eb3-4174-9f6e-3fd963003bd7/177373254.png
and paste it into a browser field am getting an error:
<Error>
<Code>AccessDenied</Code>
<Message>Access denied.</Message>
<Details>Anonymous caller does not have storage.objects.get access to the Google Cloud Storage
object.</Details>
</Error>
even with the test rules:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read: if true;
allow write: if false;
}
}
}
I have read some issues here on stackoverflow and it seems like this is because I am using google cloud storage buckets and not firebase storage buckets (I thought they are the same)
but I am very confused about how to write rules in this case, so that only authenticated firebase users can read files.
Any help is highly appreciated.
The bucket is shared between Firebase and Cloud Storage, but the way you access the bucket is quite different.
When you access the bucket through a Firebase SDK, or through a download URL generated by a Firebase SDK, your access goes through a Firebase layer. This layer enforces the security rules (for the SDK), and grants temporary read-only access to the download URL.
When you access the bucket through a Cloud SDK, or a signed URL generated by a Cloud SDK, your access does not go through any Firebase layer, and thus Firebase security rules have no effect on this access.
The public URL you have is just a way to identify a file in your Cloud Storage bucket. It does not have any implied access permissions. You will need to make sure the IAM properties for your bucket allow the access you want the user to have to the file.

How to use Firebase Security Rules to secure Cloud Functions calls with Firebase Authentication?

I'm starting deep on Firebase Security Rules and I'm following the Firebase Security Rules docs, but this document just says about Realtime Database, Cloud Firestore, and Cloud Storage options. There is a way to use Firebase Authentication to protect an invoke of a Google Cloud Function from Client-Side?
I'm trying to use a GC Function as a backend to access the Cloud SQL from a Web Application.
Cloud Functions generally use Admin SDK (or service accounts, application default credentials to access any other services like Cloud SQL) which has complete access to your Firebase project's resources and also bypasses all security rules. That being said you would have to authorize requests yourself. For example, if you are using onCall function:
export const fnName = functions.https.onCall((data, context) => {
const { auth } = context
if (!auth) console.log('User not logged in')
const { uid } = auth;
// UID of user who called the function
// Check if user has access to requested resource
// process request
})
If the caller of function is not authenticated, then context.auth will be undefined.
If your question is if you can prevent the invocation of function at first place, then there's currently no way to do so. You can use Firebase App Check to ensure the function is called from your registered application only.

Firebase Cloud Storage rules based on Firestore Data work around

In my firebase app I have
a list of users(all users are registered via Firebase Authentication )
a list of followers for each user
Each user has added a profile photo, the filename of the photo is unique and is saved in a firestore doc called "photoName" document
Only Followers of a user are permitted to read the "photoName" document ( as specified in firestore rules )
I want only the followers to be able to read the profile photo of the user, I have added a rule in cloud storage as follows:
match /profilePhotos/{profilePhotoFile_id} {
allow read: if request.auth.uid != null
}
The cloud storage rules are set on a file-level so is it accurate for me to assume that only the users who are logged in and are also a follower will be able to read the profile photo of another user? Unless they somehow magically guess the unique names of a photo.
Update dated September 28, 2022
Security Rules in Cloud Storage for Firebase now supports cross-service Rules and let you query your projects’ Firestore data, similar to the get() and exists() functions in Firestore Rules.
See this Firebase blog article and this SO answer from Puf.
Previous answer
Is it accurate for me to assume that only the users who are logged in
and are also a follower will be able to read the profile photo of
another user?
No, because it is possible, with the client SDKs to list all the files in a Cloud Storage bucket, as explained in the doc and your Cloud Storage Security Rules allow any authenticated user to read the profile photos files.
Also note that you cannot read Firestore documents when writing Cloud Storage Security Rules.
One possible approach is to use a Cloud Function to generate a signed URL that you store in the Firestore document AND to forbid read access to the profile photos files. Since Cloud Functions use the Admin SDK they can bypass the security rules.
The following Cloud Function code will generate a signed URL each time a file is added to Cloud Storage and save it in a Firestore document. With this signed URL anyone can read the profile photos file.
It's up to you to adapt it to your case by:
If necessary, only treating the profile photos (check that the file name contains profilePhotos)
Saving the URL in the correct Firestore doc: I guess the file name allows linking back to the user document. Also, you will probably have to change from add() to update().
exports.generateFileURL = functions.storage.object().onFinalize(async object => {
try {
const bucket = admin.storage().bucket(object.bucket);
const file = bucket.file(object.name);
const signedURLconfig = { action: 'read', expires: '08-12-2025' };
const signedURLArray = await file.getSignedUrl(signedURLconfig);
const url = signedURLArray[0];
await admin.firestore().collection('...').add({ signedURL: url })
return null;
} catch (error) {
console.log(error);
return null;
}
});
Two additional considerations:
You can use Custom Claims in Cloud Storage Security Rules, but it is not really recommended to use them for your case, see here.
You can also use file metadata in Cloud Storage Security Rules, but again it is not adapted to your case (you are not going to add followers Ids in file metadata each time a new follower registers...)

How to add metadata to Firebase authentication

I need to pass a custom value (device_id) during google signin with firebase authentication. This value is later obtained from cloud functions by listening for authentication event triggers and then the value is added to Firestore
I understand that you can pass values as query parameters for http triggers. However I only need to pass and get the value during and after authentication in my case. Hence is there some sort of auth.addMetaData(metadata) function in firebase authentication?
I need to be able to retrieve the custom data after an auth trigger just like we can do user.email. I need something like user.custom_data
Although Doug mentions Firebase Custom Claims, I think it’s worth extra documentation because it does allow you to add simple metadata to a Firebase User object.
Important notes
Big caveat: Custom claims are only refreshed when the user logs in. So an isAdministrator claim would require the user to logout/login before it is activated.
Firebase recommends “Use custom claims to store data for controlling user access only. All other data should be stored separately via the real-time database or other server side storage.”
Set metadata (server only)
Here’s an example on how to set device_id on a Firebase User object (on the server using firebase-admin):
await admin.auth().setCustomUserClaims(uid, { deviceId })
Note: You can not set custom claims on the client.
Get metadata (server and client)
Then to retrieve the the device_id from the User on the server:
const userRecord = await admin.auth().getUser(uid)
console.log(userRecord.customClaims.deviceId)
…and on the client:
const idTokenResult = await firebase.auth().currentUser.getIdTokenResult()
console.log(idTokenResult.claims.deviceId)
Use metadata in Firebase Security Rules
The neat thing is that custom claims are also available in Firebase Security Rules. This (slightly unrealistic) example only allows users with deviceId === 123 to see the data:
{
"rules": {
"secureContent": {
".read": "auth.token.deviceId === 123"
}
}
}
More information
Official docs: https://firebase.google.com/docs/auth/admin/custom-claims
Deep dive: https://medium.com/google-developers/controlling-data-access-using-firebase-auth-custom-claims-88b3c2c9352a
A clever pattern of synching custom claims with a Firebase database collection: https://medium.com/firebase-developers/patterns-for-security-with-firebase-supercharged-custom-claims-with-firestore-and-cloud-functions-bb8f46b24e11
Firebase Authentication doesn't support any sort of extra data provided by the client. The closest thing to metadata that gets stored per user by Firebase would be custom claims, however, the JSON blob stored there can only be set by privileged server-side applications.
If you need to store data per user, written by client apps, you should probably be using a database for that (Cloud Firestore or Realtime Database), protected by Firebase security rules, so that only the end user can read and write their own data. You could also use an HTTP type Cloud Function to pass data into your function to be recorded in a database.

How to make firebase database rule work with admin.database().ref (cloud function)

I'm working with Cloud Function for Firebase. When I use admin.database().ref then all the rules that applied to the database were ignored. With admin, I can do anything. To be clear:
I have a real-time database with have a set of rules such as; name must be string and length >= 50,...
It works when using the SDK, all the invalid data will be denied. But when I move to use firebase cloud function (to reduce work in client side by providing a set of https endpoints) it didn't work anymore.
So, I wonder if there is any way to make it work? I was thinking about:
find something replace for admin.database() (took a look on
event.data.ref already but this does not work in my case - HTTP request)
verify data in cloud function (not nice)
Could you give me some hints/clues?
This is the expected default behavior of the Firebase Admin SDKs: the code accesses the database with administrative privileges.
If you want to limit what the code can do, you can initialize it to run as a specific UID. From the documentation on Authenticate with limited privileges:
To get more fine-grained control over the resources a Firebase app instance can access, use a unique identifier in your Security Rules to represent your service.
...
Then, on your server, when you initialize the Firebase app, use the databaseAuthVariableOverride option to override the auth object used by your database rules. In this custom auth object, set the uid field to the identifier you used to represent your service in your Security Rules.
// Initialize the app with a custom auth variable, limiting the server's access
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://databaseName.firebaseio.com",
databaseAuthVariableOverride: {
uid: "my-service-worker"
}
});
Please see the linked documentation for the complete example.

Resources