Skip to content
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

Adding password reset email change fix #400

Merged
merged 14 commits into from
Sep 8, 2023

Conversation

arroyoAle
Copy link
Contributor

@arroyoAle arroyoAle commented Aug 30, 2023

What and why?

This fixes the case where a user changes email while having requested a forgot password link by deleting the password reset token in the database when the party email is changed

How to test?

  • deploy to environment
  • follow the forgot password path for an account
  • check the token is in the database
  • change the user email and verify the change through the link provided
  • check the token has been removed from the database and the counter is reset

Jira

https://jira.ons.gov.uk/browse/RAS-840

@arroyoAle arroyoAle changed the title dding password reset email change fix Adding password reset email change fix Aug 30, 2023
@arroyoAle arroyoAle marked this pull request as ready for review September 4, 2023 11:29
@arroyoAle
Copy link
Contributor Author

/deploy arroya

@ras-rm-pr-bot
Copy link
Collaborator

Deploying to dev cluster with following parameters:

  • namespace: arroya

  • tag: pr-400

  • configBranch: main

  • paramKey: ``

  • paramValue: ``

Copy link
Contributor

@LJBabbage LJBabbage left a comment

Choose a reason for hiding this comment

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

This needs a 2nd look. We should also try not to update the Pipfile.lock if not needed, just to limit the amount of things changing outside the scope of this PR. We also should be looking at line coverage when adding code, in this instance that would have helped identify some problems

@@ -746,6 +746,15 @@ def put_email_verification(token, tran, session):
# We set the user as verified on the OAuth2 server.
set_user_verified(email_address)

if not respondent.pending_email_address and respondent.password_verification_token:
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels in the wrong place and order. The issue materializes because the email in the token has the original email and due to the pending becoming the new email makes it makes it invalid. With that in mind it should be there and not here. Having it here puts in on the original verification path which isn't valid.

It should be done inside update_verified_email_address or alternatively after line 727

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been moved up to line 729 right after the email update

@@ -746,6 +746,15 @@ def put_email_verification(token, tran, session):
# We set the user as verified on the OAuth2 server.
set_user_verified(email_address)

if not respondent.pending_email_address and respondent.password_verification_token:
if not respondent.password_verification_token:
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this reachable code? As the above means respondent.password_verification_token has to be True

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The second if statement has been removed

raise NotFound("Verification token not found")

# Reset password token fields in the database since they are linked to the old email
delete_respondent_password_verification_token(respondent.party_uuid, session)
Copy link
Contributor

@LJBabbage LJBabbage Sep 5, 2023

Choose a reason for hiding this comment

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

I do wonder if these 2 are intrinsically linked and should have a parent function, maybe worth checking other usages in the code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are linked but there's no parent function and adding it would require work to frontstage as well and I think is out of the scope of the card

