I'm trying to understand what is the right approach for this following scenario :
Multiplayer game,each game structured only with two players. Each game/match will be completely randomized
Lets assume 5 users "logs" the same time into my app, each one of them "searching" for a match. Each user hold a property named opponent which equal the the opponent uniqueID(initial value equal "". so far so good.
assuming user 1 matched with user 3. user 1 will update his own oppoent value to user 3 uniqueID and will do the same to user 3
Problem
1) What if at the same moment, user 2 tried to to the same to user 3?
2) What if at the same moment, user 3 tried to do so to user 4?
Main Point
Is it possible to "lock" a user values? or freeze them once they changed? Am i going in the wrong approach?
I was thinking using Security Rules and Validation in order to create consistency but i just may picked the wrong tech(FireBase). Any thoughts?
EDIT
Security rules i have tried, which still for some reason enable a third device change "already changed opponent" value.
{
"rules": {
".read": true,
".write": true,
"Users" :
{
"$uid" : {
"opponent" :
{
".write" : "data.val() == 'empty' || data.val() == null",
".validate": "data.val() == null || data.val() == 'empty' || newData.parent().parent().child(newData.val())
.child('opponent').val() == $uid"
}
,".indexOn": "state"
}
}
}
}
You can validate many things with Firebase security rules.
For example, you can say that an opponent can only be written if there currently is no opponent for the user:
"users": {
"$uid": {
"opponent: {
".write": "!data.exists()"
}
}
}
With this and the following operations:
ref.child('users').child(auth.uid).child('opponent').set('uid:1234');
ref.child('users').child(auth.uid).child('opponent').set('uid:2345');
The second set() operation will fail, because the opponent property already has a value at that point.
You can expand that to also validate that the opponents must refer to each other:
"users": {
"$uid": {
"opponent: {
".write": "!data.exists()"
".validate": "newData.parent().parent().child(newData.val())
.child('opponent').val() == $uid"
}
}
}
From the opponent that is being written, we go up two levels back to users: newData.parent().parent().
Then we go down into the opponent's node: child(newData.val()).
And we then validate that the opponent's opponent property matches our uid: child('opponent').val() == $uid.
Now both of the write operations from above will fail, because they're only setting the opponent one at a time. To fix this, you'll need to perform a so-called multi-location update:
var updates = {};
updates['users/'+auth.uid+'/opponent'] = 'uid:1234';
updates['users/uid:1234/opponent'] = auth.uid;
ref.update(updates);
We're now sending a single update() command to the Firebase server that writes the uids to both opponents. This will satisfy the security rule.
A few notes:
these are just some examples to get you started. While they should work, you'll need to write your own rules that meet your security needs.
these rules just handle writing of opponents. You'll probably also want to testing what happens when the game is over and you need to clear the opponents.
You might also look at the transaction operation.
Firebase transactions make sure that the current set of data you are acting on is really what is in the database, guaranteeing that you are updating data that is in the right condition. The docs indicate that this is the recommended way to avoid race conditions such as you describe.
Something like this (in IOS, and warning - not tested):
NSString* user1Key = #"-JRHTHaIs-jNPLXOQivY";
NSString* user2Key = #"-NFHUaIs-kNPLJDHuvY";
Firebase *user1Ref = [[Firebase alloc] initWithUrl: #"https://docs-examples.firebaseio.com.users/-JRHTHaIs-jNPLXOQivY/opponent"];
Firebase *user2Ref = [[Firebase alloc] initWithUrl: #"https://docs-examples.firebaseio.com.users/-NFHUaIs-kNPLJDHuvY/opponent"];
//See if the proposed opponent does not yet have a match
[user2Ref runTransactionBlock:^FTransactionResult *(FMutableData *opponent) {
if (opponent.value == [NSNull null]) {
//They have no match - update with our key and signal success
[opponent setValue:user1Key];
return [FTransactionResult successWithValue: opponent];
} else {
return [FTransactionResult abort]; //They already have an opponent - fail
//Notify the user that the match didn't happen
}
} andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {
if (!error && committed) {
//The transaction above was committed with no error
//Update our record with the other player - we're matched!
[user1ref setValue:user2Key];
//Do whatever notification you want
} else {
//Notify that the matchup failed
}
}];
Related
I've got a Realtime Database where users can write data to a certain key. However, I want to avoid a potentially malicious user from inserting a huge amount of data, without creating any new middleware.
Here are my rules right now:
{
"rules": {
/*
* Only let people read a specific name
* Only let people write "$user": <public info json> or "$user": null
*/
"$user": {
".read": "true",
".write": "auth.uid === $user"
".validate": "newData.val().length <= 123456 || newData.val() == null"
}
}
}
But this incorrectly rejects a write if the data at databaseurl/$user is a child object rather than a string, even if the JSON string representation of the child is below the length size.
How can I restrict the length of a child object, if the child is more than a simple string?
Thanks!
Because of the very specific nature of this question, I could not find an answer anywhere. Basically I want to create a messaging conversation with a specific user, but only if a conversation with that user doesn't already exist. I am looping through an array of conversations, and for each conversation I fetch the identity of the other user via a call to my backend. However, if no conversation is found with a particular user, then I want to create a new conversation. This is what I am doing:
for convo in convos {
HTTPManager.getOtherUserFromConversation(conversation: convo, success: { (otherUser) in
if desiredUser == otherUser {
//Found the desired conversation, so bring the user
//to it instead of creating a new one
}
}, failure: {
//Networking failure
})
}
//IF WE DIDN'T FIND IT, CREATE A NEW CONVERSATION HERE
I have thought of making a boolean value called "found" and setting it to true if we find the desired conversation, but I don't know how to wait until the last callback has executed before checking this boolean in order to avoid calling the check too early. Can anyone point me in the right direction?
The classic solution for this is using dispatch-group
https://developer.apple.com/documentation/dispatch/dispatchgroup
There are many code examples for this. The idea is that each network call should be in a separate task and the system lets you know when all tasks are done (this is where you check "found").
How about create callback function that will called when the all the request to check the convos is done, or when the correct convo is found.
func checkConvoIfExist(convos: [Convo]){
var found = false
var countCheck = 0
for convo in convos {
HTTPManager.getOtherUserFromConversation(conversation: convo, success: { (otherUser) in
countCheck += 1
if desiredUser == otherUser {
//Found the desired conversation
found = true
callbackCheckConvo(result: found, convo: convo)
break // to stop the loop
}else{
if (countCheck == convos.count){
callbackCheckConvo(result: found)
}
}
}, failure: {
//Networking failure
countCheck += 1
if (countCheck == convos.count){
callbackCheckConvo(result: found)
}
})
}
}
func callbackCheckConvo(result: Bool, convo: Convo = nil){
if (result){
//found the desired conversation, so bring the user to it instead of creating a new one
}else{
//convo not found, create new one
}
}
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 have a Sign Up Flow using Firebase. When I check if an email already exists in the database, like so:
refUsers.queryOrdered(byChild: "email").queryEqual(toValue: emailText).observeSingleEvent(of: .value, with: { snapshot in
if (snapshot.value is NSNull) {
print("Unique email")
// Move to Password View.
let passwordViewController = self.storyboard?.instantiateViewController(withIdentifier: "PasswordViewController") as! PasswordViewController
self.navigationController?.present(passwordViewController, animated: true, completion: nil)
// Pass the emailText to the last View of the flow.
self.singleton.sharedInstance.emailText = emailText!
}
else {
print("Duplicate email")
}
})
The problem is, I don't have the permission to view /users in the database cause my rule is:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
I know I can find if an email is a duplicate using Auth.auth().createUser but it's not just email that I'm checking in the sign up flow. I use the same method for unique username, as well. How can I achieve this?
As you can see this is not the best way to do it. You should not manually check if email already exists - Firebase can do that for you when user signs up and why would you not want to use that?
What you need is a different approach. I can think of two ways right now:
First:
You can add a new rule to Firebase, eg:
{
"rules": {
"usernames": {
".read": true,
".write": "auth != null"
},
"emails": {
".read": true,
".write": "auth != null"
}
}
}
What you do here is create a new node named usernames which every user can access and read.
Here you should hold a copy of all usernames that registered users have and when registering check if users username is already inside this node.
Second way:
You could modify your signup flow a bit and let users register without a username. After account is created you let them set a username. With a nice flow it would all look as the same registration form.
UPDATE
With rules above users should be able to read from emails and usernames without being registered. This way you can fetch data and compare if email or username is already in use.
Just make sure that when user registers you insert his email and username into those two nodes.
Though #ZassX answered helped me, I've learned what a good approach for this would be, for those who are confused like me.
The best approach is to keep users data safe in /users with "auth != null" rule. Only show the user's meta data to everyone that includes just the email and password of each user. For example:
Database
{
“metaData”: {
uid: {
“email”: …,
“password”: …
}
},
“users”: {
uid: {
“email”: …,
“password”: …
// other information
}
}
}
Security
{
"rules": {
“metaData”: {
“.read”: true,
“.write”: “auth !== null”
},
“users”: {
“.read”: “auth !== null”,
“.write”: “auth !== null”
}
}
}
The information in metaData can now be matched without a user being authenticated first. Of course, this can get more complex as you add more security but for easy understanding, this is it.
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.