Realtime Database Rules - Allowing ID Read But Not Child Values - firebase

As recommended in the firebase documentation, my users are all stored under a main Users node, with each child being a corresponding Firebase Auth user ID for lookup purposes. For example, here's what it looks like:
Users
ABCDEF12345
name: xxx
age: xxx
etc: xxx
How do I make it possible for all users to read the parent node ID (ABCDEF12345), but NOT the underlying data? I'm not able to get this to work with either $user_id or the $uid variables, as they by default provide access to all underlying data and override specific fields because of the cascading effect. I want it to be possible for users to be able to read the user ID so they can perform lookups, but for there to be additional security checks in place in order to access some underlying values (i.e. maybe name will be a public facing variable with no restrictions but age must be a auth.uid === $uid case).
I want to be able to do something like this:
{
"rules": {
"Users": {
"$uid": {
"firstName" {
".read": "some specific conditions here that don't get overridden by $uid access"
...
When I implement something along the above lines, I am able to access the underlying data (firstName) but not the ID itself, unless I do something like this:
"rules": {
"Users": {
"$uid": {
".read": "true"
But this then gives access to everything below ID, which I don't want. I'm struggling to get this to work and the supporting documentation doesn't seem to help, despite them recommending the use of storing your users under an ID like this. Any help would be appreciated.
Thanks!

As Sam commented: permissions in Realtime Database security rules cascade downwards. Once you've given permission to a user at a certain level in the JSON tree, you cannot take that permission away anymore on a lower level.
The common workaround is to create an additional, second data structure that has only the keys that you want to share. So something like:
Users
ABCDEF12345
name: xxx
age: xxx
etc: xxx
UserKeys
ABCDEF12345: true
The UserKeys node here only has the keys you want to share, and a true value for each key as you can't have a path without a value in the database. Now you can grant full read access to UserKeys to only share the keys, and then implement tighter access control on the Users node.
Also see:
Firebase: How to structure public/private user data
Firebase Security Rules: Public vs. Private Data

Related

Firebase security rules based on custom user field

I'm trying to set up security rules (use Firebase Cloud Firestore).
I changed the "users" table (added company_id field) and create "appointments" table (with company_id). I want to implement the following functionality (when a user requests appointments, he only receives appointments with his company id)
Wrote a rule:
match /appointments/{appointment} {
allow write;
allow read, update, delete: if resource.data.company_id == get(/databases/$(database)/documents/users/$(request.auth.uid)).data.company_id;
}
But my code throw error about permissions
const q = query(collection(db, 'appointments'), where("company_id", "==", company_id), orderBy("createdAt"));
Firestore doesn't magically know what documents have what company_id, so it tries to read all documents to return the documents that you want, but it can't, your security rules stop it.
Either get rid of the security rules, or find a way to structure your database so that you don't have this problem.
I don't know enough about your database to tell you a good structure, but what about something like this?
A collection called companies.
Each document in the collection is a company_id
A subcollection called users, where you put all users that belong to this company.
Another subcollection, called appointments, where you put all appointments for the company.
That way, you could write a security rule like so:
match /companies/{company}/appointments/{appointment} {
allow create: if true;
allow read, update, delete: if exists(/databases/$(database)/documents/companies/$(company)/users/$(request.auth.uid))
}
The problem with this approach is that, because we have divided all users and appointments into different collections, it will be impossible to query for all existing users or for all existing appointments.
It all depends on your use case.

Firestore security rules get(). exists()

I have documents in Firestore like the following:
BID: "123"
From: "xxx"
Opn: true
I need to check if there are another document have BID == "123" and Opn == true before create the document, because it's not possible to have one more document where Opn is true with same BID.
I try to use get() and exists but it's not working with 2 data fields.
What I tried:
function checkIfThereOpenRoomForSameBBB(xxx) {
return !(
(get(/databases/$(database)/ChatRooms/$(ChatRoom)).data.BID == xxx) &&
(get(/databases/$(database)/ChatRooms/$(ChatRoom)).data.Opn == true)
);
}
is there any solution for this case?
Security rules can't search for data in the database, as that would be prohibitively slow and expensive. All they can do is check if a document exists at a specific path, or read a document as a specific path and check its contents.
This means that any time you want to check if something exists, you'll need to ensure that lives at a known path. So if the combination of BID and Opn=tru must be unique, you should create a collection where the key of each document consists of the BID value and Opn=true.
If this is a global requirement for your app, you could even use this key in your existing collection instead of the (likely auto-generated) key you currently use.
Also see:
Prevent duplicate entries in Firestore rules not working
firebase rule for unique property in firestore
I want to write a rule that will don't allow add same document second time

How to only get partial data set from firebase query

I'm attempting to create a database of Users in firebase.
The structure is as follows:
users: {
uid: {
username: exampleName
email: exampleEmail
personalInfo: examplePersonalInfo
}
}
Currently, I'm able to get the ENTIRE data object from the query I run by doing:
db.database.ref('users').orderByChild('username').equalTo('exampleName')
.once('value').then(handler);
I want to query the database to make sure no other individual has that username, but the problem I'm running into is I get ALL the data for the specific uid.
I only need the username, and would like to establish rules where:
personalInfo: {
".read" = "$uid === auth.uid"
}
but
username: {
".read" = true
}
is this possible, and is there a solution here to only query and receive the username?
Thank you!
When a query returns a child node, that child node will contain everything underneath it. You can't further filter the children to return. In fact, with security rules, once you grant access to the client to a particular child node, the client has implicit access to everything under it. You can't reject access to a child node of a parent node that you've already granted access.
If you need to limit the size of the result set by selecting only necessary children, you will have to duplicate the data into a structure that contains only the data needed for this particular query. This is common in nosql type databases. The upside is faster and smaller reads. The downside is larger amount of storage, and you'll need to keep the duplicates in sync.

Firestore security rules based on request query value

I'm trying to secure requests to a collection to allow any single get, but only to allow list if a specific key is matched.
Database structure is like this:
projects
project1
name: "Project 1 name"
board_id: "board1"
project2
name: "Project 2 name"
board_id: "board2"
boards
board1
board2
The Firestore query I'm making from Vue:
// Only return projects matching the requested board_id
db
.collection("projects")
.where("board_id", "==", this.board_id)
The security rules I'd like to have would be something like this:
match /projects/{project} {
allow get: if true // this works
allow list: if resource.data.board_id == [** the board_id in the query **]
// OR
allow list: if [** the board_id in the query **] != null
I want to do this so you can list the projects in a specific board, but can't just list everything.
Is there a way to access the requested .where() in the security rules or do I need to nest my projects collection inside my boards collection and secure it that way?
It really depends on how you want to query data in the future. If you have no requirement to list all of the projects (irrespective of the board), then your current data model is better and can be secured by adding the allowed boards as a map {board_id: true} or (ideally) sub-collection to the /users document.
Current data model
Database
/projects/{project_id}
/boards/{board_id}
/users/{uid}/boardPermissions/{board_id}
Security rules
match /projects/{project} {
allow list: if exists(/databases/$(database)/documents/users/$(request.auth.uid)/boardPermissions/${resource.data.board_id})
Alternative data model
If you want to totally partition your data (which is what I tend to do for many of my projects), then create the following model
Database
/boards/{board_id}/projects/{project_id}
/users/{uid}/boardPermissions/{board_id}
Security rules
match /boards/{board_id}/projects/{project_id} {
allow list: if exists(/databases/$(database)/documents/users/$(request.auth.uid)/boardPermissions/${board_id})

Cloud Firestore: Query secured documents by field content

EDIT: Seems to be an open issue in Firestore. Also see this post.
In Google Cloud Firestore, I want to model a collection of groups. Each group contains a name, the list of it's users and some secretGroupData. For me, the natural way to do this would be:
/groups
/group1 {
name: "Group 1"
users: { //object can be queried, simple array not
"user1": true,
"user5": true
}
secretGroupData: ...
}
/group2 { ... }
Given a user like user1, I want to query all groups he is member of. This query works fine:
groupsRef.where("users.user1", "==", true)
However, I want to secure the group data. This query only works, when all groups are readable for all users. When I protect the group to be readable only by the group members, by the rule
match /groups/{groupId} {
allow read: if resource.data.users[request.auth.uid] == true;
}
the above query does not work any more, because as soon as it sees a group where the current user is not a member of, read access is denied and the whole query fails.
What is the best solution for this problem in Firestore? Should I
tell Firestore to return only the allowed groups and ignore the other ones, instead of throwing an error? If so, how can I achieve this?
make the groups readable for all users and move the secretGroupData into subcollections, where I can then restrict the access to just the group members
add redundancy by adding the IDs of all groups of a user into the user's profile document (/users/user1/groupIds: ["group1"]), so I know the groups beforehand and can query them by ID
use a totally different solution?
Thank you very much for your ideas.

Resources