New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use UpdateUser
class when updating users
#1067
Conversation
user.update( | ||
confirmation_token: token, confirmation_sent_at: Time.current | ||
) | ||
token = Devise.token_generator.generate(User, :confirmation_token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
woah I removed the comma and this still works! is that bad?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that's necessarily bad. This Devise generator returns an array whose first element is the raw token, and the second element is the encrypted token to be stored in the DB. The generator is meant to be used such that each part of the array is assigned to a different variable.
In this case, if you remove the comma, the token
variable will be an Array, but it will get saved to the DB as a String. Then, when you make the POST request, Devise will compare the passed in token to the value in the DB. In this case, they match, so all is good.
Looking at Devise's confirmable
module, I see that the confirmation_token doesn't get encrypted. It gets generated via Devise.friendly_token
, so perhaps we should do the same in this test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this, I don't think we even need to invoke Devise at all. We just need to make sure the passed in token matches what's in the DB, so we can set it to any old String.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so right! will probably make tests faster to do that, too. updated at c299d10
f5d14de
to
bebbe3a
Compare
|
||
UpdateUser.new( | ||
user: current_user, | ||
attributes: set_time_lock_if_max_attempts_reached |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you missed the second_factor_attempts_count
attribute. Here, we definitely want to increment second_factor_attempts_count
by 1, and we want to use UpdateUser
to do that. Then, optionally, we might also want to update second_factor_locked_at
. So, I think perhaps this will do it:
attributes = { second_factor_attempts_count: current_user.second_factor_attemps_count + 1 }
attributes[:second_factor_locked_at] = Time.zone.now if current_user.max_login_attempts?
UpdateUser.new(
user: current_user,
attributes: attributes
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are still updating second_factor_attempts_count
above. If I remove Line 83, a bunch of specs fail (so it is well tested, yay!)
The code you have above works the same way as the current code. do you think it is more readable? In general I prefer longer conditional statements like we have in set_time_lock_if_max_attempts_reached
, find them easier to parse than trailing conditionals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is. I read it like this:
- Attributes are being set. Some are set conditionally.
- Update the user with the final attributes.
This way, I can see all the attributes in one place, and the update is performed using one method.
In your version, we are mixing and matching 2 ways to update the user. First, we use the setter, then we use update
. I don't think I've seen that combination before. I've seen either setter + #save
, or update
by itself. I think it has a higher cognitive load. I read it like this:
- Set some attributes on the user via setter
- Call
UpdateUser
with some other attributes - Scroll down to see what those attributes are. Notice that they can be empty, which first made me think, why are we making a DB call with empty attributes? Then I realized there were attributes being set previously via the setter call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. updated at fbd37d4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eek ok this caused many spec failures because we need to increment user second factor attempts before checking current_user.max_login_attempts?
(can't just set attr in a hash)
refactored / fixed at e9b5d9a
resource.update(confirmed_at: Time.current) unless resource.confirmed? | ||
resource.update(password: user_params[:password]) | ||
resource.confirmed_at = Time.current unless resource.confirmed? | ||
UpdateUser.new(user: resource, attributes: { password: user_params[:password] }).call | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here about using UpdateUser
consistently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which comment are you referring to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment about using one method to update the user, as opposed to mixing and matching setter + UpdateUser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it! updated at fbd37d4
UpdateUser.new( | ||
user: current_user, | ||
attributes: { idv_attempts: current_user.idv_attempts + 1, idv_attempted_at: Time.current } | ||
).call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the change from Time.zone.now
to Time.current
? I think we should be consistent and stick with one. Looks like we use the former more than the latter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
honestly: line length. I saw that we are using them both so thought I'd take the short cut. Seems like usage is kind of 50/50?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Up to you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great % comments. Thanks for doing this!
52bb9ca
to
3fae0e3
Compare
**Why**: Makes our app less tied to Rails
**Why**: Need to increment user second factor attempts before checking `current_user.max_login_attempts?`
e9b5d9a
to
b6e043f
Compare
@@ -14,7 +14,7 @@ def rotate(user:, pii_attributes: nil, profile: nil) | |||
|
|||
def rotate_email_fingerprint(user) | |||
ee = EncryptedAttribute.new_from_decrypted(user.email) | |||
user.update_columns(email_fingerprint: ee.fingerprint) | |||
UpdateUser.new(user: user, attributes: { email_fingerprint: ee.fingerprint }).call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it important for this to be update_columns
instead of update!
?
Behavior is slightly different but changing this breaks no tests. If we want to revert, I can create a ticket to write tests for this so we know not to change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want the updated_at
timestamp to change, since no user data is changing. No hard biz requirement around that. I just prefer maintenance tasks like this to be as transparent as possible. ref http://www.davidverhasselt.com/set-attributes-in-activerecord/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is your suggestion that we revert and write some tests later, or keep as is? Happy to do either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update_columns
is faster, but it doesn't perform any validations or callbacks. Given that we don't have any in the User model (except for set_default_role
), I think it should be fine to use update_columns
. I would vote to pick one for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @monfresh when you say "pick one for consistency", what exactly are you referring to? Totally down with using update_columns
in the key rotator classes, but assume we'd want to stick with update!
elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that my decision to use update_columns
was both for performance reasons (rotating millions of records can take time), and to avoid the updated_at
timestamp, I would prefer to see it stay in this instance. ActiveRecord has multiple ways to do things, because a single method does not fit every need. I would rather keep it here. Adding more indirection doesn't help here IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, will revert change here and add tests separately so we don't make this change again! https://github.com/18F/identity-private/issues/1450
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted at 6ac6a7b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I'm saying is that I think we can use update_columns
everywhere because we don't have any validations in the User model. update!
will never raise an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh huh that's interesting, sorry I merged before I saw this comment.
I feel like using an update method that does not run validations or callbacks makes sense in some situtations, like this one, but perhaps not all?
In general in any rails app I would expect records to be updated with update
or update!
, but I could be convinced otherwise, I supposed.
**Why**: Makes our app less tied to Rails
**Why**: Makes our app less tied to Rails
Why: Makes our app less tied to Rails