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.
Related
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
I'm trying to store locations nearby me for a test. I ran the following code:
var macro_query = db.collection("cafes");
macro_query = macro_query.where("name", "==", place.name); // check whether name exists
console.log("Checking final query: ", macro_query); // check whether it exists
macro_query.get()
.then(function(querySnapshot) {
console.log(querySnapshot.empty); // returns true if empty, returns false if place.name is already in database
if (querySnapshot.empty) {
db.collection("cafes").add({ // store it since it doesnt exist
name: place.name,
});
console.log("Added into database: ", place.name);
} else {
console.log("Location already in database");
}
});
The logic of the code is basically to check the database if place.name already exist. If it does, the script does nothing, otherwise the place should be added to the database.
The code works fine after a couple of tests, to make sure that it never adds the same location twice. But after running it many many times, i noticed that in my firestore there could be more than 2 unique keys with the same location (meaning, 3 locations with the exact same name)
Is my code failing somewhere and I'm not matching it correctly?
Side note, I'm wondering if this is because of my security rules?
// Allow read/write access to all users under any conditions
// Warning: **NEVER** use this rule set in production; it allows
// anyone to overwrite your entire database.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
This has nothing to do with security rules.
Your code has a race condition. Since there is some brief period of time between the query and the update, that means you could end up with multiple concurrent queries each adding a store with the same name. Unfortunately, for Firestore web and mobile clients, there is no way to make this sort of query atomic in order to avoid the race condition. Firestore transactions won't help you here, since you can't do a query inside of a transaction.
If you want to atomically check-and-set, the thing to check must the document ID, not a field in the document. If you're looking for a specific document by ID, you can then use a transaction to only create the document if it doesn't already exist. This also implies either one of two things:
Your store names are also valid document IDs
Or you have a dedicated collection for storing encoded store names as document IDs, with parallel documents in another collection that actually contain the store data.
If you choose #2, you are in for quite a bit of work to make that happen smoothly.
How do you get all documents in a collection, for which the current user has read permissions?
Trying to get all documents results in a permissions error, because it includes attempts to read documents where the user does not have permission (rather than returning the filtered list of documents).
Each user in this app can belong to multiple groups. Reads are locked down to the groups that they have been added to.
match groups/{group} {
allow read: if exists(/databases/$(database)/documents/groups/$(group)/users/$(request.auth.uid));
}
Here's how this would look with a hypothetical subcollection-contains-id operator.
firestore()
.collection("groups")
.where("users", "subcollection-contains-id", user.uid);
As a temporary workaround I've moved this logic to a cloud function. Here's a shorthand of how it works.
for (let group of firestore().collection("groups")) {
let user = firestore.doc(`groups/${group.id}/users/${uid}`);
if (user.exists) {
// Send this group id to the client
}
}
How can I keep these concerns together and move this logic to the client side without relaxing the security rules?
You could add owners field in the documents inside a collection
owners: ["uid1", "uid2"]
Then, you could get all the posts with uid by searching with array_contains
ref.where("owners", "array-contains", uid)
In rules, you could add sth like these:
allow read: if request.resource.data.owners.hasAny([request.auth.uid]) == true
allow update: if request.resource.data.owners.hasAny([request.auth.uid]) == true
Firebase real time database.
I am trying to limit number of items returned from a query only by changing the db rrules on firebase
Is this possible? I dont want to change the app side code
What is the rule if i have to fetch top 100 using the limittofirst.
Firebase's server-side security rules merely determine whether a certain operation is allowed. They don't filter data by themselves.
If you want to retrieve the first 100 items, put a limitToFirst(100) in your query.
If you only ever want the first 100 items to be retrieved (as in: want other read operations to be rejected), have a look at the documentation on securing queries, which contains this example:
You can also use query-based rules to limit how much data a client downloads through read operations.
For example, the following rule limits read access to only the first 1000 results of a query, as ordered by priority:
messages: {
".read": "query.orderByKey &&
query.limitToFirst <= 1000"
}
Example queries:
db.ref("messages").on("value", cb) // Would fail with PermissionDenied
db.ref("messages").limitToFirst(1000)
.on("value", cb) // Would succeed (default order by key)
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.