@@ -434,3 +434,31 @@ def get_pending_surveys_with_originator_party_id(self, party_id, expected_status
def delete_pending_surveys_with_batch_no(self, batch_no, expected_status=202):
response = self.client.delete(f"/party-api/v1/pending-surveys/{batch_no}", headers=self.auth_headers)
self.assertStatus(response, expected_status)

def get_password_verification_token(self, party_id, expected_status=200):
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this used? I can't see where

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 had added these for completeness but they are now removed

self.assertStatus(response, expected_status)
return response.json

def post_password_verification_token(self, party_id, payload, expected_status=200):
Copy link
Contributor

Choose a reason for hiding this comment

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

Same again, perhaps I am missing it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as above

@@ -1215,6 +1215,30 @@ def test_put_email_verification_uses_case_insensitive_email_query(self):
account_controller.put_email_verification(token)
query.assert_called_once_with("[email protected]", db.session())

def test_email_verification_resets_password_reset(self):
self.populate_with_respondent()
Copy link
Contributor

Choose a reason for hiding this comment

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

This returns a respondent, so you don't need the line below

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like it might even do a token as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been updated

db_respondent = respondents()[0]
token = self.generate_valid_token_from_email(db_respondent.email_address)
response = self.put_email_verification(token, 200)
db_respondent = respondents()[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure why this has been re-assigned to the same thing as above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This updates the respondent variable which would otherwise fail the tests as it still contains the verification token and counter is 1

db_respondent = respondents()[0]
self.assertIsNone(db_respondent.password_verification_token)
self.assertEqual(db_respondent.password_reset_counter, 0)
self.assertEqual(response["status"], RespondentStatus.ACTIVE.name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Checking status, is this needed, there should be another test that does that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been removed

self.assertEqual(db_respondent.password_reset_counter, 0)
self.assertEqual(response["status"], RespondentStatus.ACTIVE.name)

def test_email_verification_no_password_reset_token(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to the above with comments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As above

self.get_respondent_by_id(respondent.party_uuid)
with self.assertRaises(Exception):
self.get_respondent_by_email(respondent_1.email_address)
# def test_batch_delete_user_data_marked_for_deletion(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why has this been commented out? There should be reason in the code, on the PR. Or if not needed just removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was just to test locally since it was hanging for a long time locally, has been reverted

@@ -726,6 +726,10 @@ def put_email_verification(token, tran, session):

if respondent:
update_verified_email_address(respondent, tran)
if not respondent.pending_email_address and respondent.password_verification_token:
Copy link
Contributor

Choose a reason for hiding this comment

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

This still needs some work, to get to this line we know this is a pending email update. The respondent has received an email which when they clicks updates the email with the pending and then removes the pending_email (line 798) (i.e update_verified_email_address)

Now because of that and the order this has been put in if not respondent.pending_email_address becomes redundant as the code underneath will always set respondent.pending_email_address to None, all we need to do is check the verification token existence

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been updated

response = self.client.delete(
f"/party-api/v1/respondents/{party_id}/password-verification-token/{token}", headers=self.auth_headers
)
self.assertStatus(response, expected_status)
Copy link
Contributor

Choose a reason for hiding this comment

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

This assert is still a bit light and password-verification-token returns return jsonify({"message": "Successfully removed token"}) which we should use

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added functionality to check what message is returned by the api

@@ -434,3 +434,15 @@ def get_pending_surveys_with_originator_party_id(self, party_id, expected_status
def delete_pending_surveys_with_batch_no(self, batch_no, expected_status=202):
response = self.client.delete(f"/party-api/v1/pending-surveys/{batch_no}", headers=self.auth_headers)
self.assertStatus(response, expected_status)

def delete_password_verification_token(self, party_id, token, expected_status=200):
Copy link
Contributor

Choose a reason for hiding this comment

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

We wouldn't usually have a default var in a function when it is single use. It does make me wonder how we are testing 404 failures, but that is for another story I guess

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the password token parts the rest is getting tested in the test_account_controller.py

)
self.assertStatus(response, expected_status)

def reset_password_reset_counter(self, party_id, expected_status=200):
Copy link
Contributor

Choose a reason for hiding this comment

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

Both comments above are valid here as well, although return jsonify({"message": "Successfully reset counter"}) this time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added functionality to check what message is returned by the api

self.assertIsNone(respondent.password_verification_token)
self.assertEqual(respondent.password_reset_counter, 0)

def test_email_verification_no_password_reset_token(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this function? does it not have line coverage already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's more to check if the section doesn't fall over when there is no verification token as the rest of the tests have a token by default

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, it's not really needed as all you are doing is removing the token and then running the code and making sure it's not there. Not major, but I would removed

Copy link
Contributor

@LJBabbage LJBabbage left a comment

Choose a reason for hiding this comment

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

Definitely looking better and party is there as far as I can see, but there is more to do on this story. If you run it into GCP and request a password change and then go in and change the email (by clicking the email etc) the token is removed as expected, however if you then complete the circuit and click the password change email you get a nasty 500 in the code, because of this https://github.com/ONSdigital/ras-frontstage/blob/main/frontstage/views/passwords/reset_password.py#L29. and that is caused as the 404 is just returning https://github.com/ONSdigital/ras-frontstage/blob/main/frontstage/controllers/party_controller.py#L151 The passwords/password-token-not-found.html is probably correct, the code has not been written correctly for the 404

@arroyoAle arroyoAle merged commit 95fa70b into main Sep 8, 2023
1 check passed
@arroyoAle arroyoAle deleted the fix-password-reset-with-email-change branch September 8, 2023 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants