I can't understand how to write a rule for an update.
Here is the flow:
First-time users create new, "Conversation" with the following properties at initial states:
TotalMessages: 0
UsersOnline: 0
Date: Some date
Rule I wrote for this:
"$message": {
".validate": "newData.hasChildren(['TotalMessages', 'UsersOnline', 'Date',])"
}
Then when new users join conversation, I need to update the "TotalMessages" value on the client:
update(this.topicPropsRef, {
TotalMessages: increment(+1),
})
I can't understand how to write a rule for an update. According to the rule above, each request should have all these three children, while I need only one for an update.
At Firestore, I could write a separate rule just for an update, but it is only read&write rules in the real-time database.
Related
Recently I've been working on a simple state-tracking system, its main purpose is to persist updates, sent periodically from a mobile client in relational database for further analysis/presentation.
The mobile client uses JWTs issued by AAD to authenticate against our APIs. I need to find a way to verify if user has permissions to send an update for a certain Item (at this moment only its creator should be able to do that).
We assume that those updates could be sent by a lot of clients, in small intervals (15-30 seconds). We will only have one Item in active state per user.
The backend application is based on Spring-Boot, uses Spring Security with MS AAD starter and Spring Data JPA.
Obviously we could just do the following:
User_1 creates Item_1
User_1 sends an Update for Item_1
Item has an owner_ID field, before inserting Update we simply check if Item_1.owner_ID=User_1.ID - this means we need to fetch the original Item before every insert.
I was wondering if there was a more elegant approach to solving these kind of problems. Should we just use some kind of caching solution to keep allowed ID pairs, eg. {User_1, Item_1}?
WHERE clause
You can include it as a condition in your WHERE clause. For example, if you are updating record X you might have started with:
UPDATE table_name SET column1 = value1 WHERE id = X
However, you can instead do:
UPDATE table_name SET column1 = value1 WHERE id = X AND owner_id = Y
If the owner isn't Y, then the value won't get updated. You can introduce a method in your Spring Data repository that looks up the Spring Security value:
#Query("UPDATE table_name SET column1 = ?value1 WHERE id = ?id AND owner_id = ?#{principal.ownerId}")
public int updateValueById(String value1, String id);
where principal is whatever is returned from Authentication#getPrincipal.
Cache
You are correct that technically a cache would prevent the first database call, but it would introduce other complexities. Keeping a cache fresh is enough of a challenge that I would try it only when it's obvious that introducing the complexity of a cache brings the required, observed performance gains.
#PostAuthorize
Alternatively, you can make the extra call and use the framework to simplify the boilerplate. For example, you can use the #PostAuthorize annotation, like so, in your controller:
#PutMapping("/updatevalue")
#Transactional
#PostAuthorize("returnObject?.ownerId == authentication.principal.ownerId")
public MyWidget update(String value1, String id) {
MyWidget widget = this.repository.findById(id);
widget.setColumn1(value1);
return widget;
}
With this arrangement, Spring Security will check the return value's ownerId against the logged-in user. If it fails, then the transaction will be rolled back, and the changes won't make it into the database.
For this to work, ensure that Spring's transaction interceptor is placed before Spring Security's post authorize interceptor like so:
#EnableMethodSecurity
#EnableTransactionManagement(order=-1)
The downside to this solution is that there are still the same two DB calls. I like it because it's allowing the framework to enforce the authorization rule. To learn more, take a look at this sample application that follows this pattern.
I am working on a Forum-like structure using Firebase Store / Firebase Rules. My structure is something like this:
Collection --- Document ------ Collection --- Document
Topic1 CreationDate UsersJoined UserUID1
Topic2 Title UserUID2
Topic3 UpdatedDate UserUID3
... ... ...
Basically, each Topic has a Collection of Users. My goal is to be able to write a security rule where only Users in the 'UsersJoined' can read/write to the corresponding Topic. This is what I have right now as my rules:
service cloud.firestore {
match /databases/{database}/documents {
match /Topics/{topicUID} {
allow read, create, update, delete: if exists(/databases/$(database)/documents/Topics/$(topicUID)/UsersJoined/$(request.auth.uid));
match /UsersJoined/{userUID=**} {
allow read, create, update, delete;
}
}
}
}
So when I use the built in Simulator, the read works just fine; however, when I request to read it via my code for IOS, it tells me that I don't have sufficient permission.
I've tried just doing allow read: if request.auth.uid != null;, and I am able to read. I am confident that the UserUID does exist within the UsersJoined collection.
I've also tried creating a "sister" collection where I store my User IDs in, so my structure looks like this:
Collection ----------- Document
MyTestUserCollection UserUID1
Topic1 UserUID2
Topic2 ...
...
I then used this rule: if exists(/databases/$(database)/documents/MyTestUserCollection/$(request.auth.uid)); and the read works as well, both on the simulator and IOS codes.
My problem is not being able to read when the User List is nested within the Topic. So my question is... by writing a rule that checks ("reads") data in a nested collection, am I violating the "allow read" rule (since technically it hasn't determined whether I can read yet)? Or am I over complicating things a bit and there is a better way to structure my Collections/Documents? Or am I just not writing the rule correctly?
I don't believe my code on IOS is the issue, but just in case this is what I'm doing to request to read from my database: (the user is logged in via Firebase Auth)
[[myFirestore collectionWithPath:#"Topics"]
getDocumentsWithCompletion:^(FIRQuerySnapshot *snapshot, NSError *error) {
if (error != nil) {
NSLog(#"Error getting documents: %#", error);
} else {
NSLog(#"Read it");
}
}];
Any help is greatly appreciated!
Your security rules allow a user to read a specific topic, if they are following that topic. Your code tries to read all topics, which your rules don't allow. So that explains why the server rejects the read operation.
This is easiest to remember by realizing that rules themselves don't filter data. They instead either allow a listener or not. And since your rules don't allow a listener on all of /Topics, that listener gets rejected.
One solution is to only read the specific topic that you are a follower of. To determine the topic(s), you might need to store a document with the user's list of topics, such as in a /Profiles collection. This is quite common on NoSQL databases: you're essentially storing both sides of the many-to-many relationship.
Alternatively you can try to validate the query, but I'm not quite sure if that can be made to work for your situation.
I want to only update the values email, firstname and lastname if they are blank.
I need this so that if the user decides to change these in the settings, they are not overwritten every time the user logs in with facebook.
Any solutions to check if the fields are blank without a datasnapshot? Trying to maximise efficency.
Current code when user signs in with facebook?
Database Structure for each user:
One way to do this is using a firebase transaction.
A transaction allows you to check the current value of a DB reference before you set/update it. It's main use case is preventing multiple concurrent updates from multiple sources but it can be used for this case as well - read and then write.
In the transaction block you get the value of the DB ref you're transacting on & can check that the value is null (hence 'create' case) -> then update it as required and return TransactionResult.success(withValue: newData).
If the object is already set you simply abort the transaction with TransactionResult.abort() and no write to the DB is executed.
Another option, that doesn't require a read/write, is to set a Firebase database rule on the relevant ref that will only allow write if the previous value was null:
"refPath": {
".write": "data.val() == null && newDataval() != null"
}
Writing a second time to the DB for an existing ref will fail.
I'd go with the transaction - more expressive of the requirement in the client code.
In firebase the only way you have to check if the current value of your fields in your database are empty is to fetch them before you are setting them.
You can check the field is empty only by fetching them.Then Use this code to update a particular value
ref.child("yourKey").child("yourKey").updateChildValues(["email": yourValue])
The childByAutoId would be useful if you want to save in a node multiple children of the same type, that way each child will have its own unique identifier.
List:{
KJHBJJHB:{
name:List-1,
owner:John Doe,
user_id:<Fire base generated User_id>
},
KhBHJBJjJ:{
name:List-2,
owner:Jane Lannister,
user_id:<Fire base generated User_id>
},
KhBHJZJjZ:{
name:List-3,
owner:John Doe,
user_id:<Fire base generated User_id>
}
}
I am trying to access the List with the help of the following code:
let ref = FIRDatabase.database().reference(withPath: "/List")
The current user logged into the app is John Doe. When the user accesses the list, I want all the List child whose owner is John Doe(i.e. List-1 & List-3) and ignore the other child values.
Do I have to do this in my application or can this be achieved via Firebase Security rules?
My current rule definition is:
"List":{
".read": "root.child('List/'+root.child('List').val()+'/user_id').val() === auth.uid" }
But this rule is not giving me any success. Any idea how to achieve the desired result?
You're trying to use security rules to filter the list. This is not possible and one of the common pitfalls for developers coming to Firebase from a SQL background. We commonly refer to it as "rules are not filters" and you can learn more about it in:
the Firebase documentation
this answer
our new video series Firebase for SQL developers
and many previous questions mentioning "rules are not filters"
The solution is almost always the same: keep a separate list of the keys of posts that each user has access to.
UserLists:{
JohnUid: {
KJHBJJHB: true,
KhBHJZJjZ: true
},
JaneUid: {
KhBHJBJjJ: true
}
}
This type of list is often referred to as an index, since it contains references to the actual post. You can also find more about this structure in the Firebase documentation on structuring data.
I'm writing a financial app with Firebase and for an receipt to be submitted, a number of other objects also need to be updated. For the data to be valid, all data updates need to be completed successfully. If there's an error in one of the writes, all updates must be rolled back.
For example:
If the user submits a receipt, the receipt object must be updated as well as an invoice object as well as other general ledger objects.
If the update started but the user lost internet connection half way through, all changes should be rolled back.
What's the best way to achieve this in Firebase?
First, let's chat for a minute about why someone might want to do commit/rollback on multiple data paths...
Do you need this?
Generally, you do not need this if:
you are not writing with high concurrency (hundreds of write opes per minute to the SAME record by DIFFERENT users)
your dependencies are straightforward (B depends on A, and C depends on A, but A does not depend on B or C)
your data can be merged into a single path
Developers are a bit too worried about orphaned records appearing in their data.
The chance of a web socket failing between one write and the other is probably trivial and somewhere on the order of collisions between
timestamp based IDs. That’s not to say it’s impossible, but it's generally low consequency, highly unlikely, and shouldn’t be your primary concern.
Also, orphans are extremely easy to clean up with a script or even just by typing a few lines of code into the JS console. So again,
they tend to be very low consequence.
What can you do instead of this?
Put all the data that must be written atomically into a single path. Then you can write it as a single set or a transaction if necessary.
Or in the case where one record is the primary and the others depend on this, simply write the primary first, then write the others in the callback. Add security rules to enforce this, so that the primary record always exists before the others are allowed to write.
If you are denormalizing data simply to make it easy and fast to iterate (e.g. to obtain a list of names for users), then simply index that data in a separate path.
Then you can have the complete data record in a single path and the names, emails, etc in a fast, query/sort-friendly list.
When is this useful?
This is an appropriate tool to use if you have a denormalized set of records that:
cannot be merged practically into one path in a practical way
have complex dependencies (A depends on C, and C depends on B, and B depends on A)
records are written with high concurrency (i.e. possibly hundreds of write ops per minute to the SAME record by DIFFERENT users)
How do you do this?
The idea is to use update counters to ensure all paths stay at the same revision.
1) Create an update counter which is incremented using transactions:
function updateCounter(counterRef, next) {
counterRef.transaction(function(current_value) {
return (current_value||0)+1;
}, function(err, committed, ss) {
if( err ) console.error(err)
else if( committed ) next(ss.val());
}, false);
}
2) Give it some security rules
"counters": {
"$counter": {
".read": true,
".write": "newData.isNumber() && ( (!data.exists() && newData.val() === 1) || newData.val() === data.val() + 1 )"
}
},
3) Give your records security rules to enforce the update_counter
"$atomic_path": {
".read": true,
// .validate allows these records to be deleted, use .write to prevent deletions
".validate": "newData.hasChildren(['update_counter', 'update_key']) && root.child('counters/'+newData.child('update_key').val()).val() === newData.child('update_counter').val()",
"update_counter": {
".validate": "newData.isNumber()"
},
"update_key": {
".validate": "newData.isString()"
}
}
4) Write the data with the update_counter
Since you have security rules in place, records can only successfully write if the counter does not move. If it does move, then the records have been overwritten by a concurrent change, so they no longer matter (they are no longer the latest and greatest).
var fb = new Firebase(URL);
updateCounter(function(newCounter) {
var data = { foo: 'bar', update_counter: newCounter, update_key: 'myKey' };
fb.child('pathA').set(data);
fb.child('pathB').set(/* some other data */);
// depending on your use case, you may want transactions here
// to check data state before write, but they aren't strictly necessary
});
5) Rollbacks
Rollbacks are a bit more involved, but can be built off this principle:
store the old values before calling set
monitor each set op for failures
set back to old values on any committed changes, but keep the new counter
A pre-built library
I wrote up a lib today that does this and stuffed it on GitHub. Feel free to use it, but please be sure you aren't making your life complicated by reading "Do you need this?" above.