Firebase: server side logic and real time database limitations - firebase

Server side custom operations equivalent to Parse cloud code:
Parse has the possibility to write cloud code. From my understanding of it Firebase doesn't offer any tools to do so on the console.
The only way to do so would be to implement a web-service using the Firebase API and monitor nodes changes and implement the cloud code on my own server.
A - Is this correct?
Server side rules:
The legacy documentation of Firebase describes rules which seem to be limited to deciding which user can read/write as well as validation.
{
"rules": {
"foo": {
// /foo/ is readable by the world
".read": true,
// /foo/ is writable by the world
".write": true,
// data written to /foo/ must be a string less than 100 characters
".validate": "newData.isString() && newData.val().length < 100"
}
} }
On Parse the complexity of the rules is greater. The programmer is able to create functions to perform custom operations.
Understanding the reason why Firebase is designed as it is:
I imagine that the reason for not having this complexity on Firebase is that probably a node based database is more complex than a table based one and it would be better if the developer has full control of this using the web-api and a custom server app.
B - Is this correct?
Real time database limitations:
The main limitation when using a real time database like Firebase seems to me that once you fetch some real time data if the data contains a two way redundancy the events triggered on one node are not propagated to the node containing the redundant information.
E.g. If a user node has keys id (ids of a different node at the same level of the user node) and if I display the list of keys that a user has on a table view in order to detect if the key list has changed I need to listen to changes in the keys node (and not only to changes in the user node).
- C: Is this correct?

The question is a tad vague as there are no use cases but based on the comments, here you go.
A) Yes, Maybe.
Yes, there is no server side logic (code-wise).
Maybe, it depends on what you are trying to do.
B) Firebase rules are very flexible; Rules can limit who can access data, read/write access, what kind of data, type of data, location of data etc. It is neither more or less complex than a 'table based one'. It's just a different way to verify, validate and store your data.
Just an FYI: Parse was was backed by MongoDB which is a document based NoSQL database (it's not table-based). On the back-end, Parse data was stored in a way similar to Firebase. (it's actually BSON). Their front-end implementation were objects that wrapped around JSON structures which gave the feeling that it was more table-like than Firebase, and that lead to the direct ability to have relationships between PFobjects.
C) No. You can observe any node for changes. In your example, you should probably have the keys node as a separate node than the /user node and have users observe the /keys node.
To expand on that a bit, data doesn't necessarily need to be redundant, but it can be. You only need to observe changes for the specific data you are interested in.
Suppose you have a /users node and /favorite_food node of each user. If your UI is displaying a list of users and one of them changes their favorite food, you don't really care - you are just interested in the list of users. So you would observe the /users node and not the /favorite_food node.

Related

Firestore Rules: Allow or limit request to only once every 24h?

Is there a native or efficient way to restrict the user to load a document from a collection only once every 24h?
//Daily Tasks
//User should have only read rights
//User should only be able to read one document every 24h
match /tasks/{documents} {
allow read: if isSignedIn() && request.query.elapsedHours > 24;
}
I was thinking that I might be able to do this using a timestamp in the user document. But this would consume unnecessary writing resources to make a write to the user document with every request for a task document. So before I do it this way, I wanted to find out if anyone had a better approach.
Any ideas? Thanks a lot!
There is no native solution, because security rules can't write back into the database to make a record of the query.
You could instead force access through a backend (such as Cloud Functions) that also records the time of access of the particular authenticated user, and compare against that every time. Note that it will incur an extra document read every call.
There is no real "efficient" way to do so, neither a native at the moment of writing. And finding an actual solution to this "problem" won't be easy without further extensions.
There are however workarounds like with cloud functions for firebase that open new options for solving various limitations firestore has.
A native solution would be keeping track somewhere in the database when each user last accessed the document. This would, as you mentioned, create unnecessary reads and writes just for tracking.
I would prefer a caching mechanism on the client and allow the user to execute multiple reads. Don't forget that if the user clears the cache on the device, he has to query the document(s) again and won't get any data at all if you restrict him completely that way.
I think the best approach, due to the high amount of reads you get, is to cache on client side and set only a limit control (see Request.query limit value). This would look somehow like below:
match /tasks/{documents} {
// Allow only signed in users to query multiple documents
// with a limit explicitly set to less than or equal to 20 documents per read
allow list: if isSignedIn() && request.query.limit <= 20;
// Allow single document read to signed in users
allow get: if isSignedIn();
}

