Restricting access to Firebase Firestore and Storage - firebase

I'm using Firebase exclusively in my app - Auth, Firestore, Functions and Storage.
Within Firestore, my data is structured as below:
/users/<user_id>/<user_data>
/listings/<listing_id>/<listing_data>
<listing_data> contains a user_id field.
Within Storage, my data is structured as below:
/images/<user_id>/<image_id>/<images>
I have the following scenario:
A user must only be able to access their listings
A user must only be able to download their files
A user can can share their name from their <user_data> with selected users
A user can share selected images with selected users
I have no ideas how I can go about this. At the moment, anyone can access anything if they're authenticated, so I guess the first step is to lock this down, and then some how assign rights?
I thought about adding an access list object, and then writing middleware to check this, but it doesn't feel like the correct way

You have to modify the rules of firestore as:
service cloud.firestore {
match /databases/{database}/documents {
// Restaurants:
// - Authenticated user can read
// - Authenticated user can create/update (for demo)
// - Validate updates
// - Deletes are not allowed
match /restaurants/{restaurantId} {
allow read, create: if request.auth.uid != null;
allow update: if request.auth.uid != null
&& request.resource.data.name == resource.data.name
allow delete: if false;
// Ratings:
// - Authenticated user can read
// - Authenticated user can create if userId matches
// - Deletes and updates are not allowed
match /ratings/{ratingId} {
allow read: if request.auth.uid != null;
allow create: if request.auth.uid != null
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if false;
}
}
}
}
The root of my database is restaurants.You have to replace those parameters with that of yours.

Related

Limiting permissions to the author not working in Firestore rules

I am attempting to move my Firebase rules from testing where every user could read and write every document to one where only the author can update or delete documents they create.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if request.auth != null;
allow update, delete: if request.auth != null && request.auth.uid == resource.data.author_uid;
allow create: if request.auth != null;
}
}
}
This set of rules is resulting in a 'Missing or insufficient permissions" error at Firebase init and if I attempt to delete a document. If I go with my original rules then everything works.
allow read, create: if request.auth != null
I do a standard firebase.initializeApp (but dont want to publish my keys here - lets just say the same initialize works with the basic rules and on three other firebase projects I have). The delete call is as follows and works with the simpler rule set as well but not the tighter rules above:
const decrement = firebase.firestore.FieldValue.increment(-1);
firestore.collection('story').doc(storyid).delete().then(function() {
firestore.collection('users').doc(getUserID()).update({
saves: decrement
});
})
(thank to Sam Stern on the FB team for guidance)
First, there was a mistake in the rules description. While request.auth.uid is defined by firebase the resource.data.author_id needs to be defined by the developer on each of their documents. In this case the 'story' document contains a 'creator' property that is the userid of the owner. So the correct variable would be resource.data.creator in the above rules.
In addition its possible to define the documentid as the userid, as is often the case when you are creating a 'user' object for each account in your firebase app. In this case the userId is the same as the documentId so the following rule would control update permissions that only allow the owner of that document to change it.
match /users/{userId} {
// In this scope the "userId" variable is equal to the documentId because of the wildcard declaration {userId}
allow update: if request.auth.uid == userId;
}

Firebase firestore security rules sharing data

I have this data structure in firestore where I'm trying to link user to profile then to event. A profile can be shared by multiple users and should be able to access events for that profile.
user
- id
- email
- name
- profilePicUrl
profile
- id
- name
- dateOfBirth
- owners: [ "user1","user2" ]
- etc.
event
- id
- profileId
- name
- startDate
- endDate
I currently have:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{id} {
allow read, write: if request.auth.uid == id;
}
match /profiles/{id} {
allow read, write: if ("owners" in resource.data && resource.data.owners != null && request.auth.uid in resource.data.owners);
}
match /events/{id} {
allow read, write: if hasAccess(userId, resource) == true;
}
}
}
function hasAccess(userId, resource) {
// Not sure what to put here but basically need
// to get profiles where user is owner
// and get events for these profiles
}
But not sure what to put in the hasAccess function. Appreciate if someone can guide me.
UPDATE 2019/10/11
Somehow I got this to work by using the following rule:
match /events/{id} {
allow read, write: if (exists(/databases/$(database)/documents/profiles/$(resource.data.profileId)) &&
"owners" in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data &&
get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners != null &&
request.auth.uid in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners);
}
UPDATE 2019/10/14
I have some permission issues with the write so I had to revise it as shown below:
match /events/{id} {
allow read: if ( exists(/databases/$(database)/documents/profiles/$(resource.data.profileId))
&& "owners" in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data
&& get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners != null
&& request.auth.uid in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners);
allow write: if ( request.auth.uid in get(/databases/$(database)/documents/profiles/$(resource.data.profileId)).data.owners );
}
What you're trying to do is actually not possible with security rules given the existing structure of your data. This is due to the fact that security rules can't perform queries against collections. The only thing you can do is get() a specific document using its known path in order to read its fields, which isn't going to help you link up documents where you can't build that path.
What you can do instead is duplicate the data required for the rule into each document that needs to be protected. This means each event document needs to have a copy of each list of owners as a field. Yes, that is going to be more hassle to keep all the events up to date if the list of owners for an event has to change.

