Transforming before Validation
Closed this issue · 7 comments
Hi.
I'm having an issue with saving/changing a password field in the database.
As far as I see, the @Transform
decorator hashes the password
field before it is validated, which makes further validation meaningless.
@Validate("password", function(schema, data, path) {
return this.assert(Validator.password(data), "invalid password.");
})
export class UserEntity extends Instance<IUser, UserEntity> implements IUser {
@ObjectID public id?: mongo.ObjectID;
@Transform((data) => data, (data) => HashPassword(data))
@Property(String, false) public password?: string;
}
And the same goes for the onSaving
event, which is triggered before the validation, in case I try to hash my password there.
I would love to learn about some workaround, if there is one.
You're correct, unfortunately the validation is done on the underlying data since the stored data is in fact all that is held in memory by Iridium (the transforms are done when the property is accessed, with the exception of document level transforms).
My suggestion is, however, to store a passwordHash
field on your document and then have an instance level password
setter which handles this: it more clearly delinates the difference between the insecure "password" and the secure "password hash" and is less likely to result in data oddities (like writing the insecure password to a field and only being able to read the hash).
export class UserEntity extends Instance<IUser, UserEntity> implements IUser {
@ObjectID public id?: mongo.ObjectID;
@Property(String) public passwordHash?: string;
set password(value: string) {
const validated = Validator.password(value)
if (!validated.valid) throw validated.error
this.passwordHash = HashPassword(value)
}
}
In practice, this approach does effectively the same thing you are attempting to do, however it foregoes the "magic" of the @Transform
and @Validate
decorators and applies them manually.
Please let me know if this doesn't solve your requirements, or if you've got a problem with the approach that you'd like me to try and resolve within Iridium.
Regards,
Hi @spartan563 ,
Thank you for your response. I've modified my code as you have mentioned and faced a couple of issues.
Although when I change the password field and save the document, the setter is triggered, but
- When I try to save a document from outside, the setter doesn't modify neither the
password
norpasswordHash
user.password = "validpass123";
await user.save();
Now the password
has its initial value and passwordHash
is undefined.
- When I provide explicit
Changes
, it modifies thepassword
field only
await user.save({$set: {password: "validpass123"}})
Here the password
is indeed validpass123
and passwordHash
is still undefined.
Could you help me with that?
Hi, @spartan563!
Any news regarding this issue? :)
Sorry @karinehovhannisyan
Can you quickly tell me how you're creating your instance? I suspect you're just doing new User()
and then setting properties there?
Instead, can you try using new userModel.Instance()
and setting those properties? If it does work, please let me know and I'll explain why you're running into the issue.
interface UserDoc {
_id?: string;
username: string;
passwordHash?: string;
}
@Collection("users")
class User extends Instance<UserDoc, User> {
@ObjectID
_id: string;
@Property(String)
username: string;
@Property(String)
passwordHash: string;
set password(value: string) {
const validated = Validator.password(value)
if (!validated.valid) throw validated.error
this.passwordHash = HashPassword(value)
}
}
class MyCore extends Core {
Users = new Model<UserDoc, User>(this, User)
}
// Test it out
const core = new MyCore("mongo://localhost:27017/test")
await core.connect()
const user = new core.Users.Instance({ username: "testUser" })
user.password = "test"
await user.save()
const savedUser = await core.Users.get(user._id)
console.log("Saved User Password Hash:", savedUser.passwordHash)
Hi @spartan563
In reality, I do use the Instance
constructor, however, I face the issue when also using the insert
method to create the document.
Any thoughts about this?
Hi @karinehovhannisyan, sorry this has taken a while to address. With v8.0.0-alpha.13
we've standardized the meaning of TDocument
and clarified the role that transforms (and validation) play in the process. Specifically:
TInstance
represents your application's view of an object's schemaTDocument
(and your schema properties -@Property
) represent the DB's view of an object's schema- Schema validation (
@Property
) applies to the DB's view of an object's schema - Transforms are responsible for converting the types of fields on a
TDocument
to the types on aTInstance
(and vice versa)
As a result, there is no support for validating schema fields before transforms (this was always the behaviour, but it is now standardized and explained by the core design). This also results in changes to the recommended use of the .insert()
and .create()
methods - with them both recommending that "DB-ready" (in your case: post-hashing) documents be provided to them. For the time being they still support the previous behaviour and we'll be looking at how to provide a nice developer experience for users who still wish to provide "application style" objects to them while leveraging transforms.
To address the need you have for validation when assigning a value to the password
property, you can now utilize the Instance._setField
protected method in conjunction with a custom setter on your object.
interface UserDoc {
_id?: string;
username: string;
passwordHash?: string;
}
@Collection("users")
class User extends Instance<UserDoc, User> {
@ObjectID
_id: string;
@Property(String)
username: string;
set password(value: string) {
const validated = Validator.password(value)
if (!validated.valid) throw validated.error
this._setField("passwordHash", HashPassword(value))
}
isCorrectPassword(value: string) {
return this._getField("passwordHash") === HashPassword(value)
}
static createUser(username: string, password: string): UserDoc {
const validated = Validator.password(value)
if (!validated.valid) throw validated.error
return {
username,
passwordHash: HashPassword(password)
}
}
}
class MyCore extends Core {
Users = new Model<UserDoc, User>(this, User)
}
// Test it out
const core = new MyCore("mongo://localhost:27017/test")
await core.connect()
// Creating using an Instance
const user = new core.Users.Instance({ username: "testUser" })
user.password = "test"
await user.save()
// Inserting a user with a helper function
await core.Users.insert(core.Users.createUser("testUser2", "test"))
Unfortunately, regardless of all the improvements we're making to the way this stuff works, it still comes down to the need for you to perform application-level data validation, since Iridium's validation logic is primarily concerned with the DB-level data. The motivation for that approach is borne of the experience that applications tend to include weird bugs (a badly written transform function etc) and we'd rather do everything we can to safeguard the data (potentially at the cost of a short-term application crash) than safeguarding the application at the cost of the data.
Hi @spartan563,
Thank you for explanation. In that case I will proceed with pre-database validation :)