I have a data structure that looks like:
{
"farms" : {
"-J3saHfe8_2GRqn3jOTc" : {
"stockunits" : {
"-J4CAeRI-IKKXVuNILCv" : {
"name" : "Chickens",
"totalStockUnits" : 115
}
},
"owner" : 3,
"name" : "smith"
},
"-J3sbbqsGabs5cLaj0zP" : {
"owner" : 2,
"name" : "brown"
}
}
}
When I query "/farms" I would like to get a list of the farms for which the authenticated user is the owner. I would also like to prevent users from viewing farms that they are not the owner for.
My rules currently look like this:
{
"rules": {
".write": false,
".read": false,
"farms": {
"$farm": {
".read": "data.child('owner').val() == auth.id"
I get that by putting the .read at this level I am granting read access to individual farm when accessed via /farms/:farmid. And if I move the ".read" up to the "farms" level this results in granting read access to the entire list.
So I think I need to restructure my data but aren't sure how best to go about this? I don't want to add the farms object below the user object as I will be wanting to add a number of "editor" users to any given farm as well.
Thanks for your help
You'll need to denormalize your data and include references to individual farm objects under each user's profile. There's a full example of how to do this kind of denormalization in this blog post: https://www.firebase.com/blog/2013-04-12-denormalizing-is-normal.html
In that example, the code snippets show organizing comments under posts, you can use a similar technique to map farms to users.
Related
I am stuck trying to allow an an array of admins access to their data.
I have a database structure like this:
{
"Respondents": {
"Acme Corp": {
"admins": ["mMK7eTrRL4UgVDh284HntNRETmx1", ""mx1TERNmMK7eTrRL4UgVDh284Hnt"],
"data": {data goes here...}
},
"Another Inc": {
"admins": ["Dh284HmMK7eTrRL4UgVDh284HntN", ""x1TERNmx1TERNmMK7eTrRL4UgVDh"],
"data": {their data goes here...}
}
}
}
And then I tried to set my rules like this
{
"rules": {
"Respondents": {
"$organisation" : {
".read": "root.child('Respondents').child($organisation).child('admins').val().includes(auth.id)",
".read": "root.child('Respondents').child($organisation).child('admins').val().includes(auth.id)"
}
}
}
}
..but that won't parse in the Firebase Database Rules editor
I get "Error saving rules - Line 7: No such method/property 'includes'", but I need something to match the user id with the array of admins.
Any experience or suggestions?
As you've found, there is no includes() operation in Firebase's security rules. This is because Firebase doesn't actually store the data as an array. If you look in the Firebase Database console or read this blog post you will see that Firebase stores it as a regular object:
"admins": {
"0": "mMK7eTrRL4UgVDh284HntNRETmx1",
"1": "mx1TERNmMK7eTrRL4UgVDh284Hnt"
}
And since that is a regular JavaScript object, there is no contains() method on it.
In general creating arrays are an anti-pattern in the Firebase Database. They're often the wrong data structure and when used are regularly the main cause of scalability problems.
In this case: you're not really looking to store a sequence of UIDs. In fact: the order of the UIDs doesn't matter, and each UID can be meaningfully present in the collection at most once. So instead of an array, you're looking to store set of uids.
To implement a set in Firebase, you use this structure:
"admins": {
"mMK7eTrRL4UgVDh284HntNRETmx1": true,
"mx1TERNmMK7eTrRL4UgVDh284Hnt": true
}
The value doesn't matter much. But since you must have a value to store a key, it is idiomatic to use true.
Now you can test whether a key with the relevant UID exists under admins (instead of checking whether it contains a value):
"root.child('Respondents').child($organisation).child('admins').child(auth.uid).exists()",
So, let's say I have data like this:
{
"events" : {
"s0d980983s" :
{ creator: "bob#bob.com",
text: "Bob says 'My name is Robert'" },
"kjl34jl234j" :
{ creator: "fred#fred.com",
text: "Fred says 'My name is Fredrick'" }
}
"users" : {
"bob#bob.com" : { "paid": true },
"fred#fred.com" : { "paid": false }
}
}
I'm assuming this is the correct way to structure the data. When the data is created, I use the push() method to create a new key for the data, and then store the creator of the data inside it.
I'd like to make it so that:
I can allow anyone from a group of users to access certain data (and disallow others obviously).
The query is "optimized," meaning if I have thousands of records I am not iterating over all the data.
More concretely, for example, I want lizzie#lizzie.com to be able to see the s0d980983s.
I'm confused how to structure the data, and what my Firebase rules should look like.
Would it be something like this?
{ "events" : {
"s0d980983s" :
{ creator: "bob#bob.com",
viewers: { "bob#bob.com": true,
"lizzie#lizzie.com" : true },
text: "Bob says 'My name is Robert'" },
...
}
I don't understand how I can search for events that are viewable by a group of users. I don't believe Firebase supports some kind of wildcard that would make this code work, right?
var ref = firebase.database().ref( "events/*/viewers/lizzie#lizzie.com" ).on(...);
Do I also want to reference the events inside my users table? I'm not sure I understand how to flatten data (denormalize it) and keep references in both places to support a query like this. Should I expect to make multiple queries where I first retrieve a list of events stored in a user object and then retrieve them one by one using their key? But, how do I put that logic into my firebase rules?
{ "events" : {
"s0d980983s" :
{ creator: "bob#bob.com",
viewers: { "[insert bobs id]": true,
"[insert liz id]" : true
},
text: "Bob says 'My name is Robert'" },
...
}
Based on the above structure as you suggested, and if you are using firebase authentication to authenticate your user, you can add another 'read' or 'write' rule for checking whether that user is in the list of your 'viewers'. something like:
{
"rules": {
"users": {
"$uid": {
".write": "auth != null &&
root.child('users').child(auth.uid).child('viewers').child(auth.uid).val() ==
true"
}
}
}
}
This should help. setting firebase security rules at a location/node
The current query you see below is not efficient because I have not setup the proper indexing. I get the suggestion Consider adding ".indexOn": "users/kxSWLGDxpYgNQNFd3Q5WdoC9XFk2" at /conversations in the console in Xcode. I have tried it an it works.
However, I need the user id after users/ to be dynamic. I've added a link to another post below that has tried a similar thing, but I just can't seem to get it. All help would be much appreciated!
Note: The console output user id above does not match the screenshot below, but does not matter to solve the problem I believe. Correct me if I'm wrong. Thanks!
Here is the structure of my DB in Firebase:
{
"conversationsMessagesID" : "-KS3Y9dMLXfs3FE4nlm7",
"date" : "2016-10-19 15:45:32 PDT",
"dateAsDouble" : 4.6601793282986E8,
"displayNames" : [ “Tester 1”, “Tester 2” ],
"hideForUsers" : [ "SjZLsTGckoc7ZsyGV3mmwc022J93" ],
"readByUsers" : [ "mcOK5wVZoZYlFZZICXWYr3H81az2", "SjZLsTGckoc7ZsyGV3mmwc022J93" ],
"users" : {
"SjZLsTGckoc7ZsyGV3mmwc022J93" : true,
"mcOK5wVZoZYlFZZICXWYr3H81az2" : true
}
}
and the Swift query:
FIRDatabase.database().reference().child("conversations")
.queryOrderedByChild("users/\(AppState.sharedInstance.uid!)").queryEqualToValue(true)
Links to other post:
How to write .indexOn for dynamic keys in firebase?
It seems fairly simple to add the requested index:
{
"rules": {
"users": {
".indexOn": ["kxSWLGDxpYgNQNFd3Q5WdoC9XFk2", "SjZLsTGckoc7ZsyGV3mmwc022J93", "mcOK5wVZoZYlFZZICXWYr3H81az2"]
}
}
}
More likely your concern is that it's not feasible to add these indexes manually, since you're generating the user IDs in your code.
Unfortunately there is no API to generate indexes.
Instead you'll need to model your data differently to allow the query that you want to do. In this case, you want to retrieve the conversations for a specific user. So you'll need to store the conversations for each specific user:
conversationsByUser {
"SjZLsTGckoc7ZsyGV3mmwc022J93": {
"-KS3Y9dMLXfs3FE4nlm7": true
},
"mcOK5wVZoZYlFZZICXWYr3H81az2": {
"-KS3Y9dMLXfs3FE4nlm7": true
}
}
It may at first seem inefficient to store this data multiple times, but it is very common when using NoSQL databases. And is really no different than if the database would auto-generate the indexes for you, except that you have to write the code to update the indexes yourself.
This question already has an answer here:
Granting access to Firebase locations to a group of users
(1 answer)
Closed 7 years ago.
Let's say you have a firebase database with news articles to show in a webapp. On the top level we use Firebase auth to make sure that the user has access to the news-articles in the first place.
But then the different articles (items in the news-database) should be visible for different people. For example should users I put in a group called "management" have access to view some articles that the rest of the users cannot.
How can I achieve this using Firebase? (Web-version)
Sample database:
{
"newsfeed" : {
"1001" : {
"body" : "Lots of text here for the body of the article.",
"header" : "This is an open article",
"leadParagraph" : "With a lead paragraph and lots of content",
"permissions" : {
"All" : false,
"Group11" : true,
"Management" : true
}
},
"1002" : {
"Permissions" : {
"All" : true
},
"body" : "Content here",
"header" : "Testarticle for everyone",
"leadParagraph" : "Everyone can read this one"
}
}
}
So the question is not about how to limit a users access to the whole of "newsfeed", but rather how to edit access individually on the item level. In the example above all users can read item 1002, whilst only users that are tagged as members of the groups "Management" and/or "Group11" should be able to read item 1001.
Is this possible with rules dynamically? If I have to create a new rule for every item, is there a limit to the number of rules?
If you haven't already, I would read the Security Quickstart guide. It does a great job of explaining how Firebase security rules work.
In the case of your problem, I would suggest having a list of groups, where each group has the uids of the users in that group. For example:
{
"groups" : {
"Group11" : {
"<uid1>": true,
"<uid2>": true
},
"Management" : {
"<uid1>": true,
"<uid2>": true
}
}
}
If a user is a member of a group, add their uid to the group's list of members. Then in your rules, you can make the .read check whether or not the user who is trying to read the data has their uid in any of the allowed groups.
I'm having an issue setting my Security rules properly, specifically reading the post data.
The data hierarchy goes:
posts : {
0 : {
title: "Post One",
userId: 6
}
},
users : {
6 : {
name: "My Name"
}
}
And my rules are:
{
"rules": {
"posts" : {
"$post": {
".read":"data.child('userId').val() == auth.id",
".write":"newData.child('userId').val() == auth.id"
}
},
"users":{
"$user": {
".read":"auth.id == $user",
".write":"auth.id == $user"
}
}
}
}
I know that the "auth.id" is 6, because it's pulling the rules correctly for my user info. If I change the rules to pull the number statically, it works:
"$post": {
".read":"data.child('userId').val() == 6",
".write":"newData.child('userId').val() == auth.id"
}
but using auth.id does not. Is there something I'm missing?
One thing to keep in mind is that security rules are type-safe. In particular, In the rules, "6" != 6 (since one is a string and one is a number). So perhaps your auth.id is "6" (as a string), but your userId is 6 as a number?
If that's the case, one potential fix would be changing your rule expression to something like:
data.child('userId').val() + '' == auth.id
which will force userId to be a string. Alternatively, you could change your data to make sure userId is always stored as a string.
You haven't included the code you're using to look up this data--probably where the error is--or the error you are receiving; those would help quite a bit.
Your rules should work fine, assuming you are attempting to read a single post at a time, and assuming your authentication is set up correctly.
A quick guess would be that you're trying to read the entire "posts" path, and using security rules to filter your posts. But security rules are essentially atomic. If you try to read "posts", and one of the posts has a rule that prevents read, the entire operation is going to fail.
Instead, you need to segment the posts into paths where all the data can be read by the authenticated user, then you can apply security rules accordingly.
One thing that will help immensely is to test your security rules by going into your Forge and using the "simulator". You can log in as any user, then try a read/write, and see exactly which security rules is failing and why.