Firebase Realtime Database Rules checking for array contains - firebase

My auth token includes a stores property. This property is an Array that includes ID's of all stores the user is allowed to access. For example:
stores: ['abc123', 'def456'].
Now, in my Firebase Realtime Database, I am saving store specific data like this:
{
"stores": {
"abc123": {
"data": "mydata"
},
"def456": {
"data": "mydata"
}
}
}
Users are allowed to access the realtime database data if their stores property array includes the storeID specific data they want to access.
What I would like my rules to look like:
{
"rules": {
"stores": {
"$store_id": {
".read": "auth.token.stores.includes($store_id)"
}
}
}
}
This is not possible. I get the following error:
Error saving rules - line 5: type error: function call on target that is not a function.
Is it possible to search trough token property arrays in the firebase rules or will I have to use an object? Thanks for any answers!

Firebase Realtime Database does not have any includes() to check if a value if present in an array or not. You can store the claims as a map instead of an array as shown below:
// custom claims
{
stores: {
store1: true,
store2: true,
store3: false
}
}
Then you can use the following rules to check if user has access to a particular store or no:
{
"rules": {
"stores": {
"$store_id": {
".read": "auth.token.stores[$store_id] == true"
}
}
}
}

You can use in operator from List
v in x
Check if value v exists in list x.
'a' in ['a','b'] == true
Sample Security Rules how it can be done
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
match /stores/{storeId} {
allow read: if isAuthenticated() && isUserAllowed();
}
function isAuthenticated() {
return request.auth != null;
}
function isUserAllowed() {
return request.auth.uid in resource.data.stores
}
}
}
}

Related

Making a client-side request to satisfy a Firestore permissions rule that uses ACLs as arrays with hasAny()

Consider the following :
A Firebase auth user has an ACL (access control list)
user.roles = [ a, b, c ]
Each record in a collection must be secured with a list of roles that are permitted to perform read/write operations on the document:
resource.access.roles = [ c, d, e ]
If there is an intersection between the two arrays, the operation should be permitted.
function userHasAccess (resource) {
return getUser().roles.hasAny(resource.data.access.roles)
}
match /{collection}/{id} {
allow read: if userHasAccess(resource)
}
Note: in the Firebase console, getUser().roles is an array and resource.data.access.roles is an array and there IS an intersection between them. Consequently, IN THE CONSOLE, the request is permitted.
Question: how to query this collection from the client whilst satisfying the rule.
What I had expected is that this would work.
ref
.collection(collection)
.where('access.roles', 'array-contains-any', user.roles)
... would satisfy the rule and allow the read operation, but it does not. It throws standard permissions error, even though I can read the ref.collection(collection).doc(id) just fine.
FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
Would appreciate an example or better a ref to the docs where it shows how to formulate a query in the client that satisfies the hasAny() rule that the framework provides.
** EDIT: ** Added full details per Doug's recommendation:
The database is structured as follows:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAuthUser(userId) {
return request.auth.uid == userId;
}
function isTenantUser(env, tenantId) {
return get(/databases/$(database)/documents/envs/$(env)/usersMap/$(request.auth.uid)).data.tenant == tenantId
}
function getUser(env, tenantId) {
return get(/databases/$(database)/documents/envs/$(env)/tenants/$(tenantId)/users/$(request.auth.uid)).data
}
function userHasAccess (env, tenantId, resource) {
return resource.data.access.role in getUser(env, tenantId).roles
}
// note if set env here, have to pass through all fns to match path
match /envs/{env} {
match /usersMap/{userId} {
allow read: if isAuthUser(userId)
}
match /tenants/{tenantId} {
allow read: if isTenantUser(env, tenantId)
allow update: if getUser(env, tenantId).account.isAdmin
match /lists/{settingId} {
allow read, update: if isTenantUser(env, tenantId)
}
match /settings/{settingId} {
allow read: if true
allow update: if getUser(env, tenantId).account.isAdmin
}
// todo: users can't change admin flag or email
match /users/{userId} {
allow read: if true
allow create: if getUser(env, tenantId).account.isAdmin
allow update: if getUser(env, tenantId).account.isAdmin || request.auth.uid == userId
}
// todo: permissions on read/update
match /{collection}/{id} {
allow read, update, delete: if userHasAccess(env, tenantId, resource)
allow create: if true
}
}
}
}
}
Based on this, I want to secure access to the following url:
/databases/$(database)/documents/envs/$(env)/tenants/$(tenantId)/{collection}/{documentId}
eg:
/databases/$(database)/documents/envs/devleopment/tenants/shalom-shul-20AZ/cases/case-one
with ACLs where the user record is:
and the case record is:
Under these conditions, the rules evaluate as allowed in the firebase console, but the client-side query as posted returns denied error (even though i can read any individual doc just fine).

Problem with write in subcollections Firestore Rules

