Skip to content

LG-449 - Cancelling account deletion should notify both factors#2320

Merged
stevegsa merged 1 commit intomasterfrom
stevegsa-cancel-account-reset-should-notify-both-factors
Jul 17, 2018
Merged

LG-449 - Cancelling account deletion should notify both factors#2320
stevegsa merged 1 commit intomasterfrom
stevegsa-cancel-account-reset-should-notify-both-factors

Conversation

@stevegsa
Copy link
Contributor

@stevegsa stevegsa commented Jul 14, 2018

Why: Other than our event log and db there is no forensic evidence of a cancellation and an attacker controlling one of the factors can cancel the deletion.

How: Add an SMS and email to the cancel controller

Hi! Before submitting your PR for review, and/or before merging it, please
go through the following checklist:

  • For DB changes, check for missing indexes, check to see if the changes
    affect other apps (such as the dashboard), make sure the DB columns in the
    various environments are properly populated, coordinate with devops, plan
    migrations in separate steps.

  • For route changes, make sure GET requests don't change state or result in
    destructive behavior. GET requests should only result in information being
    read, not written.

  • For encryption changes, make sure it is compatible with data that was
    encrypted with the old code.

  • For secrets changes, make sure to update the S3 secrets bucket with the
    new configs in all environments.

  • Do not disable Rubocop or Reek offenses unless you are absolutely sure
    they are false positives. If you're not sure how to fix the offense, please
    ask a teammate.

  • When reading data, write tests for nil values, empty strings,
    and invalid formats.

  • When calling redirect_to in a controller, use _url, not _path.

  • When adding user data to the session, use the user_session helper
    instead of the session helper so the data does not persist beyond the user's
    session.

  • When adding a new controller that requires the user to be fully
    authenticated, make sure to add before_action :confirm_two_factor_authenticated.

@stevegsa
Copy link
Contributor Author

stevegsa commented Jul 14, 2018

Screenshots:

As before we get an email notification after a delete request:
screen shot 2018-07-14 at 12 15 42 am

New cancel email after cancellation from either factor:
screen shot 2018-07-14 at 12 15 51 am

New cancel SMS:
screen shot 2018-07-14 at 12 20 22 am

@stevegsa stevegsa force-pushed the stevegsa-cancel-account-reset-should-notify-both-factors branch 2 times, most recently from 98fda7c to c1a1981 Compare July 16, 2018 13:23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be perform_later? Not that it matters while we're using the inline adapter, but thought I'd as since we're using deliver_later above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should we add a guard clause somewhere to protect against a nil phone number?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! I also need to fix another spot on nil phone. Rather than a guard clause I'll just check for nil.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to only calling the job if the user has a phone, since currently, it's possible for folks to not have a number.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name for this makes me think it is resetting an SMS account. What do you think about AccountResetCancelSmsAlertJob

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That name sounds like we are cancelling an sms alert. It appears the convention is SmsXXXJob.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe one of these? Cancel makes it sound like we are canceling the Sms Alert.

  • AccountResetCancellationSmsAlertJob
  • SmsAlertForAccountResetCancellationJob

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I had SmsAccountResetCancelledJob. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention appears to also explain what the SMS is doing, such as sending an OTP or notifying about account reset, so then maybe SmsAccountResetCancellationNotifierJob?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please follow the convention where the Service/Form that the controller interacts with returns a FormResponse object? That way, you only need to make one call to analytics (as opposed to one for success, and one for failure), and it makes things consistent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, the User UUID would be captured as extra_analytics_attributes inside the Service Object. Here's an example: https://github.com/18F/identity-idp/blob/master/app/services/email_confirmation_token_validator.rb

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As would the event name and token_valid attributes. The only parameter that would go in the analytics call is result.to_h, like we do everywhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree with that convention, the complexity of this method does not warrant an additional object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. The complexity of which method? What I'm proposing is instead of a generic AccountResetService class that handles all aspects of this feature, it would be broken up into smaller classes that each take care of one aspect. Here, it would be the cancellation, so you would have a CancelAccountReset class with a submit or call method.

Also, services should not have Service in their name. That's redundant. The class should describe what it is doing. If you find yourself having to add Service to the end of a class name, that's usually a code smell indicating the class doesn't have a clearly defined purpose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another test I like to do is to try to describe what the class does in one sentence. If the sentence contains the word and, that's usually a sign it's doing too much. For example, the current AccountResetService could be described as "it handles account reset requests, and account reset cancellations, and reports fraud, and sends notifications." That's a lot of stuff for one class!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not to say that the one public method the class has can only do one thing. The class can make calls to other services, as long as those other calls are to private methods, and usually those methods would then call other classes to perform those actions. For example:

