From e80029e5f0ba9cc6429191b823fb455fbc6de55d Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 25 Oct 2024 16:29:34 -0400 Subject: [PATCH 1/8] Properly handling and validating the state for login.gov Signed-off-by: Cliff Hill --- app/service_invite/rest.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index e7d0d4b20..26718a35f 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -32,7 +32,7 @@ service_invite = Blueprint("service_invite", __name__) register_errors(service_invite) -def _create_service_invite(invited_user, nonce): +def _create_service_invite(invited_user, nonce, state): template_id = current_app.config["INVITATION_EMAIL_TEMPLATE_ID"] @@ -58,7 +58,7 @@ def _create_service_invite(invited_user, nonce): user_data_url_safe = get_user_data_url_safe(data) - url = url.replace("STATE", user_data_url_safe) + url = url.replace("STATE", state) personalisation = { "user_name": invited_user.from_user.name, @@ -94,11 +94,16 @@ def create_invited_user(service_id): except KeyError: current_app.logger.exception("nonce not found in submitted data.") raise + try: + state = request_json.pop("state") + except KeyError: + current_app.logger.exception("state not found in submitted data.") + raise invited_user = invited_user_schema.load(request_json) save_invited_user(invited_user) - _create_service_invite(invited_user, nonce) + _create_service_invite(invited_user, nonce, state) return jsonify(data=invited_user_schema.dump(invited_user)), 201 From c800fbc2b02a753250298431c0022d9278d675dd Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 25 Oct 2024 16:33:24 -0400 Subject: [PATCH 2/8] Removing unused code. Signed-off-by: Cliff Hill --- app/service_invite/rest.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index 26718a35f..bd600ec88 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -56,8 +56,6 @@ def _create_service_invite(invited_user, nonce, state): url = url.replace("NONCE", nonce) # handed from data sent from admin. - user_data_url_safe = get_user_data_url_safe(data) - url = url.replace("STATE", state) personalisation = { @@ -216,9 +214,3 @@ def validate_service_invitation_token(token): invited_user = get_invited_user_by_id(invited_user_id) return jsonify(data=invited_user_schema.dump(invited_user)), 200 - - -def get_user_data_url_safe(data): - data = json.dumps(data) - data = base64.b64encode(data.encode("utf8")) - return data.decode("utf8") From 123aa7129be3be7badd274a4a16ed932ac41dff7 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 30 Oct 2024 09:07:08 -0400 Subject: [PATCH 3/8] Making state be validated. Signed-off-by: Cliff Hill --- app/service_invite/rest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index bd600ec88..cc22201b5 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -1,4 +1,3 @@ -import base64 import json import os From 017406cbb6f57f271e13fb828aa76d13e9217001 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 1 Nov 2024 12:12:30 -0400 Subject: [PATCH 4/8] Fixing tests, and resend invites endpoint. Signed-off-by: Cliff Hill --- app/service_invite/rest.py | 5 ++++- tests/app/service_invite/test_service_invite_rest.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index cc22201b5..21ee15ff7 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -155,6 +155,9 @@ def resend_service_invite(service_id, invited_user_id): invited_user_id=invited_user_id, ) + nonce = request.json["nonce"] + state = request.json["state"] + fetched.created_at = utc_now() fetched.status = InvitedUserStatus.PENDING @@ -163,7 +166,7 @@ def resend_service_invite(service_id, invited_user_id): save_invited_user(update_dict) - _create_service_invite(fetched, current_app.config["ADMIN_BASE_URL"]) + _create_service_invite(fetched, nonce, state) return jsonify(data=invited_user_schema.dump(fetched)), 200 diff --git a/tests/app/service_invite/test_service_invite_rest.py b/tests/app/service_invite/test_service_invite_rest.py index 5cea786f5..61b8b79e7 100644 --- a/tests/app/service_invite/test_service_invite_rest.py +++ b/tests/app/service_invite/test_service_invite_rest.py @@ -46,6 +46,7 @@ def test_create_invited_user( auth_type=AuthType.EMAIL, folder_permissions=["folder_1", "folder_2", "folder_3"], nonce="FakeNonce", + state="FakeState", **extra_args, ) @@ -110,6 +111,7 @@ def test_create_invited_user_without_auth_type( "permissions": "send_messages,manage_service,manage_api_keys", "folder_permissions": [], "nonce": "FakeNonce", + "state": "FakeState", } json_resp = admin_request.post( @@ -134,6 +136,7 @@ def test_create_invited_user_invalid_email(client, sample_service, mocker, fake_ "permissions": "send_messages,manage_service,manage_api_keys", "folder_permissions": [fake_uuid, fake_uuid], "nonce": "FakeNonce", + "state": "FakeState", } data = json.dumps(data) @@ -235,6 +238,7 @@ def test_resend_expired_invite( response = client.post( url, headers=[("Content-Type", "application/json"), auth_header], + data='{"nonce": "FakeNonce", "state": "FakeState"}', ) assert response.status_code == 200 From f95738a7634c8069c05645abb5cb7c7e858f2d96 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 6 Nov 2024 14:30:57 -0500 Subject: [PATCH 5/8] Getting all the needed data in place. Signed-off-by: Cliff Hill --- app/service_invite/rest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index 21ee15ff7..aa14e28ff 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -1,5 +1,7 @@ +import base64 import json import os +from urllib.parse import unquote from flask import Blueprint, current_app, jsonify, request from itsdangerous import BadData, SignatureExpired @@ -51,6 +53,9 @@ def _create_service_invite(invited_user, nonce, state): data["invited_user_id"] = str(invited_user.id) data["invited_user_email"] = invited_user.email_address + invite_redis_key = f"invite-data-{unquote(state)}" + redis_store.set(invite_redis_key, get_user_data_url_safe(data)) + url = os.environ["LOGIN_DOT_GOV_REGISTRATION_URL"] url = url.replace("NONCE", nonce) # handed from data sent from admin. @@ -216,3 +221,9 @@ def validate_service_invitation_token(token): invited_user = get_invited_user_by_id(invited_user_id) return jsonify(data=invited_user_schema.dump(invited_user)), 200 + + +def get_user_data_url_safe(data): + data = json.dumps(data) + data = base64.b64encode(data.encode("utf8")) + return data.decode("utf8") From 958c3cd61ee5a290fcfd1d235beeb4e5e5a4bf82 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 6 Nov 2024 16:03:50 -0500 Subject: [PATCH 6/8] Trying to get invites to flow correctly. Signed-off-by: Cliff Hill --- app/dao/users_dao.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/dao/users_dao.py b/app/dao/users_dao.py index 690ecc7f9..bf0f21592 100644 --- a/app/dao/users_dao.py +++ b/app/dao/users_dao.py @@ -52,15 +52,22 @@ def get_login_gov_user(login_uuid, email_address): current_app.logger.exception("Error getting login.gov user") db.session.rollback() + print("In here instead!") return user # Remove this 1 July 2025, all users should have login.gov uuids by now stmt = select(User).filter(User.email_address.ilike(email_address)) user = db.session.execute(stmt).scalars().first() + print("*" * 80) + print(user) + if user: + print(f"login_uuid: {login_uuid}") save_user_attribute(user, {"login_uuid": login_uuid}) return user + print("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% WTF") + return None From 70404a2a8b62235601e20f354ae40e670473ce01 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 7 Nov 2024 08:37:55 -0500 Subject: [PATCH 7/8] Invites are now working. Signed-off-by: Cliff Hill --- app/dao/users_dao.py | 7 ------- app/service/rest.py | 2 +- app/service_invite/rest.py | 6 ++++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/dao/users_dao.py b/app/dao/users_dao.py index bf0f21592..690ecc7f9 100644 --- a/app/dao/users_dao.py +++ b/app/dao/users_dao.py @@ -52,22 +52,15 @@ def get_login_gov_user(login_uuid, email_address): current_app.logger.exception("Error getting login.gov user") db.session.rollback() - print("In here instead!") return user # Remove this 1 July 2025, all users should have login.gov uuids by now stmt = select(User).filter(User.email_address.ilike(email_address)) user = db.session.execute(stmt).scalars().first() - print("*" * 80) - print(user) - if user: - print(f"login_uuid: {login_uuid}") save_user_attribute(user, {"login_uuid": login_uuid}) return user - print("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% WTF") - return None diff --git a/app/service/rest.py b/app/service/rest.py index 6441b74b7..7dd614058 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -373,7 +373,7 @@ def add_user_to_service(service_id, user_id): service = dao_fetch_service_by_id(service_id) user = get_user_by_id(user_id=user_id) if user in service.users: - error = "User id: {} already part of service id: {}".format(user_id, service_id) + error = f"User id: {user_id} already part of service id: {service_id}" raise InvalidRequest(error, status_code=400) data = request.get_json() diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index aa14e28ff..1b25fe92c 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -87,6 +87,8 @@ def _create_service_invite(invited_user, nonce, state): ) send_notification_to_queue(saved_notification, queue=QueueNames.NOTIFY) + return data + @service_invite.route("/service//invite", methods=["POST"]) def create_invited_user(service_id): @@ -105,9 +107,9 @@ def create_invited_user(service_id): invited_user = invited_user_schema.load(request_json) save_invited_user(invited_user) - _create_service_invite(invited_user, nonce, state) + invite_data = _create_service_invite(invited_user, nonce, state) - return jsonify(data=invited_user_schema.dump(invited_user)), 201 + return jsonify(data=invited_user_schema.dump(invited_user), invite=invite_data), 201 @service_invite.route("/service//invite/expired", methods=["GET"]) From 15711430124ec39f6819e859cb9df58d4b066b79 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 7 Nov 2024 09:56:09 -0500 Subject: [PATCH 8/8] Resend invites works. Signed-off-by: Cliff Hill --- app/service_invite/rest.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index 1b25fe92c..38bc1c404 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -157,14 +157,23 @@ def resend_service_invite(service_id, invited_user_id): Note: This ignores the POST data entirely. """ + request_json = request.get_json() + try: + nonce = request_json.pop("nonce") + except KeyError: + current_app.logger.exception("nonce not found in submitted data.") + raise + try: + state = request_json.pop("state") + except KeyError: + current_app.logger.exception("state not found in submitted data.") + raise + fetched = get_expired_invite_by_service_and_id( service_id=service_id, invited_user_id=invited_user_id, ) - nonce = request.json["nonce"] - state = request.json["state"] - fetched.created_at = utc_now() fetched.status = InvitedUserStatus.PENDING @@ -173,9 +182,9 @@ def resend_service_invite(service_id, invited_user_id): save_invited_user(update_dict) - _create_service_invite(fetched, nonce, state) + invite_data = _create_service_invite(fetched, nonce, state) - return jsonify(data=invited_user_schema.dump(fetched)), 200 + return jsonify(data=invited_user_schema.dump(fetched), invite=invite_data), 200 def invited_user_url(invited_user_id, invite_link_host=None):