Cloud Firestore rules on subcollection

I'm working on an iOS app which has (whoah surprise!) chat functionality. The whole app is heavily using the Firebase Tools, for the database I’m using the new Cloud Firestore solution.
Currently I'm in the process of tightening the security using the database rules, but I'm struggling a bit with my own data model :) This could mean that my data model is poorly chosen, but I'm really happy with it, except for implementing the rules part.
The conversation part of the model looks like this. At the root of my database I have a conversations collection:
/conversations/$conversationId
- owner // id of the user that created the conversation
- ts // timestamp when the conversation was created
- members: {
$user_id_1: true // usually the same as 'owner'
$user_id_2: true // the other person in this conversation
...
}
- memberInfo: {
// some extra info about user typing, names, last message etc.
...
}
And then I have a subcollection on each conversation called messages. A message document is a very simple and just holding information about each sent message.
/conversations/$conversationId/messages/$messageId
- body
- sender
- ts
And a screenshot of the model:
The rules on the conversation documents are fairly straightforward and easy to implement:
match /conversations/{conversationId} {
allow read, write: if resource.data.members[(request.auth.uid)] == true;
match /messages/{messageId} {
allow read, write: if get(/databases/$(database)/documents/conversations/$(conversationId)).data.members[(request.auth.uid)] == true;
}
}
Problem
My problem is with the messages subcollection in that conversation. The above works, but I don’t like using the get() call in there.
Each get() call performs a read action, and therefore affects my bill at the end of the month, see documentation.
Which might become a problem if the app I’m building will become a succes, the document reads ofcourse are really minimal, but to do it every time a user opens a conversation seems a bit inefficient. I really like the subcollection solution in my model, but not sure how to efficiently implement the rules here.
I'm open for any datamodel change, my goal is to evaluate the rules without these get() calls. Any idea is very welcome.
Honestly, I think you're okay with your structure and get call as-is. Here's why:
If you're fetching a bunch of documents in a subcollection, Cloud Firestore is usually smart enough to cache values as needed. For example, if you were to ask to fetch all 200 items in "conversions/chat_abc/messages", Cloud Firestore would only perform that get operation once and re-use it for the entire batch operation. So you'll end up with 201 reads, and not 400.
As a general philosophy, I'm not a fan of optimizing for pricing in your security rules. Yes, you can end up with one or two extra reads per operation, but it's probably not going to cause you trouble the same way, say, a poorly written Cloud Function might. Those are the areas where you're better off optimizing.
If you want to save those extra reads, you can actually implement a "cache" based on custom claims.
You can, for example, save the chats the user has access to in the custom claims under the object "conversations". Keep in mind custom claims has a limit of 1000 bytes as mentioned in their documentation.
One workaround to the limit is to just save the most recent conversations in the custom claims, like the top 50. Then in the security rules you can do this:
allow read, write: if request.auth.token.conversations[conversationId] || get(/databases/$(database)/documents/conversations/$(conversationId)).data.members[(request.auth.uid)] == true;
This is especially great if you're already using cloud functions to moderate messages after they were posted, all you need is to update the custom claims

Firebase indexOn works with something not in rules JSON

