diff --git a/api/magic_links/routes.py b/api/magic_links/routes.py index 68f27aae..753a47c9 100644 --- a/api/magic_links/routes.py +++ b/api/magic_links/routes.py @@ -1,6 +1,7 @@ import json -import urllib.parse from datetime import datetime +from urllib.parse import urlencode +from urllib.parse import urljoin from api.session.auth_session import AuthSessionView from config import Config @@ -87,8 +88,8 @@ def use(self, link_id: str): "round": round_short_name, } query_params = {k: v for k, v in query_params.items() if v is not None} - query_string = urllib.parse.urlencode(query_params) - frontend_account_url = f"{Config.MAGIC_LINK_REDIRECT_URL}?{query_string}" + query_string = urlencode(query_params) + frontend_account_url = urljoin(Config.APPLICANT_FRONTEND_HOST, f"account?{query_string}") current_app.logger.warning( f"The magic link with hash: '{link_hash}' has already been" f" used but the user with account_id: '{g.account_id}' is" diff --git a/api/sso/routes.py b/api/sso/routes.py index f2dfc832..e1820213 100644 --- a/api/sso/routes.py +++ b/api/sso/routes.py @@ -1,5 +1,6 @@ import warnings from urllib.parse import urlencode +from urllib.parse import urljoin from urllib.parse import urlparse import msal @@ -115,7 +116,9 @@ def get_token(self): roles=session["user"].get("roles") or [], ) - redirect_url = Config.ASSESSMENT_POST_LOGIN_URL # TODO: Remove defaulting to Assessment, instead use return_app + redirect_url = urljoin( # TODO: Remove defaulting to Assessment, instead use return_app + Config.ASSESSMENT_FRONTEND_HOST, "/assess/fund_dashboard" + ) if return_app := session.get("return_app"): if safe_app := Config.SAFE_RETURN_APPS.get(return_app): if return_path := session.get("return_path"): diff --git a/app.py b/app.py index e90b471a..463877f0 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,8 @@ from pathlib import Path from typing import Any from typing import Dict +from urllib.parse import urlencode +from urllib.parse import urljoin import connexion import prance @@ -114,17 +116,15 @@ def before_request_modifier(): # This is silently used by flask in the background. @flask_app.context_processor def inject_global_constants(): + query_params = urlencode({"fund": request.args.get("fund", ""), "round": request.args.get("round", "")}) return dict( stage="beta", service_meta_author="Department for Levelling up Housing and Communities", - accessibility_statement_url=Config.APPLICANT_FRONTEND_ACCESSIBILITY_STATEMENT_URL, # noqa - cookie_policy_url=Config.APPLICANT_FRONTEND_COOKIE_POLICY_URL, - contact_us_url=Config.APPLICANT_FRONTEND_CONTACT_US_URL - + f"?fund={request.args.get('fund', '')}&round={request.args.get('round', '')}", - privacy_url=Config.APPLICANT_FRONTEND_PRIVACY_URL - + f"?fund={request.args.get('fund', '')}&round={request.args.get('round', '')}", - feedback_url=Config.APPLICANT_FRONTEND_FEEDBACK_URL - + f"?fund={request.args.get('fund', '')}&round={request.args.get('round', '')}", + accessibility_statement_url=urljoin(Config.APPLICANT_FRONTEND_HOST, "/accessibility_statement"), # noqa + cookie_policy_url=urljoin(Config.APPLICANT_FRONTEND_HOST, "/cookie_policy"), + contact_us_url=urljoin(Config.APPLICANT_FRONTEND_HOST, f"/contact_us?{query_params}"), + privacy_url=urljoin(Config.APPLICANT_FRONTEND_HOST, f"/privacy?{query_params}"), + feedback_url=urljoin(Config.APPLICANT_FRONTEND_HOST, f"/feedback?{query_params}"), ) @flask_app.context_processor diff --git a/config/envs/default.py b/config/envs/default.py index 3268b332..c4ef189d 100644 --- a/config/envs/default.py +++ b/config/envs/default.py @@ -5,6 +5,7 @@ from os import environ from os import getenv from pathlib import Path +from urllib.parse import urljoin import redis from distutils.util import strtobool @@ -40,8 +41,6 @@ class DefaultConfig(object): # Hostname for this service AUTHENTICATOR_HOST = environ.get("AUTHENTICATOR_HOST", "") NEW_LINK_ENDPOINT = "/service/magic-links/new" - SSO_LOGOUT_ENDPOINT = "api_sso_routes_SsoView_logout_get" - SSO_LOGIN_ENDPOINT = "api_sso_routes_SsoView_login" SSO_POST_SIGN_OUT_URL = AUTHENTICATOR_HOST + "/service/sso/signed-out/signout-request" AUTO_REDIRECT_LOGIN = False @@ -61,13 +60,9 @@ class DefaultConfig(object): "https://login.microsoftonline.com/" + AZURE_AD_TENANT_ID ) - AZURE_AD_REDIRECT_PATH = ( - # Used for forming an absolute URL to your redirect URI. - "/sso/get-token" - ) # The absolute URL must match the redirect URI you set # in the app's registration in the Azure portal. - AZURE_AD_REDIRECT_URI = AUTHENTICATOR_HOST + AZURE_AD_REDIRECT_PATH + AZURE_AD_REDIRECT_URI = urljoin(AUTHENTICATOR_HOST, "/sso/get-token") # You can find the proper permission names from this document # https://docs.microsoft.com/en-us/graph/permissions-reference @@ -100,24 +95,15 @@ class DefaultConfig(object): """ # Account Store ACCOUNT_STORE_API_HOST = environ.get("ACCOUNT_STORE_API_HOST") - ACCOUNTS_ENDPOINT = "/accounts" - ACCOUNT_ENDPOINT = "/accounts/{account_id}" # Notification Service DISABLE_NOTIFICATION_SERVICE = False # Applicant Frontend APPLICANT_FRONTEND_HOST = environ.get("APPLICANT_FRONTEND_HOST", "frontend") - APPLICANT_FRONTEND_ACCESSIBILITY_STATEMENT_URL = APPLICANT_FRONTEND_HOST + "/accessibility_statement" - APPLICANT_FRONTEND_COOKIE_POLICY_URL = APPLICANT_FRONTEND_HOST + "/cookie_policy" - APPLICANT_FRONTEND_CONTACT_US_URL = APPLICANT_FRONTEND_HOST + "/contact_us" - APPLICANT_FRONTEND_PRIVACY_URL = APPLICANT_FRONTEND_HOST + "/privacy" - APPLICANT_FRONTEND_FEEDBACK_URL = APPLICANT_FRONTEND_HOST + "/feedback" - APPLICATION_ALL_QUESTIONS_URL = APPLICANT_FRONTEND_HOST + "/all_questions/{fund_short_name}/{round_short_name}" # Assessment Frontend ASSESSMENT_FRONTEND_HOST = environ.get("ASSESSMENT_FRONTEND_HOST", "") - ASSESSMENT_POST_LOGIN_URL = ASSESSMENT_FRONTEND_HOST + "/assess/fund_dashboard" FSD_ASSESSMENT_SESSION_TIMEOUT_SECONDS = CommonConfig.FSD_SESSION_TIMEOUT_SECONDS # Fund store service @@ -128,21 +114,19 @@ class DefaultConfig(object): # Post-award frontend POST_AWARD_FRONTEND_HOST = environ.get("POST_AWARD_FRONTEND_HOST", "") - POST_AWARD_FRONTEND_LOGIN_URL = POST_AWARD_FRONTEND_HOST + "/" # Post-award submit POST_AWARD_SUBMIT_HOST = environ.get("POST_AWARD_SUBMIT_HOST", "") - POST_AWARD_SUBMIT_LOGIN_URL = POST_AWARD_SUBMIT_HOST + "/" # Safe list of return applications SAFE_RETURN_APPS = { SupportedApp.POST_AWARD_FRONTEND.value: SafeAppConfig( - login_url=POST_AWARD_FRONTEND_LOGIN_URL, + login_url=urljoin(POST_AWARD_FRONTEND_HOST, "/"), logout_endpoint="sso_bp.signed_out", service_title="Find monitoring and evaluation data", ), SupportedApp.POST_AWARD_SUBMIT.value: SafeAppConfig( - login_url=POST_AWARD_SUBMIT_LOGIN_URL, + login_url=urljoin(POST_AWARD_SUBMIT_HOST, "/"), logout_endpoint="sso_bp.signed_out", service_title="Submit monitoring and evaluation data", ), @@ -153,10 +137,6 @@ class DefaultConfig(object): """ MAGIC_LINK_EXPIRY_DAYS = 1 MAGIC_LINK_EXPIRY_SECONDS = 86400 * MAGIC_LINK_EXPIRY_DAYS - if APPLICANT_FRONTEND_HOST: - MAGIC_LINK_REDIRECT_URL = APPLICANT_FRONTEND_HOST + "/account" - else: - MAGIC_LINK_REDIRECT_URL = "https://www.gov.uk/error" MAGIC_LINK_RECORD_PREFIX = "link" MAGIC_LINK_USER_PREFIX = "account" MAGIC_LINK_LANDING_PAGE = "/service/magic-links/landing/" diff --git a/config/envs/development.py b/config/envs/development.py index 515b41bb..c2d2f0ac 100644 --- a/config/envs/development.py +++ b/config/envs/development.py @@ -20,18 +20,8 @@ class DevelopmentConfig(Config): # Hostname for this service AUTHENTICATOR_HOST = getenv("AUTHENTICATOR_HOST", "http://localhost:5000") - SSO_POST_SIGN_OUT_URL = AUTHENTICATOR_HOST + "/service/sso/signed-out/signout-request" # Azure Active Directory Config - AZURE_AD_AUTHORITY = ( - # consumers|organizations| - signifies the Azure AD - # tenant endpoint - "https://login.microsoftonline.com/organizations" - ) - - # The absolute URL must match the redirect URI you set - # in the app's registration in the Azure portal. - AZURE_AD_REDIRECT_URI = AUTHENTICATOR_HOST + Config.AZURE_AD_REDIRECT_PATH # Session Settings SESSION_TYPE = ( diff --git a/config/envs/unit_test.py b/config/envs/unit_test.py index 2ea454cd..f8d5769f 100644 --- a/config/envs/unit_test.py +++ b/config/envs/unit_test.py @@ -33,10 +33,6 @@ class UnitTestConfig(Config): + AZURE_AD_TENANT_ID ) - # The absolute URL must match the redirect URI you set - # in the app's registration in the Azure portal. - AZURE_AD_REDIRECT_URI = AUTHENTICATOR_HOST + Config.AZURE_AD_REDIRECT_PATH - SESSION_TYPE = ( # Specifies how the token cache should be stored # in server-side session @@ -66,14 +62,6 @@ class UnitTestConfig(Config): Config.TALISMAN_SETTINGS["force_https"] = False WTF_CSRF_ENABLED = False - APPLICANT_FRONTEND_HOST = "frontend" - APPLICANT_FRONTEND_ACCESSIBILITY_STATEMENT_URL = "/accessibility_statement" - APPLICANT_FRONTEND_COOKIE_POLICY_URL = "/cookie_policy" - - # Assessment Frontend - ASSESSMENT_FRONTEND_HOST = "" - ASSESSMENT_POST_LOGIN_URL = "" - # --------------- # S3 Config # --------------- diff --git a/frontend/default/routes.py b/frontend/default/routes.py index f1a2241d..080485b8 100644 --- a/frontend/default/routes.py +++ b/frontend/default/routes.py @@ -32,7 +32,8 @@ def internal_server_error(error): return ( render_template( "500.html", - contact_us_url=Config.APPLICANT_FRONTEND_CONTACT_US_URL + contact_us_url=Config.APPLICANT_FRONTEND_HOST + + "/contact_us" + f"?fund={request.args.get('fund', '')}&round={request.args.get('round', '')}", ), 500, diff --git a/frontend/magic_links/routes.py b/frontend/magic_links/routes.py index 60f2dabf..f4e3d16c 100644 --- a/frontend/magic_links/routes.py +++ b/frontend/magic_links/routes.py @@ -84,10 +84,7 @@ def landing(link_id): app_guidance = None if round_data.application_guidance: app_guidance = round_data.application_guidance.format( - all_questions_url=Config.APPLICATION_ALL_QUESTIONS_URL.format( - fund_short_name=fund_short_name, - round_short_name=round_short_name, - ) + all_questions_url=f"{Config.APPLICANT_FRONTEND_HOST}/all_questions/{fund_short_name}/{round_short_name}" ) round_prospectus = round_data.prospectus if round_data.prospectus else None return render_template( diff --git a/frontend/sso/routes.py b/frontend/sso/routes.py index e27e090c..606c7ec8 100644 --- a/frontend/sso/routes.py +++ b/frontend/sso/routes.py @@ -1,4 +1,3 @@ -from config import Config from flask import Blueprint from flask import render_template from flask import request @@ -20,7 +19,7 @@ def signed_out(status): render_template( "sso_signed_out.html", status=status, - login_url=url_for(Config.SSO_LOGIN_ENDPOINT, return_app=return_app, return_path=return_path), + login_url=url_for("api_sso_routes_SsoView_login", return_app=return_app, return_path=return_path), ), 200, ) diff --git a/frontend/user/routes.py b/frontend/user/routes.py index 917f07b9..69eaa03c 100644 --- a/frontend/user/routes.py +++ b/frontend/user/routes.py @@ -41,8 +41,8 @@ def user(): "user.html", roles_required=roles_required, logged_in_user=logged_in_user, - login_url=url_for(Config.SSO_LOGIN_ENDPOINT), - logout_url=url_for(Config.SSO_LOGOUT_ENDPOINT), + login_url=url_for("api_sso_routes_SsoView_login"), + logout_url=url_for("api_sso_routes_SsoView_logout_get"), support_mailbox=Config.SUPPORT_MAILBOX_EMAIL, ), status_code, diff --git a/models/account.py b/models/account.py index e749c1ff..481af617 100644 --- a/models/account.py +++ b/models/account.py @@ -70,7 +70,7 @@ def get_account(email: str = None, account_id: str = None, azure_ad_subject_id=N if email is account_id is azure_ad_subject_id is None: raise TypeError("Requires an email address, azure_ad_subject_id or account_id") - url = Config.ACCOUNT_STORE_API_HOST + Config.ACCOUNTS_ENDPOINT + url = Config.ACCOUNT_STORE_API_HOST + "/accounts" params = { "email_address": email, "azure_ad_subject_id": azure_ad_subject_id, @@ -103,7 +103,7 @@ def update_account( Returns: Account object or None """ - url = Config.ACCOUNT_STORE_API_HOST + Config.ACCOUNT_ENDPOINT.format(account_id=id) + url = Config.ACCOUNT_STORE_API_HOST + "/accounts/{account_id}".format(account_id=id) if config.FLASK_ENV == "development" and not roles: account = get_account_data(email) @@ -131,7 +131,7 @@ def create_account(email: str) -> Account | None: Returns: Account object or None """ - url = Config.ACCOUNT_STORE_API_HOST + Config.ACCOUNTS_ENDPOINT + url = Config.ACCOUNT_STORE_API_HOST + "/accounts" params = {"email_address": email} response = post_data(url, params) diff --git a/models/data.py b/models/data.py index c22ff28b..16753265 100644 --- a/models/data.py +++ b/models/data.py @@ -127,7 +127,7 @@ def get_round_data_fail_gracefully(fund_id, round_id, use_short_name=False): def get_account_data(email: str): - url = Config.ACCOUNT_STORE_API_HOST + Config.ACCOUNTS_ENDPOINT + url = Config.ACCOUNT_STORE_API_HOST + "/accounts" params = { "email_address": email, } diff --git a/models/magic_link.py b/models/magic_link.py index 4f023a4f..e7e89aa1 100644 --- a/models/magic_link.py +++ b/models/magic_link.py @@ -8,6 +8,7 @@ from datetime import timedelta from typing import List from typing import TYPE_CHECKING +from urllib.parse import urljoin from config import Config from flask import abort @@ -191,7 +192,9 @@ def create_magic_link( self.clear_existing_user_record(account.id) if not redirect_url: - redirect_url = Config.MAGIC_LINK_REDIRECT_URL + "?fund=" + fund_short_name + "&round=" + round_short_name + redirect_url = urljoin( + Config.APPLICANT_FRONTEND_HOST, f"/account?fund={fund_short_name}&round={round_short_name}" + ) new_link_json = self._make_link_json(account, redirect_url) diff --git a/tests/test_signout.py b/tests/test_signout.py index 0ccb1d59..97fdb131 100644 --- a/tests/test_signout.py +++ b/tests/test_signout.py @@ -182,7 +182,7 @@ def test_session_sign_out_using_correct_route_with_specified_return_app(self, fl :param flask_test_client: """ mocker.patch( - "frontend.sso.routes.Config.SAFE_RETURN_APPS", + "config.Config.SAFE_RETURN_APPS", new_callable=PropertyMock, return_value={ "test-app": SafeAppConfig( @@ -229,7 +229,7 @@ def test_sign_out_template_service_title_is_dynamic(self, flask_test_client, moc :param flask_test_client: """ mocker.patch( - "frontend.sso.routes.Config.SAFE_RETURN_APPS", + "config.Config.SAFE_RETURN_APPS", new_callable=PropertyMock, return_value={ "test-app": SafeAppConfig( diff --git a/tests/test_signout_get.py b/tests/test_signout_get.py index ed303007..7a0d3fa8 100644 --- a/tests/test_signout_get.py +++ b/tests/test_signout_get.py @@ -105,7 +105,7 @@ def test_session_sign_out_using_correct_route_with_specified_return_app(self, fl :param flask_test_client: """ mocker.patch( - "frontend.sso.routes.Config.SAFE_RETURN_APPS", + "config.Config.SAFE_RETURN_APPS", new_callable=PropertyMock, return_value={ "test-app": SafeAppConfig(