So I have my Database structured like this, the owner gets set when the group is created and the owner the should have the permission to add other Users to allowed so they can access and edit the data too.
-Groups
|-Groupname
|- Owner: string
|- Allowed: List<string>
|- Data: all the data
So my attempt were these rules but they dont work when I use the playground feature with a saved uid under owner or allowed:
"Groups" : {
"$group": {
".read": "auth != null && (data.child('Owner').val() === auth.uid || data.child('Allowed').val() === auth.uid)",
".write": "auth != null && (data.child('Owner').val() === auth.uid || data.child('Allowed').val() === auth.uid)"
}
}
And would a User still be able to create a new group when these rules would work?
Pictures of the Database and Errors:
First, in the Realtime Database, avoid using arrays and use a map instead.
Change this:
"Allowed": {
"0": "8ZiQGBPFkiZOLgLJBgDeLw9ie9D3",
"1": "KEuhrxnAWXS0dnotjhjFAYUOcm42",
"2": "48yULftKSxgyS84ZJC4hs4ug4Ei2"
}
to this:
"Allowed": {
"8ZiQGBPFkiZOLgLJBgDeLw9ie9D3": true,
"KEuhrxnAWXS0dnotjhjFAYUOcm42": true,
"48yULftKSxgyS84ZJC4hs4ug4Ei2": true
}
Read that linked blog post for more info, but in short, it makes adding/removing users really simple:
const groupRef = firebase.database.ref(`Groups/${groupId}`);
// add a user
groupRef.child("E04HLbIjGDRUQxsRReHSKifaXIr2").set(true);
// remove a user
groupRef.child("KEuhrxnAWXS0dnotjhjFAYUOcm42").remove();
You can also change true to whatever you want. Here are some examples:
false = participant, true = moderator
false = read-only, true = can edit
Role names: "member", "admin", "moderator", etc.
Privilege levels: 0 (member), 500 (moderator), 1000 (owner), etc. (make sure to space these out, you don't want to have to add in a level between 0 and 1 and have to edit your entire database).
The most important point though, is that Realtime Database security rules don't know about arrays. data.val() won't return an array, it will just return a sentinel value that says "non-null object is here!". This means a map is necessary for security rules.
This reference document covers the structure and variables you can use in your Realtime Database Security Rules.
With your proposed rules, you attempt to allow any user in the group to be able to write to the group's data - but you don't manage what they can and can't write to. Any malicious member of a group could add/delete anyone else, make themselves the owner, or even delete the group entirely.
{
"rules": {
"Groups" : {
"$group": {
// If this group doesn't exist, allow the read.
// If the group does exist, only the owner & it's members
// can read this group's entire data tree.
".read": "!data.exists() || (auth != null && (data.child('Owner').val() === auth.uid || data.child('Allowed').child(auth.uid).val() === true))",
"Owner": {
// Only the current owner can write data to this key if it exists.
// If the owner is not yet set, they can only claim it for themselves.
".write": "auth != null && (data.val() === auth.uid || (!data.exists() && newData.val() === auth.uid))",
// Force this value to be a string
".validate": "newData.isString()"
},
"Allowed": {
// Only the owner can edit the entire member list
// For a new group, the owner is also granted write access
// for it's creation
".write": "auth != null && (data.parent().child('Owner').val() === auth.uid || (!data.exists() && newData.parent().child('Owner').val() === auth.uid))",
"$member": {
// Allows the user to remove themselves from the group
".write": "auth != null && auth.uid === $member && !newData.exists()",
// Force this value to be a boolean
".validate": "newData.isBoolean()"
}
},
"Data": {
// The owner and members can edit anything under "Data"
// Currently this includes deleting everything under it!
// For a new group, the owner is also granted write access
// for it's creation
// TODO: tighten structure of "Data" like above
".write": "auth != null && (data.parent().child('Owner').val() === auth.uid || data.parent().child('Allowed').child(auth.uid).val() === true || (!data.exists() && newData.parent().child('Owner').val() === auth.uid))"
}
}
}
}
}
Related
I would like to prevent deletion of data in Firebase Realtime Database based on a condition. A user that is not author should be able to update the "note", but not delete it.
I have a collection of "notes" in my database that has the following rule set.
"notes": {
".read": "
auth.uid !== null //&&
",
"$note_id": {
".write": "
//New data
!data.exists() && auth.uid !== null ||
//Existing data
data.child('access').child('author').val() === auth.uid ||
data.child('access/members').child(auth.uid).exists()
",
"data": { .. },
"access": {
"author" : { .. },
"members" : { .. }
}
}
}
How can I only allow "author" to delete the "note"?
I have tried using Google Cloud Functions for Firebase, but only have access to the .onDelete() event which is run after delete is already performed. Could I use the .onWrite() for this purpose - and if so how? I have already implemented listeners for .onCreate() .onUpdate() and .onDelete() for the /notes node in the database.
It's not possible to use Cloud Functions to intercept incoming requests before they affect the database. As you've seen, they are only used for post-processing.
If security rules are not sufficient to control access the way you want, consider routing the request through an HTTP type Cloud Function that checks permissions, then performs the delete or rejects the request.
I guess some Firebase rules along the lines
".write" : "
//Create
!data.exists() && auth.uid !== null
//Update
||(data.exists() && newData.exists() && data.child('access').child('author').val() === auth.uid || data.child('access/members').child(auth.uid).exists())
//Delete
||(data.exists() && !newData.exists() && data.child('access').child('author').val() === auth.uid)
"
would prevent delete if the user is not author?
I have a Realtime Database that pushes objects of the following structure into the DB.
Teams
- 0
-- Players
--- 0
---- ID
--- 1
---- ID
- 1
--- 0
---- ID
--- 1
---- ID
Basically, there are two teams, each team has up to two players, and each player has an ID. Once a game is saved, the game object is pushed to the DB. I am able to get the game data from /games/[unique-ID-generated-by-pushed]
Currently, all my (authenticated) users are able to read all the data under /games. I would like to restrict this: users are to only see games that they were involved in.
I managed to get this far with my rules.
{
"rules":
{
"games":
{
"$match_id":
{
".read": "data.child('$match_id').child('teams').child('0').child('players').child('0').child('ID').val() == auth.uid ||
data.child('$match_id').child('teams').child('0').child('players').child('1').child('ID').val() == auth.uid ||
data.child('$match_id').child('teams').child('1').child('players').child('0').child('ID').val() == auth.uid ||
data.child('$match_id').child('teams').child('1').child('players').child('1').child('ID').val() == auth.uid"
},
}
}
}
It isn't elegant, but works when I am querying /games/[unique-ID]. I don't seem to be getting any DataChanged event from /games reference, however, which is what I need.
Any idea how I can proceed from here? Or if it is even possible?
If you want to only allow read access to a game to players that were in that game, something like this should be enough in your current data structure:
{
"rules": {
"games": {
"$match_id": {
".read": "data.child('teams/0/players/0/ID').val() == auth.uid ||
data.child('teams/0/players/1/ID').val() == auth.uid ||
data.child('teams/1/players/0/ID').val() == auth.uid ||
data.child('teams/1/players/1/ID').val() == auth.uid"
},
}
}
}
Changes from your rules:
The child($match_id) seems like a mistake, so I removed it.
You can pass an entire path to child, which makes the rules a lot shorter.
If you'd like simpler rules, consider keeping an (additional) simpler list of all players for the game:
games
gameid1
players
player1id: true
player2id: true
player3id: true
player4id: true
Then the read rule can be a single clause:
{
"rules": {
"games": {
"$match_id": {
".read": "data.child('players').child(auth.uid).exists()"
},
}
}
}
Essentially you're trading simpler code/rules for more complex data in this case.
I am using Firebase for a small iOS project and I wonder if the following is possible.
Under a given node "myNode_123" I store data records; each record having the shape below:
[fieldStr: "ABC_xyz", fieldNum: 678]
A record is in fact a subnode.
Now my question is about updating the data, for already existing records.
Someone should be allowed to update a data record only if the new value for fieldNum is higher than the one already there.
And in any case the value for fieldStr should stay as it is.
I have already written some rules to make sure fieldNum is fullfilling my request.
But I still have the issue of fieldStr having the risk of being changed.
Here are my rules:
{
"rules": {
".read": true,
//".write": true
"myNode_123": {
"$Record": {
// Ensure that we are either creating a new record not yet existing,
// or that we are only updating for a higher fieldNum.
".write": "(!root.child('myNode_123/'+$Record).exists()) ||
(root.child('myNode_123/'+$Record).exists() && (newData.child('fieldNum').val() > data.child('fieldNum').val()))"
}
}
}
}
The control of fieldNum is working as I wish. But it is still possible to change fieldStr, which I do not want.
Any advice from a Firebase expert?
Add this under $Record.
{
"rules": {
".read": true,
//".write": true
"myNode_123": {
"$Record": {
// Ensure that we are either creating a new record not yet existing,
// or that we are only updating for a higher fieldNum.
".write": "!data.exists() || newData.child('fieldNum').val() > data.child('fieldNum').val()",
"fieldStr": {
".validate": "!data.exists() || data.val() == newData.val()"
}
}
}
}
}
The !data.exists() will make sure only new data can be written to this location. And data.val() == newData.val() will add an exception to allow writes when the new data is the same as the old data, just in case you want to write the entire object to the Database and include the fieldStr.
May I ask what runTransactionBlock is doing behind the hood? When I run a simple setValue with the exact same rules it works, but not with runTransactionBlock. I suspect that behind the hood runTransactionBlock writes to paths outside of just the path I stated, which is causing my security rules to deny permission. Hence, I have to write a global ".write": "auth != null" and avoid doing stuff such as my wildcard ".validate": false.
My security rules are mapped out this way:
{
"rules": {
// NOTE: I NEED THIS GLOBAL WRITE ALLOW FOR TRANSACTION TO WORK
".write": "auth != null",
"real_db": {
// USERS
"users": {
"$user": {
".read": "auth != null",
"$other": {
// NOTE: I NEED TO COMMENT VALIDATE FOR OTHER FIELDS IN USER
// ".validate": false
},
"pushToken": {
".read": "auth != null",
".validate": "auth != null"
},
...
My runTransactionBlock is run on the path real_db/users/$uid and I am changing the value of pushToken. When setValue is run on this same path and modifying pushToken it works.
Let's assume that users store some private data in /private/$userId which they can either share with others or not. The decision should be stored in /privacySettings/$userId/shareData which is of kind Bool. If the user sets its value to true others should be able to read the private data.
I have persistance enabled and tried to solve this with server rules:
"private": {
".read": false,
".write": false,
"$userId": {
".read": "auth != null && root.child('privacySettings/' + $userId + '/shareData').val() === true",
".write": "auth != null && auth.uid == $userId"
}
}
This works fine, but unfortunately a change in shareData does not raise an event when private/$userId is observed with .Value. So if the other user has observed this path before the change in the privacy, he will still see the data cached in the persistancy data store, which shouldn't be the case. When shareData is false all data should be hidden to others.
How to do this?
EDIT:
Just found out that once the data has been read, the observer will always return the cached data no matter if shareData has been set to false. This also happens when the app gets restarted.
EDIT 2:
After thinking more about it I came to the conclusion that this problem can easily be solved if the callback gave back a "permission denied" error.
I guess I found a reasonable workaround / solution for the problem:
Embedding the shareData in /private/$userId like so:
- private
- $userId
- shareData // Bool
- data // contains private data
Even if there is cached data, one can easily hide it according to the value of shareData without observing another node.
Server rules:
"private": {
".read": false,
".write": false,
"$userId": {
".read": "auth != null", //(*)
".write": false,
"shareData": {
".write": "auth != null && auth.uid == $userId",
".read": "auth != null",
},
"data": {
".read": "auth != null && root.child('private/' + $userId + '/shareData').val() === true",
".write": "auth != null && auth.uid == $userId",
}
}
}
EDIT:
Seems like the line marked with (*) overrides the child rules... Using "data.child('shareData').val() === true" there will cause the same effect as before: if there is cached data, it will be displayed.