Firestore Security Rules: Allow User To Create Doc Only If New Doc ID is same as User ID

When users log in for the first time, I need to also call a function that creates a document in my firestore users collection to store their profile data. Using Web SDK.
(I was previously using a new user triggered event with firebase functions, but it was too slow to wait for a cold function to spin up).
Security Rule Requirements
Needs to ensure that the user can only create a document if the document id is the same as their user id (to prevent the user from creating other docs). Needs to ensure that this doc doesn't already exist.
Attempt - Works In Simulator, Not IRL
These tests pass in the simulator, but not IRL.
// Allow users to create a doc if the doc ID == their user id
allow create: if path("/databases/" + database + "/documents/users/" + request.auth.uid) == request.path;
OR
allow create: if /databases/$(database)/documents/users/$(request.auth.uid) == request.resource['__name__']
Have also tried this (again, works in simulator, but not IRL)
match /users/{userId} {
// Allow users to read their own profile
allow create: if request.auth.uid == userId;
}
Update
I recently had to update my rule set because of some changes to the way firestore rules worked, and changes in how the "getAfter" function works. Specifically, I am now able to use request.resource for data comarisons. Anyways, it appears that I can accomplish my goals with simpler rules now so I thought I'd update this answer and share.
Goals
User can create a document, only if the new document ID matches their user ID.
User cannot declare themselves an "admin", block create / update / write requests if "admin" is a field (unless they are already an admin)
service cloud.firestore {
match /databases/{database}/documents {
// Allow users to create a document for themselves in the users collection
match /users/{document=**} {
allow create: if request.resource.id == request.auth.uid &&
!("admin" in request.resource.data);
}
// Allow users to read, write, update documents that have the same ID as their user id
match /users/{userId} {
// Allow users to read their own profile (doc id same as user id)
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 - unless they are already an admin
allow write, update: if request.auth.uid == userId &&
(
!("admin" in request.resource.data) ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true // allow admin to update their own profile
)
// Allow users to read their own feeds
match /feeds/{document=**} {
allow read: if request.auth.uid == userId;
}
}
}
}
Old Answer
So I figured out how to do this in a workaround way. I also had some additional write / update conditions that prevent the user from changing their permission level. This was for some reason, preventing any "creates" from happening. So I had to mirror the same conditions in create, and the write / update rules. For some reason this was necessary.
This new rule structure accomplishes the following
First Section, for create rule
allows the only authenticated users to create documents only in the "users" collection (during the user setup process, a document is created automatically with the same ID as their user id).
does not allow creation of a document containing the "admin" field, which would suggest they are trying to gain admin access.
it seems that validating the id of the document during creation is not possible, hence additional write / update rules below
Second Section - read, update, write
allows users to read / write / update only documents that have the same ID as their user id (user trying to create a document with an ID other than their user id will fail, also prevents the user from spamming creation of tons of docs by manipulating the client-side JS request.)
does not allow users to write / update their profile to include the "admin" field
Rules
service cloud.firestore {
match /databases/{database}/documents {
// Allow users to create documents in the user's collection
match /users/{document=**} {
allow create: if request.auth.uid != null &&
!("admin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
}
// Allow users to read, write, update documents that have the same ID as their user id
match /users/{userId} {
// Allow users to read their own profile (doc id same as user id)
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 &&
!("admin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
}
}
}
PS
This was not intuitive at all, so if someone has a better workaround, please post it. Also, I'm really hoping that once firestore 1.0 is out, it will bring with it some huge improvements to rules and rule documentation.
A little bit late, but I manage to tweak one of your possible solutions and make it work:
allow create: if path("/databases/(default)/documents/users/" + request.auth.uid) == request.path;
Just had to replace the database variable with (default). Yes, not fancy...
The solution i came up with. My tests showed it's not possible to create other user-docs than the own uid and it prevents normal users to change any admin state.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin == true ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
}
function signedIn(){
return request.auth.uid != null;
}
match /users/{user} {
// allow updates to own user-doc
allow read, update, delete: if request.auth.uid == user &&
// allow updates to own user-doc if "isAdmin" field is the same as before the update (in case user was already admin)
(request.resource.data.isAdmin == resource.data.isAdmin ||
// or allow updates if "isAdmin" will be set to false
request.resource.data.isAdmin == false ||
// or allow updates if no "isAdmin" field exists after the update
!("isAdmin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data)
);
// allow creation of user-doc with own uid and no others
allow create: if request.auth.uid == user &&
// if no "isAdmin" field is set
!("isAdmin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
// give full access to admins
allow read, write: if isAdmin();
}
}
}

Firestore Rules: validate data does not have field

So I currently have two roles for all users: isAdmin and isReader.
An Admin is allowed to read and write data and an Reader is allowed to read data.
When someone creates an account he has no rights. Not even isReader. Only an Admin can change rules.
This is how I planned to do it:
Once someone creates an account I create an Document in the Users Collection like this:
uid: user.uid,
email: user.email,
role: {
isAdmin: false,
isReader: false,
}
On each login I update 'email' and uid but keep role untouched. To secure this behaviour I have these rules:
match /Users/{userId} {
allow read: if isOwner(userId) || isAdmin();
allow create: if request.resource.data.hasAll(['uid', 'email', 'role']) && request.resource.data.role.isAdmin == false && request.resource.data.role.isReader == false;
allow update: if resource.data.role == null || isAdmin();
}
function isAdmin() {
return getUserData().role.isAdmin == true;
}
I think I have 2 errors:
for some reason the data.hasAll(['uid', 'email', 'role']) does not work. When I remove this part the create rule works as planned.
resource.data.role == null does not work. I intend to check if the data contains any updates for role because I can't allow it is it doesn't come from an Admin. But for some reason it does not work.
Any Ideas what I'm doing wrong? Also is my strategy save or is there a way someone could "hack" himself Reader or Admin rights?
This looks like it may be a good use case for custom auth claims. You can set specific roles on a user within a secured environment, as shown in this codelab. Below is an example of setting a custom claim in your server. You can even use Cloud Functions for this. I recommend you check out the full code of the Codelab so you can see how to ensure not just anyone can request custom claims be added to their user.
admin.auth().setCustomUserClaims(uid, {Admin: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
Then you can check for those roles on the user in your security rules.
service cloud.firestore {
match /databases/{database}/documents {
match /Users/{userId} {
allow read: if request.auth.token.Owner == true || request.auth.token.Admin == true;
allow create: request.auth.uid == userId &&
request.resource.data.uid == request.auth.uid &&
request.resource.data.email != null;
allow update: request.auth.uid == userId || request.auth.token.Admin == true;
}
}
}
Notice all the rules about "role" have been removed because they're no longer needed. Let me know if you have questions about implementation because I'm trying to make some more content around this since it's such a common problem.
request.resource.data.hasAll(['uid', 'email', 'role']) does not work, because request.resource.data is a Map and not a List. You should use keys() to create a List from the Map and ensure certain keys exist.
In regards to your second issue, you should just check whether there is a write to roles: allow update: if !('roles' in request.writeFields) || isAdmin();. This will ensure that any updates to roles will fail unless a user is an Admin.
About your security question; I see a couple issues. The first is anyone can create unlimited users which also means that any Admin can create unlimited other Admin accounts. To stop this from happening, I would add another section to the allow create that restricts creation to the user:
allow create: if userId == request.resource.data.uid
&& request.auth.uid == request.resource.data.uid
&& request.resource.data.hasAll(['uid', 'email', 'role'])
&& request.resource.data.role.isAdmin == false
&& request.resource.data.role.isReader == false;`
The second is anyone can change their uid and try to impersonate someone else. Obviously this doesn't change the uid associated to their Auth Token, but depending on how you write the rest of your rules, the backend security, or even the frontend display, someone could use that flaw to exploit your code or another user (potentially an Admin). You can ensure no one changes their uid by checking whether it is in the writeFields (you will also need the previous security solution to also ensure they don't impersonate during creation).
allow update: if !('uid' in request.writeFields)
&& (!('roles' in request.writeFields) || isAdmin());
I hope this helps.

Configuring rules for Firestore so user only gets records they own

This is a followup to this question
Firestore permissions
I'm trying to set rules on my firestore
service cloud.firestore {
match /databases/{database}/documents {
match /analysis/{analysis} {
allow read, write: if request.auth.uid == resource.data.owner_uid;
}
}
}
My goal is
a. When doing a list operation only those documents belonging to a user are returned
b. only documents a user owns can be read or written by that user.
With the above configuration b. is accomplished.
how do I do accomplish a. ?
Remember that firestore rules are not filters, they're a server-side validation of your queries.
You should always make your queries match your rules, or else you'll get permission errors.
In your case you already made the rule to enforce reading/listing on user owned documents. Now you simply have to make the corresponding query with the right filters :
const userId = firebase.auth().currentUser.uid
db.collection("analysis").where("owner_uid", "==", userId)
Another thing.
With your current rules, your users won't be able to create a new document, only edit an existing one, here are the updated rules to allow that :
allow read: if request.auth.uid == resource.data.owner_uid;
allow write: if request.auth.uid == resource.data.owner_uid
|| request.auth.uid == request.resource.data.owner_uid;

Resources