I have a Firestore database and has the following structure:
collection (teachers): {
doc (id): {
name: name
lastName: lastName
collection (classes): {
doc (id): {
name: math
}
}
}
}
What I'm trying to achieve is that the headmaster is able to get the teacher's name and add/create some classes for the very same teacher. The problem comes with adding the Firestore rules. I've tried this three rule possibilities, but none worked as expected. I was able to read, but no writing was possible.
1
service cloud.firestore {
match /databases/{database}/documents {
match /teachers/{teacher} {
allow get if true;
allow create if true;
}
}
}
2
service cloud.firestore {
match /databases/{database}/documents {
match /teachers/{teacher} {
allow get if true;
}
match /teachers/{teacher}/classes/{class} {
allow create if true;
}
}
3
service cloud.firestore {
match /databases/{database}/documents {
match /teachers/{teacher} {
allow get if true;
match /classes/{class} {
allow create if true;
}
}
}
I'm working with angularfirestore2, by the way.
What I get from your question is that, you want to give read, write for /teachers/{teacher}/classes/{class} to some users with headmaster role.
For that, first you need to check which users are headmasters.
If your teachers doc id is same as userid created in firebase auth, you can add a data field to your teacher document named isHM and set this to true, if user is headmaster:
collection (teachers): {
doc (id): {
name: name
lastName: lastName
isHM: true
collection (classes): {
doc (id): {
name: math
}
}
}
}
Now add the following rule:
service cloud.firestore {
match /databases/{database}/documents {
match /teachers/{teacher}/classes/{class} {
function isHeadMaster() {
return get(/databases/$(database)/documents/teachers/$(request.auth.uid)).data.isHM;
}
// HM can read, write classes
allow read, write: if isHeadMaster == true;
}
}
}
or else you need to create a different collection with firebase userid as the doc id and add isHM field if the user is headmaster, as follows:
collection (headmasters): {
doc (uid): {
----
isHM: true
----
}
}
and then add following rule:
service cloud.firestore {
match /databases/{database}/documents {
match /teachers/{teacher}/classes/{class} {
function isHeadMaster() {
return get(/databases/$(database)/documents/headmasters/$(request.auth.uid)).data.isHM;
}
// HM can read, write classes
allow read, write: if isHeadMaster == true;
}
}
}
To find more role based access rules, check this https://firebase.google.com/docs/firestore/solutions/role-based-access

Firestore Rule allowing overwrite

I have a users collection where each document contains a name field and and an access map.
"users" :[
{
"mHbVq5TUY7brlleejClKm71NBGI2": {
"name": "Bob Johnson",
"access": {
"X0w1VaVIljR1Nc5u3Sbo" : true
}
}
]
I would like the Firestore rules to allow creation of a new document only if it doesn't already exist and only if the person performing the action has had their email verified. For the update, only the user owning that node can perform the update, the name must be a string, and the access map should not be able to be changed. I tested my update and create rules in the simulator and they worked as expected. However, when I run a .set() it completely overwrites my entire node and removes the access map which I cannot have happen. I assume that a .set() is actually performing an update and thus meeting my update criteria. So, how do I prevent someone from completely overwriting my node. Thanks in advance...code below.
---CODE PERFORMING OVERWRITE
db.collection("users").doc("mHbVq5TUY7brlleejClKm71NBGI2").set(
{
name: "Bill Swanson",
}
).catch(err => {
console.log(err.message)
})
---RULES
function incomingData() {
return request.resource.data
}
function emailVerified() {
return request.auth.token.email_verified;
}
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
function userExists(user_Id) {
return exists(/databases/$(database)/documents/users/$(user_Id));
}
allow create: if !userExists(userId) && emailVerified();
allow update: if request.auth.uid == userId
&& !('access' in incomingData())
&& request.resource.data.name is string;
allow read: if request.auth.uid != null;
}
}
}
When using set(), If you're not sure whether the document exists, pass the option to merge the new data with any existing document to avoid overwriting entire documents.
Here's how to pass the option to merge the update with the existing document.
db.collection("users")
.doc("mHbVq5TUY7brlleejClKm71NBGI2")
.set(
{
name: "Bill Swanson"
},
{
merge: true
}
).catch(err => {
console.log(err.message)
});
Hope this helps.

How to prevent duplicate document entries into firestore based on fields in Firestore rules

I'm trying to prevent the creation of a new document if there is one that currently exists in the collection with two id fields with the same values as the incoming data.
Right now, I'm trying to attempt this with Firestore rules.
I've found way no way in the simulator to actually do this as I need to test the incoming data against every document in the collection.
I'm not quite sure what I'm missing here...
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
}
match /someCollection/{someCollectionID} {
allow write: if existingData().someValue != incomingData().someValue &&
existingData().anotherValue != incomingData().anotherValue
}
}
function existingData() {
return resource.data
}
function incomingData() {
return request.resource.data
}
}

Firestore rules objects

I am trying to setup some basic Firestore security rules in my database. I am having some trouble finding the relevant documentation to learn how to do this.
Currently my document is structured like this:
project(document): {
createdBy(string): chris#emailaddress.com,
users(object): {
{ graham#emailaddress.com(object): access: write },
{ paul#emailaddress.com(object): access: read }
}
}
I'd like to setup my rules so that:
Users must be signed in to read, write or delete anything
If a user is added to a project with 'read' access they can only read the document.
If a user is setup with write access they can update and read the document but not update the createdBy field.
If a user has created the document they can read, update and delete the document.
My security rules are setup like this:
service cloud.firestore {
match /databases/{database}/documents {
match /projects/{projectId} {
allow read: if existingData().users[getUser()token.email].access != null && isSignedInAndVerified()
allow read, update: if existingData().users[getUser()token.email].access != "write" && isSignedInAndVerified()
allow update, delete: if sameAsEmail(existingData().createdBy) && isSignedInAndVerified()
}
//my functions
function getUser(){
return request.auth
}
function existingData(){
return resource.data
}
function sameAsEmail(resource){
return resource == request.auth.token.email
}
function isSignedInAndVerified() {
return request.auth != null && request.auth.token.email_verified;
}
}
}
Incorrect use of syntax use: getUser().token.email instead.

Resources