From a02448a0fa979a37b9b1df83ad8b667ff04c40dc Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 25 Oct 2024 16:10:45 -0400 Subject: [PATCH 01/13] Removing unused state parameter for _get_access_token() Signed-off-by: Cliff Hill --- app/main/views/register.py | 2 +- app/main/views/sign_in.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index cc6850055..8f2ae34a7 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -164,7 +164,7 @@ def set_up_your_profile(): login_gov_error = request.args.get("error") if redis_client.get(f"invitedata-{state}") is None: - access_token = sign_in._get_access_token(code, state) + access_token = sign_in._get_access_token(code) debug_msg("Got the access token for login.gov") user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index a326202f3..9c8fbb3f0 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -39,7 +39,7 @@ def _reformat_keystring(orig): # pragma: no cover return new_keystring -def _get_access_token(code, state): # pragma: no cover +def _get_access_token(code): # pragma: no cover client_id = os.getenv("LOGIN_DOT_GOV_CLIENT_ID") access_token_url = os.getenv("LOGIN_DOT_GOV_ACCESS_TOKEN_URL") keystring = os.getenv("LOGIN_PEM") @@ -110,7 +110,7 @@ def _do_login_dot_gov(): # $ pragma: no cover # activate the user try: - access_token = _get_access_token(code, state) + access_token = _get_access_token(code) user_email, user_uuid = _get_user_email_and_uuid(access_token) if not is_gov_user(user_email): current_app.logger.error( @@ -211,7 +211,7 @@ def sign_in(): # pragma: no cover url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") nonce = secrets.token_urlsafe() - redis_key = f"-{unquote(nonce)}" + redis_key = f"login-nonce-{unquote(nonce)}" redis_client.set(redis_key, nonce) # handle unit tests From a39c844c30445e7496583a026a136037a15841dc Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 25 Oct 2024 16:28:25 -0400 Subject: [PATCH 02/13] Properly handling and validating the state for login.gov Signed-off-by: Cliff Hill --- app/main/views/index.py | 18 +++++++++--------- app/main/views/sign_in.py | 22 +++++++++++++++------- app/notify_client/invite_api_client.py | 13 +++++++++++++ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/app/main/views/index.py b/app/main/views/index.py index 8b965991c..8d00305c7 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -35,24 +35,24 @@ def index(): if current_user and current_user.is_authenticated: return redirect(url_for("main.choose_account")) - token = generate_token( + # make and store the state + state = generate_token( str(request.remote_addr), current_app.config["SECRET_KEY"], current_app.config["DANGEROUS_SALT"], ) - url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") - # handle unit tests - - current_app.logger.warning(f"############### {str(request.remote_addr)}") + state_key = f"login-nonce-{unquote(state)}" + redis_client.set(state_key, state) + # make and store the nonce nonce = secrets.token_urlsafe() + nonce_key = f"login-nonce-{unquote(nonce)}" + redis_client.set(nonce_key, nonce) - redis_key = f"login-nonce-{unquote(nonce)}" - redis_client.set(redis_key, nonce) - + url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") if url is not None: url = url.replace("NONCE", nonce) - url = url.replace("STATE", token) + url = url.replace("STATE", state) return render_template( "views/signedout.html", sms_rate=CURRENT_SMS_RATE, diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 9c8fbb3f0..34f5badb3 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -66,8 +66,8 @@ def _get_access_token(code): # pragma: no cover response_json = response.json() id_token = get_id_token(response_json) nonce = id_token["nonce"] - redis_key = f"login-nonce-{unquote(nonce)}" - stored_nonce = redis_client.get(redis_key).decode("utf8") + nonce_key = f"login-nonce-{unquote(nonce)}" + stored_nonce = redis_client.get(nonce_key).decode("utf8") if nonce != stored_nonce: current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}") @@ -99,6 +99,12 @@ def _do_login_dot_gov(): # $ pragma: no cover # start login.gov code = request.args.get("code") state = request.args.get("state") + state_key = f"login-state-{unquote(state)}" + stored_state = redis_client.get(state_key).decode("utf8") + if state != stored_state: + current_app.logger.error(f"State Error: {state} != {stored_state}") + abort(403) + login_gov_error = request.args.get("error") if login_gov_error: @@ -203,21 +209,23 @@ def sign_in(): # pragma: no cover return redirect(redirect_url) return redirect(url_for("main.show_accounts_or_dashboard")) - token = generate_token( + state = generate_token( str(request.remote_addr), current_app.config["SECRET_KEY"], current_app.config["DANGEROUS_SALT"], ) - url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") + state_key = f"login-state-{unquote(state)}" + redis_client.set(state_key, state) nonce = secrets.token_urlsafe() - redis_key = f"login-nonce-{unquote(nonce)}" - redis_client.set(redis_key, nonce) + nonce_key = f"login-nonce-{unquote(nonce)}" + redis_client.set(nonce_key, nonce) + url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL") # handle unit tests if url is not None: url = url.replace("NONCE", nonce) - url = url.replace("STATE", token) + url = url.replace("STATE", state) return render_template( "views/signin.html", diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index 711cc1f55..d45137981 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -1,12 +1,15 @@ import secrets from urllib.parse import unquote +from flask import current_app, request + from app import redis_client from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache from app.utils.user_permissions import ( all_ui_permissions, translate_permissions_from_ui_to_db, ) +from notifications_utils.url_safe_token import generate_token class InviteApiClient(NotifyAdminAPIClient): @@ -37,11 +40,21 @@ class InviteApiClient(NotifyAdminAPIClient): } data = _attach_current_user(data) + # make and store the state + state = generate_token( + str(request.remote_addr), + current_app.config["SECRET_KEY"], + current_app.config["DANGEROUS_SALT"], + ) + state_key = f"login-state-{unquote(state)}" + redis_client.set(state_key, state) + # make and store the nonce nonce = secrets.token_urlsafe() redis_key = f"login-nonce-{unquote(nonce)}" redis_client.set(f"{redis_key}", nonce) # save the nonce to redis. data["nonce"] = nonce # This is passed to api for the invite url. + data["state"] = state # This is passed to api for the invite url. resp = self.post(url=f"/service/{service_id}/invite", data=data) return resp["data"] From a090c90ed644a35ab065fcba2907c68d80c29e60 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 25 Oct 2024 16:55:34 -0400 Subject: [PATCH 03/13] Debuggin' Signed-off-by: Cliff Hill --- app/main/views/index.py | 2 +- app/main/views/register.py | 8 ++++++++ app/main/views/sign_in.py | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/main/views/index.py b/app/main/views/index.py index 8d00305c7..7728cb325 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -41,7 +41,7 @@ def index(): current_app.config["SECRET_KEY"], current_app.config["DANGEROUS_SALT"], ) - state_key = f"login-nonce-{unquote(state)}" + state_key = f"login-state-{unquote(state)}" redis_client.set(state_key, state) # make and store the nonce diff --git a/app/main/views/register.py b/app/main/views/register.py index 8f2ae34a7..10422aefd 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -2,6 +2,7 @@ import base64 import json import uuid from datetime import datetime, timedelta +from urllib.parse import unquote from flask import ( abort, @@ -161,6 +162,13 @@ def set_up_your_profile(): debug_msg(f"Enter set_up_your_profile with request.args {request.args}") code = request.args.get("code") state = request.args.get("state") + + state_key = f"login-state-{unquote(state)}" + stored_state = redis_client.get(state_key).decode("utf8") + if state != stored_state: + current_app.logger.error(f"State Error: {state} != {stored_state}") + abort(403) + login_gov_error = request.args.get("error") if redis_client.get(f"invitedata-{state}") is None: diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 34f5badb3..8ae3a9770 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -99,11 +99,6 @@ def _do_login_dot_gov(): # $ pragma: no cover # start login.gov code = request.args.get("code") state = request.args.get("state") - state_key = f"login-state-{unquote(state)}" - stored_state = redis_client.get(state_key).decode("utf8") - if state != stored_state: - current_app.logger.error(f"State Error: {state} != {stored_state}") - abort(403) login_gov_error = request.args.get("error") @@ -113,6 +108,11 @@ def _do_login_dot_gov(): # $ pragma: no cover ) raise Exception(f"Could not login with login.gov {login_gov_error}") elif code and state: + state_key = f"login-state-{unquote(state)}" + stored_state = redis_client.get(state_key).decode("utf8") + if state != stored_state: + current_app.logger.error(f"State Error: {state} != {stored_state}") + abort(403) # activate the user try: From fc09e70579bbd295a2627f9202f97cacde9dbbf7 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 30 Oct 2024 09:03:23 -0400 Subject: [PATCH 04/13] Working on fixing the tests. --- tests/app/notify_client/test_invite_client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/app/notify_client/test_invite_client.py b/tests/app/notify_client/test_invite_client.py index a251d5367..4bc81e127 100644 --- a/tests/app/notify_client/test_invite_client.py +++ b/tests/app/notify_client/test_invite_client.py @@ -10,6 +10,7 @@ def test_client_creates_invite( sample_invite, ): mocker.patch("app.notify_client.current_user") + mocker.patch("flask.request") mock_post = mocker.patch( "app.invite_api_client.post", @@ -26,6 +27,7 @@ def test_client_creates_invite( "auth_type", "folder_permissions", "nonce", + "state", } ) }, @@ -33,8 +35,12 @@ def test_client_creates_invite( mock_token_urlsafe = mocker.patch("secrets.token_urlsafe") fake_nonce = "1234567890" + fake_state = "0987654321" mock_token_urlsafe.return_value = fake_nonce + mock_generate_token = mocker.patch("notifications_utils.url_safe_token.generate_token") + mock_generate_token.return_value = fake_state + invite_api_client.create_invite( "12345", "67890", "test@example.com", {"send_messages"}, "sms_auth", [fake_uuid] ) @@ -51,6 +57,7 @@ def test_client_creates_invite( "invite_link_host": "http://localhost:6012", "folder_permissions": [fake_uuid], "nonce": fake_nonce, + "state": fake_state, }, ) From b532ce8959cda71ff06f6e691b0e5b5a63fa212b Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 1 Nov 2024 12:12:04 -0400 Subject: [PATCH 05/13] Fixing tests, and resend invites endpoint. Signed-off-by: Cliff Hill --- app/main/views/sign_in.py | 2 +- app/notify_client/invite_api_client.py | 22 ++++++++++++++++++- tests/app/notify_client/test_invite_client.py | 21 +++++++++++++----- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 8ae3a9770..d948f459e 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -109,7 +109,7 @@ def _do_login_dot_gov(): # $ pragma: no cover raise Exception(f"Could not login with login.gov {login_gov_error}") elif code and state: state_key = f"login-state-{unquote(state)}" - stored_state = redis_client.get(state_key).decode("utf8") + stored_state = unquote(redis_client.get(state_key).decode("utf8")) if state != stored_state: current_app.logger.error(f"State Error: {state} != {stored_state}") abort(403) diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index d45137981..5ae1c2807 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -88,7 +88,27 @@ class InviteApiClient(NotifyAdminAPIClient): self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data) def resend_invite(self, service_id, invited_user_id): - self.post(url=f"/service/{service_id}/invite/{invited_user_id}/resend", data={}) + # make and store the state + state = generate_token( + str(request.remote_addr), + current_app.config["SECRET_KEY"], + current_app.config["DANGEROUS_SALT"], + ) + state_key = f"login-state-{unquote(state)}" + redis_client.set(state_key, state) + + # make and store the nonce + nonce = secrets.token_urlsafe() + nonce_key = f"login-nonce-{unquote(nonce)}" + redis_client.set(nonce_key, nonce) + + data = { + "nonce": nonce, + "state": state, + } + self.post( + url=f"/service/{service_id}/invite/{invited_user_id}/resend", data=data + ) @cache.delete("service-{service_id}") @cache.delete("user-{invited_user_id}") diff --git a/tests/app/notify_client/test_invite_client.py b/tests/app/notify_client/test_invite_client.py index 4bc81e127..4cf21c9e1 100644 --- a/tests/app/notify_client/test_invite_client.py +++ b/tests/app/notify_client/test_invite_client.py @@ -1,5 +1,7 @@ from unittest.mock import ANY +from flask import current_app + from app import invite_api_client @@ -10,7 +12,6 @@ def test_client_creates_invite( sample_invite, ): mocker.patch("app.notify_client.current_user") - mocker.patch("flask.request") mock_post = mocker.patch( "app.invite_api_client.post", @@ -38,15 +39,23 @@ def test_client_creates_invite( fake_state = "0987654321" mock_token_urlsafe.return_value = fake_nonce - mock_generate_token = mocker.patch("notifications_utils.url_safe_token.generate_token") + mock_generate_token = mocker.patch( + "app.notify_client.invite_api_client.generate_token" + ) mock_generate_token.return_value = fake_state - invite_api_client.create_invite( - "12345", "67890", "test@example.com", {"send_messages"}, "sms_auth", [fake_uuid] - ) + with current_app.test_request_context("/whatever"): + invite_api_client.create_invite( + "12345", + "67890", + "test@example.com", + {"send_messages"}, + "sms_auth", + [fake_uuid], + ) mock_post.assert_called_once_with( - url="/service/{}/invite".format("67890"), + url=f"/service/{"67890"}/invite", data={ "auth_type": "sms_auth", "email_address": "test@example.com", From c8ed66cbfe9d51baf994bc2fb752ad167b039584 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 6 Nov 2024 10:41:20 -0500 Subject: [PATCH 06/13] Storing user data in redis. Signed-off-by: Cliff Hill --- app/main/views/register.py | 25 ++++++++++++++++--------- app/notify_client/invite_api_client.py | 8 ++++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index 10422aefd..83328afe9 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -164,14 +164,24 @@ def set_up_your_profile(): state = request.args.get("state") state_key = f"login-state-{unquote(state)}" - stored_state = redis_client.get(state_key).decode("utf8") + stored_state = unquote(redis_client.get(state_key).decode("utf8")) if state != stored_state: current_app.logger.error(f"State Error: {state} != {stored_state}") abort(403) login_gov_error = request.args.get("error") - if redis_client.get(f"invitedata-{state}") is None: + invite_data = json.loads(redis_client.get(f"invitedata-{state}")) + user_email = redis_client.get(f"user_email-{state}").decode("utf8") + user_uuid = redis_client.get(f"user_uuid-{state}").decode("utf8") + # invite_data = json.loads(redis_client.get(f"invitedata-{state}")) + # user_email = redis_client.get(f"user_email-{state}").decode("utf8") + # user_uuid = redis_client.get(f"user_uuid-{state}").decode("utf8") + # invited_user_email_address = redis_client.get( + # f"invited_user_email_address-{state}" + # ).decode("utf8") + + if user_email is None or user_uuid is None: # invite path access_token = sign_in._get_access_token(code) debug_msg("Got the access token for login.gov") @@ -179,9 +189,9 @@ def set_up_your_profile(): debug_msg( f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov" ) - invite_data = state.encode("utf8") - invite_data = base64.b64decode(invite_data) - invite_data = json.loads(invite_data) + # invite_data = state.encode("utf8") + # invite_data = base64.b64decode(invite_data) + # invite_data = json.loads(invite_data) debug_msg(f"final state {invite_data}") invited_user_id = invite_data["invited_user_id"] invited_user_email_address = get_invited_user_email_address(invited_user_id) @@ -202,10 +212,7 @@ def set_up_your_profile(): form = SetupUserProfileForm() - if ( - form.validate_on_submit() - and redis_client.get(f"invitedata-{state}") is not None - ): + if form.validate_on_submit(): invite_data, user_email, user_uuid, invited_user_email_address = ( get_invite_data_from_redis(state) ) diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index 5ae1c2807..6f8555af0 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -51,8 +51,12 @@ class InviteApiClient(NotifyAdminAPIClient): # make and store the nonce nonce = secrets.token_urlsafe() - redis_key = f"login-nonce-{unquote(nonce)}" - redis_client.set(f"{redis_key}", nonce) # save the nonce to redis. + nonce_key = f"login-nonce-{unquote(nonce)}" + redis_client.set(f"{nonce_key}", nonce) # save the nonce to redis. + + redis_invite_data = json.dumps(data) + redis_client.set(f"invitedata-{state}", json.dumps(invite_data), ex=ttl) + data["nonce"] = nonce # This is passed to api for the invite url. data["state"] = state # This is passed to api for the invite url. From d798d3695e32a7e54f2ec0f4978fd81d3b725b2f Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 6 Nov 2024 10:48:28 -0500 Subject: [PATCH 07/13] More little fixes. Signed-off-by: Cliff Hill --- app/notify_client/invite_api_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index 6f8555af0..7dbdca0e5 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -1,3 +1,4 @@ +import json import secrets from urllib.parse import unquote @@ -52,10 +53,11 @@ class InviteApiClient(NotifyAdminAPIClient): # make and store the nonce nonce = secrets.token_urlsafe() nonce_key = f"login-nonce-{unquote(nonce)}" - redis_client.set(f"{nonce_key}", nonce) # save the nonce to redis. + redis_client.set(nonce_key, nonce) # save the nonce to redis. + invite_data_key = f"invitedata-{state}" redis_invite_data = json.dumps(data) - redis_client.set(f"invitedata-{state}", json.dumps(invite_data), ex=ttl) + redis_client.set(invite_data_key, redis_invite_data) data["nonce"] = nonce # This is passed to api for the invite url. data["state"] = state # This is passed to api for the invite url. From f363a8e184689bef34b58b87c127befa4db169b9 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 6 Nov 2024 14:30:28 -0500 Subject: [PATCH 08/13] Getting all the needed data in place. Signed-off-by: Cliff Hill --- app/main/views/register.py | 19 +++++-------------- app/notify_client/invite_api_client.py | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/main/views/register.py b/app/main/views/register.py index 83328afe9..d5188e5ce 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -1,4 +1,3 @@ -import base64 import json import uuid from datetime import datetime, timedelta @@ -171,15 +170,8 @@ def set_up_your_profile(): login_gov_error = request.args.get("error") - invite_data = json.loads(redis_client.get(f"invitedata-{state}")) - user_email = redis_client.get(f"user_email-{state}").decode("utf8") - user_uuid = redis_client.get(f"user_uuid-{state}").decode("utf8") - # invite_data = json.loads(redis_client.get(f"invitedata-{state}")) - # user_email = redis_client.get(f"user_email-{state}").decode("utf8") - # user_uuid = redis_client.get(f"user_uuid-{state}").decode("utf8") - # invited_user_email_address = redis_client.get( - # f"invited_user_email_address-{state}" - # ).decode("utf8") + user_email = redis_client.get(f"user_email-{state}") + user_uuid = redis_client.get(f"user_uuid-{state}") if user_email is None or user_uuid is None: # invite path access_token = sign_in._get_access_token(code) @@ -189,11 +181,10 @@ def set_up_your_profile(): debug_msg( f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov" ) - # invite_data = state.encode("utf8") - # invite_data = base64.b64decode(invite_data) - # invite_data = json.loads(invite_data) + invite_data = redis_client.get(f"invitedata-{state}") + invite_data = json.loads(invite_data) debug_msg(f"final state {invite_data}") - invited_user_id = invite_data["invited_user_id"] + invited_user_id = invite_data["user_id"] invited_user_email_address = get_invited_user_email_address(invited_user_id) debug_msg(f"email address from the invite_date is {invited_user_email_address}") check_invited_user_email_address_matches_expected( diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index 7dbdca0e5..994ccfd3f 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -55,15 +55,26 @@ class InviteApiClient(NotifyAdminAPIClient): nonce_key = f"login-nonce-{unquote(nonce)}" redis_client.set(nonce_key, nonce) # save the nonce to redis. - invite_data_key = f"invitedata-{state}" - redis_invite_data = json.dumps(data) - redis_client.set(invite_data_key, redis_invite_data) - data["nonce"] = nonce # This is passed to api for the invite url. data["state"] = state # This is passed to api for the invite url. resp = self.post(url=f"/service/{service_id}/invite", data=data) - return resp["data"] + + resp_data = resp["data"] + invite_data_key = f"invitedata-{unquote(state)}" + remap_keys = { + "service": "service_id", + "from_user": "from_user_id", + "id": "user_id", + } + redis_invite_data = { + remap_keys[key] if key in remap_keys else key: value + for key, value in resp_data.items() + } + redis_invite_data = json.dumps(redis_invite_data) + redis_client.set(invite_data_key, redis_invite_data) + + return resp_data def get_invites_for_service(self, service_id): return self.get(f"/service/{service_id}/invite")["data"] From 95a39cfd318f663c66f96424c35dcee9e1cbfb61 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 6 Nov 2024 16:04:18 -0500 Subject: [PATCH 09/13] Trying to get invites to flow correctly. Signed-off-by: Cliff Hill --- app/main/views/invites.py | 10 ++++------ app/main/views/register.py | 20 ++++++++++---------- app/notify_client/invite_api_client.py | 11 +---------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/app/main/views/invites.py b/app/main/views/invites.py index 49fb66f88..07f6b3ac8 100644 --- a/app/main/views/invites.py +++ b/app/main/views/invites.py @@ -17,14 +17,12 @@ def accept_invite(token): and current_user.email_address.lower() != invited_user.email_address.lower() ): message = Markup( - """ - You’re signed in as {}. + f""" + You’re signed in as {current_user.email_address}. This invite is for another email address. - Sign out + Sign out and click the link again to accept this invite. - """.format( - current_user.email_address, url_for("main.sign_out") - ) + """ ) flash(message=message) diff --git a/app/main/views/register.py b/app/main/views/register.py index d5188e5ce..1762cb285 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -173,7 +173,9 @@ def set_up_your_profile(): user_email = redis_client.get(f"user_email-{state}") user_uuid = redis_client.get(f"user_uuid-{state}") - if user_email is None or user_uuid is None: # invite path + new_user = user_email is None or user_uuid is None + + if new_user: # invite path access_token = sign_in._get_access_token(code) debug_msg("Got the access token for login.gov") @@ -184,7 +186,7 @@ def set_up_your_profile(): invite_data = redis_client.get(f"invitedata-{state}") invite_data = json.loads(invite_data) debug_msg(f"final state {invite_data}") - invited_user_id = invite_data["user_id"] + invited_user_id = invite_data["id"] invited_user_email_address = get_invited_user_email_address(invited_user_id) debug_msg(f"email address from the invite_date is {invited_user_email_address}") check_invited_user_email_address_matches_expected( @@ -193,7 +195,7 @@ def set_up_your_profile(): invited_user_accept_invite(invited_user_id) debug_msg( - f"accepted invite user {invited_user_email_address} to service {invite_data['service_id']}" + f"accepted invite user {invited_user_email_address} to service {invite_data['service']}" ) # We need to avoid taking a second trip through the login.gov code because we cannot pull the # access token twice. So once we retrieve these values, let's park them in redis for 15 minutes @@ -203,7 +205,7 @@ def set_up_your_profile(): form = SetupUserProfileForm() - if form.validate_on_submit(): + if form.validate_on_submit() and not new_user: invite_data, user_email, user_uuid, invited_user_email_address = ( get_invite_data_from_redis(state) ) @@ -229,17 +231,15 @@ def set_up_your_profile(): debug_msg("activated user") usr = User.from_id(user["id"]) usr.add_to_service( - invite_data["service_id"], + invite_data["service"], invite_data["permissions"], invite_data["folder_permissions"], - invite_data["from_user_id"], - ) - debug_msg( - f"Added user {usr.email_address} to service {invite_data['service_id']}" + invite_data["from_user"], ) + debug_msg(f"Added user {usr.email_address} to service {invite_data['service']}") # notify-admin-1766 # redirect new users to templates area of new service instead of dashboard - service_id = invite_data["service_id"] + service_id = invite_data["service"] url = url_for(".service_dashboard", service_id=service_id) url = f"{url}/templates" return redirect(url) diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index 994ccfd3f..9184abbd1 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -62,16 +62,7 @@ class InviteApiClient(NotifyAdminAPIClient): resp_data = resp["data"] invite_data_key = f"invitedata-{unquote(state)}" - remap_keys = { - "service": "service_id", - "from_user": "from_user_id", - "id": "user_id", - } - redis_invite_data = { - remap_keys[key] if key in remap_keys else key: value - for key, value in resp_data.items() - } - redis_invite_data = json.dumps(redis_invite_data) + redis_invite_data = json.dumps(resp_data) redis_client.set(invite_data_key, redis_invite_data) return resp_data From 14bbabcab7874be74f428f80809e0301803fe6e4 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 7 Nov 2024 08:37:45 -0500 Subject: [PATCH 10/13] Invites are now working. Signed-off-by: Cliff Hill --- app/main/views/invites.py | 4 ++-- app/main/views/register.py | 15 +++++++++------ app/notify_client/invite_api_client.py | 3 ++- app/notify_client/user_api_client.py | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/main/views/invites.py b/app/main/views/invites.py index 07f6b3ac8..a4c936456 100644 --- a/app/main/views/invites.py +++ b/app/main/views/invites.py @@ -38,9 +38,9 @@ def accept_invite(token): ) if invited_user.status == "accepted": session.pop("invited_user_id", None) - service = Service.from_id(invited_user.service) + service = Service.from_id(invited_user.service_id) return redirect( - url_for("main.service_dashboard", service_id=invited_user.service) + url_for("main.service_dashboard", service_id=invited_user.service_id) ) session["invited_user_id"] = invited_user.id diff --git a/app/main/views/register.py b/app/main/views/register.py index 1762cb285..d0ceb65a9 100644 --- a/app/main/views/register.py +++ b/app/main/views/register.py @@ -186,7 +186,7 @@ def set_up_your_profile(): invite_data = redis_client.get(f"invitedata-{state}") invite_data = json.loads(invite_data) debug_msg(f"final state {invite_data}") - invited_user_id = invite_data["id"] + invited_user_id = invite_data["invited_user_id"] invited_user_email_address = get_invited_user_email_address(invited_user_id) debug_msg(f"email address from the invite_date is {invited_user_email_address}") check_invited_user_email_address_matches_expected( @@ -195,7 +195,7 @@ def set_up_your_profile(): invited_user_accept_invite(invited_user_id) debug_msg( - f"accepted invite user {invited_user_email_address} to service {invite_data['service']}" + f"accepted invite user {invited_user_email_address} to service {invite_data['service_id']}" ) # We need to avoid taking a second trip through the login.gov code because we cannot pull the # access token twice. So once we retrieve these values, let's park them in redis for 15 minutes @@ -230,16 +230,19 @@ def set_up_your_profile(): activate_user(user["id"]) debug_msg("activated user") usr = User.from_id(user["id"]) + usr.add_to_service( - invite_data["service"], + invite_data["service_id"], invite_data["permissions"], invite_data["folder_permissions"], - invite_data["from_user"], + invite_data["from_user_id"], + ) + debug_msg( + f"Added user {usr.email_address} to service {invite_data['service_id']}" ) - debug_msg(f"Added user {usr.email_address} to service {invite_data['service']}") # notify-admin-1766 # redirect new users to templates area of new service instead of dashboard - service_id = invite_data["service"] + service_id = invite_data["service_id"] url = url_for(".service_dashboard", service_id=service_id) url = f"{url}/templates" return redirect(url) diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index 9184abbd1..f4a8538cd 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -62,7 +62,8 @@ class InviteApiClient(NotifyAdminAPIClient): resp_data = resp["data"] invite_data_key = f"invitedata-{unquote(state)}" - redis_invite_data = json.dumps(resp_data) + redis_invite_data = resp["invite"] + redis_invite_data = json.dumps(redis_invite_data) redis_client.set(invite_data_key, redis_invite_data) return resp_data diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index 01a3a78c9..3514a0d6e 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -168,7 +168,7 @@ class UserApiClient(NotifyAdminAPIClient): @cache.delete("user-{user_id}") def add_user_to_service(self, service_id, user_id, permissions, folder_permissions): # permissions passed in are the combined UI permissions, not DB permissions - endpoint = "/service/{}/users/{}".format(service_id, user_id) + endpoint = f"/service/{service_id}/users/{user_id}" data = { "permissions": [ {"permission": x} From 6e3a569a0a48e9ff062008197958dbacf5055ab2 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 7 Nov 2024 09:56:18 -0500 Subject: [PATCH 11/13] Resend invites works. Signed-off-by: Cliff Hill --- app/notify_client/invite_api_client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index f4a8538cd..39cf7dbce 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -115,10 +115,15 @@ class InviteApiClient(NotifyAdminAPIClient): "nonce": nonce, "state": state, } - self.post( + resp = self.post( url=f"/service/{service_id}/invite/{invited_user_id}/resend", data=data ) + invite_data_key = f"invitedata-{unquote(state)}" + redis_invite_data = resp["invite"] + redis_invite_data = json.dumps(redis_invite_data) + redis_client.set(invite_data_key, redis_invite_data) + @cache.delete("service-{service_id}") @cache.delete("user-{invited_user_id}") def accept_invite(self, service_id, invited_user_id): From 923a345d19442c157b0e22a4092f25dcccf7c712 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 7 Nov 2024 11:56:03 -0500 Subject: [PATCH 12/13] Fixed tests. Signed-off-by: Cliff Hill --- app/main/views/invites.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main/views/invites.py b/app/main/views/invites.py index a4c936456..07f6b3ac8 100644 --- a/app/main/views/invites.py +++ b/app/main/views/invites.py @@ -38,9 +38,9 @@ def accept_invite(token): ) if invited_user.status == "accepted": session.pop("invited_user_id", None) - service = Service.from_id(invited_user.service_id) + service = Service.from_id(invited_user.service) return redirect( - url_for("main.service_dashboard", service_id=invited_user.service_id) + url_for("main.service_dashboard", service_id=invited_user.service) ) session["invited_user_id"] = invited_user.id From e9b515d4ccce9dc54aa61a6dc6307420f12e6a76 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 7 Nov 2024 12:01:06 -0500 Subject: [PATCH 13/13] Another test fixed. Signed-off-by: Cliff Hill --- tests/app/notify_client/test_invite_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/app/notify_client/test_invite_client.py b/tests/app/notify_client/test_invite_client.py index 4cf21c9e1..2a6f5113d 100644 --- a/tests/app/notify_client/test_invite_client.py +++ b/tests/app/notify_client/test_invite_client.py @@ -30,7 +30,8 @@ def test_client_creates_invite( "nonce", "state", } - ) + ), + "invite": {}, }, )