class CancelAccountReset
  def submit(params)
    success = valid?
    if success
      notify_user_via_email 
      notify_user_via_sms
    end
  end
  
  private
  
  def notify_user_via_email
    UserMailer.send_email
  end
  
  def notify_user_via_sms
    SmsAccountResetCancellationNotifierJob.perform_now
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You misunderstood. I love breaking up the service object. In fact I used that same methodology on controllers to breakup the rails monolith and everything funneling through routes. What I was referring to was an additional response object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complexity of the method should not determine whether or not an additional response object is necessary. It's about consistency and expectations. We expect all analytics events to have at a minimum success and errors attributes so we can easily differentiate between successful and unsuccessful events. This makes queries consistent for people searching in Kibana, or for the analytics team to run queries and monitor events. If I wanted to see what percentage of cancellations had an invalid token, I would normally look for properties.event_properties.success, but that doesn't exist in this case. So then, I would have to dig into all the properties and figure out which key to use to determine success, which in this case is a custom key called token_valid. This results in a poor developer experience.

Copy link
Contributor

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 a Service Object whose only purpose here is to process the deletion and validate the token, and returns a FormResponse object, much like the EmailConfirmationTokenValidator I linked to above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AccountReset service is currently doing too much. I would break it up into smaller services that each have a single responsibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general rule is that a Service Object should only have one public method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually love that rule. Now all the rails community needs to do is apply it to controllers too. That was partly how in another life I was able to ditch the routes file and breakup the Rails monolith. Hence why you'll notice my controller names sometimes slip in a verb (which is not appropriate in this codebase). We can refactor that service object separately because I do think it's the right way to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the url_helpers used for here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover from a copy and paste of the other job that needed it for account_reset_cancel_url. I'll remove it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please add a spec for when the user doesn't have a phone to make sure it doesn't send an SMS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a spec to the cancel_controller. This sms job will not be called if there is not a phone.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these are needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reset_job_queues needed ActiveJobHelper. I'll remove the url_helpers

@monfresh
Copy link
Contributor

For the sake of time, we can wait until later to refactor the AccountService class, as long as it's prioritized for the next sprint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just remembered that both this and SmsAccountResetNotifierJob need to be using the same logic as SmsOtpSenderJob to determine whether to send the SMS via Programmable SMS or via Verify, and they both need to rescue Twilio errors and log them to analytics, similar to what we do in TwoFactorAuthenticationController.

Copy link
Contributor Author

@stevegsa stevegsa Jul 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three sms jobs are still using the old format. What are the implications of them not using this new logic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implication is that International users might not receive these messages. The reason we purchased the Verify service is to improve deliverability internationally.

Relatedly, would it be be worth it to mention in the email that they should also expect to receive an SMS on their phone (if they have one)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That email idea might be useful. It would also be helpful for them to get an SMS telling them when they have the email that is used to delete their account. It seems some users are forgetting to check their email and then the link expires.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Steve and I talked about this privately, so I'm documenting here for future reference. I had forgotten that Verify is only for sending OTP codes using Verify's own message. Its use case is not for sending custom messages, so we'll stick with Programmable SMS for this feature.

@konklone
Copy link
Contributor

One wording nit - in the email version it says the request, and in the SMS version it says your request.

If I had to pick one, I'd say your request is better, as it would more quickly (and deservedly) raise a red flag with a user if it was not actually them that did it. Our system is behaving as if it was the recipient of this message who issued the cancellation, so if that assumption is wrong, the message should look in error and concerning.

Why: Other than our event log and db there is no forensic evidence of a cancellation and an attacker controlling one of the factors can cancel the deletion.

How: Add an SMS and email to the cancel controller
@stevegsa stevegsa force-pushed the stevegsa-cancel-account-reset-should-notify-both-factors branch from 52e5ed4 to f29fc9d Compare July 17, 2018 03:16
Copy link
Contributor

@monfresh monfresh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@stevegsa stevegsa merged commit 22880d1 into master Jul 17, 2018
@konklone konklone deleted the stevegsa-cancel-account-reset-should-notify-both-factors branch September 18, 2018 02:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants