Firestore security rules for specific document - firebase

I am trying to apply the following situation :
all authenticated users have read and write access to the database except for admin document.
Admin document is accessible only for him for read and write.
My rules:
service cloud.firestore {
match /databases/{database}/documents {
//Functions
function isAuthenticated(){
return request.auth != null;
}
function isAdministrator(){
return request.auth != null && request.auth.token.name == resource.data.oid;
}
//Administrator Identity Check Point
match /admin/identity {
allow read, write: if isAdministrator();
}
//Allow Reads and Writes for All Authenticated Users
match /{document=**}{
allow read, write: if isAuthenticated();
}
}//databases/{database}/documents
}//cloud.firestore
Is there any way i can achieve this, actually when testing these rules, the tests succeed because only isAuthenticated() is being called because of the tag /{document=**}. I also tried /{document!=/admin/identity} but it does not work.
How can I write a security rule that follow this model ?

Maybe on your default user rule you could check if the collection isn't admin, something like this:
//Allow Reads and Writes for All Authenticated Users
match /{collection}/{document=**}{
allow read, write: if (isAuthenticated() && collection != "admin") || isAdministrator();
}

Since June 17, Firebase has provided new improvements to Firestore Security Rules.
Firebase blog - 2020/06 - New Firestore Security Rules features
New Map methods
We'll use Map.get() to get the "roleToEdit" field. If the document doesn't have the field, it will default to the "admin" role. Then we'll compare that to the role that's on the user's custom claims:
allow update, delete: if resource.data.get("roleToEdit", "admin") == request.auth.token.role;
Local variables
Say you're commonly checking that a user meets the same three conditions before granting access: that they're an owner of the product or an admin user.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function privilegedAccess(uid, product) {
let adminDatabasePath = /databases/$(database)/documents/admins/$(uid);
let userDatabasePath = /databases/$(database)/documents/users/$(uid);
let ownerDatabasePath = /databases/$(database)/documents/$(product)/owner/$(uid);
let isOwnerOrAdmin = exists(adminDatabasePath) || exists(ownerDatabasePath);
let meetsChallenge = get(userDatabasePath).data.get("passChallenge", false) == true;
let meetsKarmaThreshold = get(userDatabasePath).get("karma", 1) > 5;
return isOwnerOrAdmin && meetsChallenge && meetsKarmaThreshold;
}
match /products/{product} {
allow read: if true;
allow write: if privilegedAccess();
}
match /categories/{category} {
allow read: if true;
allow write: if privilegedAccess();
}
match /brands/{brand} {
allow read, write: if privilegedAccess();
}
}
}
The same conditions grant access to write to documents in the three different collections.
Ternary operator
This is the first time we've introduced an if/else control flow, and we hope it will make rules smoother and more powerful.
Here's an example of using a ternary operator to specify complex conditions for a write.
A user can update a document in two cases: first, if they're an admin user, they need to either set the field overrideReason or approvedBy. Second, if they're not an admin user, then the update must include all the required fields:
allow update: if isAdminUser(request.auth.uid) ?
request.resource.data.keys().toSet().hasAny(["overrideReason", "approvedBy"]) :
request.resource.data.keys().toSet().hasAll(["all", "the", "required", "fields"])
It was possible to express this before the ternary, but this is a much more concise expression. ;)

Related

Firebase query, rule for collectiongroup query getting in the way

Just when I thought I had the hang of it, the query rules throws me a curve ball :(
I have this query rule:
// Needed for collection group (Member) query
// https://firebase.googleblog.com/2019/06/understanding-collection-group-queries.html
match /{rootPath=**}/Members/{member} {
allow read: if request.auth != null;
}
It's pretty basic, only needs an authorized user. collectiongroup query works perfectly as expected.
Now, I want to have another query just to get member documents:
Firebase.firestore.collection("Companies\\$companyID\\Members").get().await()
The query returns an error (PERMISSION_DENIED).
I also tried adding a rule just for members like this:
match /Companies/{companyID} {
allow read: if request.auth != null &&
isMember(database, companyID, request.auth.uid)
match /Members/{member} {
allow read: if request.auth != null
}
}
Still, the same error.
This is the document path:
I looked at a few resources, but I didn't see anything to suggest a solution:
Understanding Collection Group Queries in Cloud Firestore
Recursive wildcards
I am posting this as an answer, as it is too long for comment.
Have you tried the following example rule:
service cloud.firestore {
match /databases/{database}/documents {
match /Companies/{companyID}/Members/{member} {
allow read, write: if <condition>;
}
}
}
as mentioned earlier in the documentation you shared based on structuring rules with hierarchical data?
I would recommend that you have a look at the following documentation where you can find some examples:
service cloud.firestore {
match /databases/{database}/documents {
match /Companies/{companyID}/Members/{memberID} {
// Only authenticated users can read
allow read: if request.auth != null;
}
}
}
Using the above security rules, any authenticated user can retrieve the members of any single company:
db.collection("Companies/{companyID}/Members").get()
Now , if you would like to have the same security rules applied to collection group queries, you must use version 2:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Authenticated users can query the posts collection group
// Applies to collection queries, collection group queries, and
// single document retrievals
match /{path=**}/Members/{memberID} {
allow read: if request.auth != null;
}
}
}
Any authenticated user can retrieve the members of any single company:
But what if you want to show a certain user their members across all companies? You can use a collection group query to retrieve results from all members collections:
var user = firebase.auth().currentUser;
db.collectionGroup("members").where("author", "==", user.uid).get()
Note: This query requires will require a composite index for the members collection. If you haven't enabled this index, the query will return an error link you can follow to create the required index.
You can try using "match /{path=**}/Members/{member}" instead of rootPath. I have not used the latter but the former worked for me in other projects.

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();
}
}
}

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;

Recursive wildcards in Firestore security rules not working as expected

I have a data structure like this (Collections and Documents rather than JSON of course but you get the idea):
{
users: {
user1:{
name: Alice,
groups: {
groupA:{subbed:true},
groupB:{subbed:true}
}
},
user2:{
name: Bob,
groups: {
groupC:{subbed:true},
groupD:{subbed:true}
}
}
}
}
Basically this is registered users IDs and the group IDs that each user is subscribed to. I wanted to write a security rule allowing access to a users profile and sub-collections only if they are the current auth user and, based on my reading of the docs, I thought that a wildcard would achieve this...
match /users/{user=**}{
allow read,write: if user == request.auth.uid;
}
With this in place I can read the user document fine but I get a permissions error when I try and read the groups sub-collection. I can only make it work by matching the sub-collection explicitly...
match /appUsers/{user}{
allow read,write: if user == request.auth.uid;
match /groups/{group}{
allow read,write: if user == request.auth.uid;
}
}
...so my question is, what is the difference between the two examples and what am I misunderstanding about the recursive wildcards? I thought that the {user=**} part of the first example should grant access to the user document and all its sub-collections, sub-sub-collections etc etc ad infinitum (for the authorised user) and should remove the need to write rules specifically for data stored lower down as I have had to do in the second example.
I've only been messing around with Firestore for a short time so this could be a real dumb question :)
Thanks all
The firebase docs are a bit confusing when it comes to using the recursive while card. What I found in testing was that I needed to set two rules to give a user permission to write to the users document and all sub collections (and their sub documents) which is the most logical setup for managing user data.
You must set two rules.
Give user permission to the /users/{userId} document
Give user permission to all sub collections and their sub documents that begin at the /users/{userId} path.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
match /users/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Rules
Sorry about including the images. I couldn't get SO to format them correctly.
I think the problem is that, while you are indeed using the subcollections wildcard =**, you are then allowing permissions only if user == request.auth.uid, so this is what happens (pseudocode):
(when accessing users/aHt3vGtyggD5fgGHJ)
user = 'aHt3vGtyggD5fgGHJ'
user == request.auth.uid? Yes
allow access
(when accessing users/aHt3vGtyggD5fgGHJ/groups/h1s5GDS53)
user = 'aHt3vGtyggD5fgGHJ/groups/h1s5GDS53'
user == request.auth.uid? No
deny access
You have two options: either you do as you've done and explicitly match the subcollection, or use this:
function checkAuthorization(usr) {
return usr.split('/')[0] == request.auth.uid;
}
match /users/{user=**}{
allow read,write: if checkAuthorization(user);
}
(the function must be inside your match /databases/{database}/documents, like your rule)
Let me know if this works :)
Security rules now has version 2.
match/cities/{city}/{document=**} matches documents in any
subcollections as well as documents in the cities collection.
You must opt-in to version 2 by adding rules_version = '2'; at the top
of your security rules.
Recursive wildcards (version 2).
This is what works for me:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Matches any document in the cities collection as well as any document
// in a subcollection.
match /cities/{city}/{document=**} {
allow read, write: if <condition>;
}
}
}

Resources