I'm using Angular for my frontend and Cloud functions + Firestore for my backend.
When I set data in Firestore, I want to limit the users to a certain model of data. For example let's say that my "model" is:
interface Person {
name: string
age: number
}
Nothing prevents a malicious user to send an object like the following:
{
name: 'Anna',
age: 22,
unWantedData: 'Some unwanted data'
}
What is the best way to prevent that ?
I've seen the Firestore converters but is it really the way to go ?
I also have another related question. I'm quite familiar working with SQL and ORM's using Entities, for example with TypeORM:
#Entity()
export class Post {
#PrimaryGeneratedColumn()
id: number
#Column()
#Length(10, 20)
title: string
#Column()
#IsInt()
#Min(0)
#Max(10)
rating: number
#Column()
#IsEmail()
email: string
#Column()
#IsDate()
createDate: Date
}
Is there a way to have validators like that with Firestore ? Maybe using rules ?
You have the answer in the last line of your question: you can indeed use Firebase's server-side security rules to validate the data.
The security rules only apply to access from the client-side SDKs, not from connections over the Admin SDK (such as those from Cloud Functions that you mention). If writes happen through such a trusted environment, you will have to validate the data in your application code before passing it to Firebase.
Related
I have 2 sign up pages, one for students and one for teachers.
How can I set a different custom claim for each when they sign up? (student: true OR teacher:true)
I am guessing it could be possible at .onCreate? I can set just one custom claim per onCreate function, how to make this dynamic?
exports.AddTeacherRole = functions.auth.user().onCreate(async (authUser) => {
if (authUser.email) {
const customClaims = {
teacher: true,
};
The authUser Object passed to Authentication Cloud Functions is exactly the same type of Object than the UserRecord that is returned by the Firebase Admin SDK.
Depending on how you created the user (e.g. from the front-end, with createUserWithEmailAndPassword() in JavaScript or e.g. from the Admin SDK, with createUser()) the UserRecord may hold more or less "extra" properties (e.g. displayName, photoURL, etc).
The problem is that there is no specific property of this UserRecord instance that you could use to indicate the role of the user (student or teacher). Therefore you cannot get this information in your Cloud Function, which is triggered when the user is created.
A common approach is to add this kind of extra data to a Firestore document (or a Realtime Database node) which has the same id than the user Id. Then from this document, you could trigger a Cloud Function that updates the user record.
Another approach is to use a Cloud Function that does all the job: create the user, set the custom claim and create a Firestore document. You will find in the following article some detailed explanations about this kind of approach.
To answer the question you asked in the deleted answer:
Just to verify, it could be solved by creating 2 separate https
callable cloud functions that handle the sign up for each role for
example?
Yes you could have two callable Cloud Functions, but I think it should be possible to just have one and pass different parameter values when calling this Cloud Function, depending on the user role.
Something like:
const setCustomClaim = firebase.functions().httpsCallable('setCustomClaim');
const userRole = "student" // or role = "teacher"
setCustomClaim({role: userRole, userId, bar: "foo" }).then(function(result) {
// Read result of the Cloud Function.
// ...
});
I totally made up the name "type specifiers." What I mean is the stringValue key in front of a value. Usually I would expect a more-standard response: "name" : "name_here".
{
"fields": {
"name": {
"stringValue": "name_here"
}
}
}
Is it possible to remove those when making a GET call?
More importantly, it be nice to understand why it's structured like it is. Even for POST-ing data? The easy answer is probably because Cloud Firestore, unlike Realtime Database, needs to know the specific types, but what are all the deeper reasons? Is there an "official" name for formatting like this where I could do more research?
For example, is the reasoning any related to Protocol Buffers? Is there a way to request a protobuf instead of JSON?
Schema:
Is it possible to remove those when making a GET call?
In short No. The Firestore REST API GET returns an instance of Document.
See https://firebase.google.com/docs/firestore/reference/rest/v1beta1/projects.databases.documents#Document
{
"name": string,
"fields": {
string: {
object(Value)
},
...
},
"createTime": string,
"updateTime": string,
}
Regarding the "Protocol Buffer": When the data is deserialized you could just have a function to convert into the structure you wish to use, e.g. probably using the protocol buffers if you wish but as there appear to be libraries for SWIFT, OBJECTIVE-C, ANDROID, JAVA, PYTHON, NODE.JS, GO maybe you won’t need to use the REST API and craft a Protocol Buffer.
Hopefully address your “More Importantly” comment:
As you eluded to in your question Firestore has a different data model to the Realtime Database.
Realtime database data model allows JSON objects with the schema and keywords as you want to define it.
As you point out, the Firestore data model uses predefined schemas, in that respect some of the keywords and structure cannot be changed.
The Cloud Firestore Data Model is described here: https://firebase.google.com/docs/firestore/data-model
Effectively the data model is / where a document can contain a subcollection and the keywords “name”, “fields”, “createdTime”, “upTime” are in a Firestore document (a pre-defined JSON document schema).
A successful the Firestore REST API GET request results in a Document instance which could contain collection of documents or a single document. See https://firebase.google.com/docs/firestore/reference/rest/. Also the API discovery document helps give some detail about the api:
https://firestore.googleapis.com/$discovery/rest?version=v1beta1
An example REST API URL structure is of the form:
https://firestore.googleapis.com/v1beta1/projects/<yourprojectid>/databases/(default)/documents/<collectionName>/<documentID>
It is possible to mask certain fields in a document but still the Firestore Document schema will persist. See the three examples GET:
collection https://pastebin.com/98qByY7n
document https://pastebin.com/QLwZFGgF
document with mask https://pastebin.com/KA1cGX3k
Looking at another example, the REST API to run Queries
https://firebase.google.com/docs/firestore/reference/rest/v1beta1/projects.databases.documents/runQuery
the response body is of the form:
{
"transaction": string,
"document": {
object(Document)
},
"readTime": string,
"skippedResults": number,
}
In summary:
The Realtime database REST API will return the JSON for the object according to the path/nodes as per your “more-standard response”.
The Firestore REST API returns a specific Firestore predefined response structure.
There API libraries available for several language so maybe it’s not necessary to use the REST API and craft your own Protocol Buffer but if you needed to you it’s probably feasible.
I don't understand why somebody just say that you can't and don't try think some solution for help! Seriously that this is a really problem solver?
Anyway, I created a script that will help you (maybe it's late now hahaha).
The script encode json and after replace it as string to modify and remove Google type fields (low process).
It's a simple code, I know that you can improve it if necessary!
WARNING!!
Maybe you will have problems with values that contain '{}' or '[]'. This can be solved with a foreach that convert all strings that contains this elements in other char (like '◘' or '♦', some char that you know that doesn't will be in value.
Ex.: Hi {Lorena}! ------> Hi ◘Lorena♦!
After the process, convert again to '{}' or '[]'
YOU CAN'T HAVE FIELDS WITH THE SAME NAME THAT GOOGLE FIELDS
Ex.: stringValue, arrayValue, etc
You can see and download the script in this link:
https://github.com/campostech/scripts-helpers/blob/master/CLOUD%20STORE%20JSON%20FIELDS%20REMOVER/csjfr.php
I can't understand the way Firebase db works.
Yes, I know it is a JSON with my data, I know the ways to set one-to-many and many-to-many relationship between different objects. But is there any way to set up some kind of "schema" to database?
An example to understand what I mean: what if I'm creating an Android app, using Firebase SDK, and my friend is creating an iOS app. I'm pushing this to "users": {name: John, city: LA}. And my friend makes a mistake, pushing {name: Tom, cety: NY}. Than, trying to get Toms city - what will I get? Null?
Is there a way to specify a structure to saved data. In Firebase console I see only way to add exact values, not specify the way it should be structured. How to tell other developers, or "me-in-the-future" that my users should contain, for example "name", "gender" and "dog_eyes_color"?
Hope you understand me.
To answer your question, Firebase is a schema-less database. If you push data to a node
users
uid_0
name: John
city: LA
and your friend adds another user
uid_1
name: Frank
cety: NY
Then your users node will have two users with children that have different keys; city vs cety.
As Frank mentioned in his comment, you can 'catch' and prevent data from being written to a node that's invalid. However, ponder this class:
class User {
userId = ""
name = ""
city = ""
func saveToFirebase() {
myRef.setValue( user id etc etc)
}
}
and you tell your friend that any time they want to store a user in Firebase, to use that class. You now have a standardized model in which to interact with Firebase. That class (or structure or whatever you use) defines a schema to work with.
The Firebase database (NoSQL) provides a mechanism for storage and retrieval
of data which is modeled in means other than the tabular relations
used in relational databases
And as a followup to the users question: Firebase DOES store user authentication data 'in the back end' which is not directly accessible (queryable) to the developer. The idea here is that when a user is created in Firebase with Firebase functions such as createUser(), you are provided the user id (UID) of that user when it's created and that's what you can use to store additional information in a /users node you create.
users
uid_0
name: Frank
location: LA
fav_food: Pizza
uid_1
name: Leroy
location: NY
fav_food: Tacos
.validate....
I would not leverage .validate rules to define a structure (schema) or keep other developers in check. Providing coding tools such as the Users class mentioned above will provide far more flexibility and less aggravation (and coding) in the long run and will be much more maintainable.
how about this way of implementation
class User {
constructor(name, city) {
this.name = name;
this.city = city;
return {
name: this.name,
city: this.city,
};
}
}
const saveDocument = function (collectionName, obj) {
db.collection(collectionName)
.add(obj)
.then((docRef) => {
console.log("Document written with ID: ", docRef.id);
})
.catch((error) => {
console.error("Error adding document: ", error);
});
};
saveDocument("user", new User("Jhon", "NY"));
The following answer goes for anyone using Firestore and having this same question.
Firebase security rules, this video does an amazing job explaining them, are a way in which you can sort of create schemas.
An important consideration when using these rules is that if you're using the Firestore Admin SDK (used for backend code) then your rules will be bypassed. To enforce this rules the request must come from a client SDK. I think that one of the best things about Firebase is serverless computing so this type of validations should work just fine with most requests considering they come from the client.
To validate requests coming from a source that bypasses the basic security rules make sure to set up Identity and Access Management (IAM) for Cloud Firestore.
I'd like to add a property to a Firebase user object. The user documentation says that I can only store additional properties using the Firebase real time database.
I am unsure on how this can works in practice.
What does the following mean in practice?
You cannot add other properties to the Firebase User object directly;
instead, you can store the additional properties in your Firebase
Realtime Database.
I interpret it as following:
"you cannot modify properties of a FIRUser object but you can combine this with additional objects"
I found the set function documentation which I interpet in this way:
var userRef = ref.child("users");
userRef.set({
newfield: "value"
});
Is this a sensible approach?
You're almost there. In the legacy Firebase documentation, we had a section on storing such additional user data.
The key is to store the additional information under the user's uid:
let newUser = [
"provider": authData.provider,
"displayName": authData.providerData["displayName"] as? NSString as? String
]
// Create a child path with a key set to the uid underneath the "users" node
// This creates a URL path like the following:
// - https://<YOUR-FIREBASE-APP>.firebaseio.com/users/<uid>
ref.childByAppendingPath("users")
.childByAppendingPath(authData.uid).setValue(newUser)
I've added a note that we should add this information in the new documentation too. We just need to find a good spot for it.
According to the Custom Claims documentation,
The Firebase Admin SDK supports defining custom attributes on user accounts. [...] User roles can be defined for the following common cases:
Add an additional identifier on a user. For example, a Firebase user could map to a different UID in another system.
[...] Custom claims payload must not exceed 1000 bytes.
However, do this only for authentication-related user data, not for general profile information, per the Best Practices:
Custom claims are only used to provide access control. They are not designed to store additional data (such as profile and other custom data). While this may seem like a convenient mechanism to do so, it is strongly discouraged as these claims are stored in the ID token and could cause performance issues because all authenticated requests always contain a Firebase ID token corresponding to the signed in user.
Use custom claims to store data for controlling user access only. All other data should be stored separately via the real-time database or other server side storage.
We'd like to use Firepad in our (mostly non-Firebase hosted) project, but we're having some troubles figuring out the best way to approach the problem.
Basically, we have many users, and each user can be a member of many groups. These "groups" each have their own Firepad which users can edit. We already have a deeply developed database structure using MySQL and don't really want to migrate our user data into Firebase right now, so we figured we'd get more creative.
We don't want users being able to edit the Firepads of groups they do not belong to. As such, as part of our authentication token, we figured we'd try sending along the user ID and the list of groups they belong to. Then, using the Firebase JSON security system, we could verify that the Firepad currently being edited is in the list of groups the user belongs to.
The problem is, the JSON system doesn't seem to accept many commands. There's no indexOf, and I can't call hasChild on the auth variable.
How can we ensure that users can only edit the Firepads of groups they belong to, without migrating all of our data to Firebase? (Or maintaining two copies of the database - one on MySQL and one on Firebase)
The trick here is to use an object instead of an array to store the groups (a tad awkward, I know. We'll try to make this easier / more intuitive). So in your auth token, you'd store something like:
{ userid: 'blah', groups: { 'group1': true, 'group2': true, ... } }
And then in your security rules you could have something like:
{
...
"$group": {
".read": "auth.groups[$group] == true",
".write": "auth.groups[$group] == true"
}
}
And then a user will have read/write access to /groups/<group> only if <group> is in their auth token.