I have a legacy Firebase project i contribute to. In it I have the following rules for the resource songs:
"songs": {
".indexOn": ["artist_timestamp"]
},
Which allows me to do things like curl htttp://my-fire-base-ref/songs.json?orderBy="artist_timestamp"
However I can also do orderBy="$priority" which is a property we add to all song objects. This works even though it is not explicitly in the rules json definition. Is this a secretly allowed property??
The .priority of each node is implicitly indexed, so you don't need to define an index for it.
Why are you using priorities though? While they still work, using named properties allows you to accomplish the same with more readable code. See What does priority mean in Firebase?
According to the documentation for indexing data:
Firebase provides powerful tools for ordering and querying your data.
Specifically, Firebase allows you to do ad-hoc queries on a collection
of nodes using any common child key. As your app grows, the
performance of this query degrades. However, if you tell Firebase
about the keys you will be querying, Firebase will index those keys at
the servers, improving the performance of your queries.
This means you can order by any key at any time without specifying it as an index, but without a specific index specified for a key, performance may be very bad for large sets of data.

How can Firebase nodes be structured to restrict user access and allow admin to pull report data?

Context: I am putting together a time tracking application using Firebase as my backend. My current node structure has Time Entries and Clients at the root like so:
Time Entry
Entry ID
UserID
clientID, hours, date, description, etc
Clients
ClientID
name, projects, etc
This structure works fine if I'm just adding and pulling time entries based on the user, but I want to start putting together reports on a per client basis. Currently, this means making a separate HTTP request for each user and then filtering by the clientID to get at the data.
The rule structure for Firebase grants access to all child nodes once access is given to the parent node, so one big list doesn't work as it can't restrict users from seeing or editing each other's entries.
Question: Is there a way to structure the nodes that would allow for restricting users to only managing their own time entries, as well as allow for one query to pull all entries tied to a client?
** The only solution I could come up with was duplicating the entries into a single node used just for reporting purposes, but this doesn't seem like a sustainable option
#AL. your answer was what I went up going with after scouring the docs across the web. Duplicating the data is the best route to take.
The new Firestore beta seems to provided some workarounds to this.
The way that I would do this is with Cloud Firestore.
Create a root collection clients and a document for each client. This partitions the data into easily manageable chunks, so that a client admin can see all data for their company.
Within the client document, create a sub-collection called timeEntries. When a user writes to this, they must include a userId field (you can enforce this in the rules) which is equal to request.auth.uid
https://firebase.google.com/docs/firestore/security/rules-conditions#data_validation
You can now create read rules which allow an admin to query any document in the timeEntries sub-collection, but an individual user must query with userId = request.auth.uid in order to only return the entries that they have created.
https://firebase.google.com/docs/firestore/security/rules-conditions#security_rules_and_query_results
Within your users/{uid} collection or clients/{clientId} collection, you can easily create a flag to identify admin users and check this when reading data.
https://firebase.google.com/docs/firestore/security/rules-conditions#access_other_documents

How to limit size of the data on the server side a user can fetch in Firebase?

I gave a talk about basics of Firebase (http://szimek.github.io/presentation-firebase-intro) at our local meetup and got 2 interesting questions from the audience.
Imagine you have a Twitter-like app with billion of tweets and everyone has read access to them.
Is there a way to limit size of the data (on the server side) a user can fetch? Even if I have tweetsRef.limit(10) call, a user could easily change it to tweetsRef.limit(10e9) and try to fetch all tweets.
How to prevent users from updating existing records (even if it was created by that user), but allow them to delete existing records (only if it was created by that user)?
If you are worried about limit manipulation you could just fetch the tweets on your server instead of the client (as you suggested) so users can't manipulate the limits.
For your second question, it depends on how you want to handle deletion. Often you don't actually want the object deleted, so you could just give the creating user write access on the deleted attribute. Alternatively, if you want them to actually delete the object, check to see that the user is the creator and that the value of newData is null.
Here is an example security rule from #Kato's comment below (writes/deletes allowed, updates prevented):
".write": "!data.exits() || !newData.exists()"

Resources