diff --git a/src/eduid/webapp/jsconfig/settings/jsapps.py b/src/eduid/webapp/jsconfig/settings/jsapps.py index d0d3ca89b..fd378a013 100644 --- a/src/eduid/webapp/jsconfig/settings/jsapps.py +++ b/src/eduid/webapp/jsconfig/settings/jsapps.py @@ -44,49 +44,61 @@ class JsAppsConfig(PasswordConfigMixin): This is sent to the client, so care must be taken to avoid setting secrets here. """ - debug: bool = False - environment: EduidEnvironment = EduidEnvironment.production - csrf_token: Optional[str] = None available_languages: dict[str, str] = Field(default={"en": "English", "sv": "Svenska"}) + csrf_token: Optional[str] = None + dashboard_link: HttpUrl + dashboard_url: Optional[str] # deprecated + debug: bool = False + eduid_site_link: HttpUrl = "https://eduid.se" eduid_site_name: str = "eduID" - eduid_site_url: str = "https://eduid.se" - dashboard_url: str - signup_url: str - reset_password_link: str # used for directing a user to the reset password app - static_faq_url: str + eduid_site_url: Optional[str] = "https://eduid.se" # deprecated + environment: EduidEnvironment = EduidEnvironment.production + faq_link: HttpUrl + reset_password_link: HttpUrl # used for directing a user to the reset password app sentry_dsn: Optional[str] = None + signup_link: HttpUrl + signup_url: Optional[str] # deprecated + static_faq_url: Optional[str] # deprecated # backend endpoint urls - authn_url: str - eidas_url: str - emails_url: str - group_mgmt_url: str - ladok_url: str - letter_proofing_url: str - login_base_url: AnyUrl - login_next_url: HttpUrl # Needs to be a full URL since the backend is on the idp, not on https://eduid.se - request_other_url: Optional[ + authn_service_url: HttpUrl + authn_url: Optional[str] # deprecated + eidas_service_url: HttpUrl + eidas_url: Optional[str] # deprecated + emails_service_url: HttpUrl + emails_url: Optional[str] # deprecated + error_info_url: Optional[ HttpUrl ] = None # Needs to be a full URL since the backend is on the idp, not on https://eduid.se - error_info_url: Optional[ + group_mgmt_service_url: HttpUrl + group_mgmt_url: Optional[str] # deprecated + ladok_service_url: HttpUrl + ladok_url: Optional[str] # deprecated + letter_proofing_service_url: HttpUrl + letter_proofing_url: Optional[str] # deprecated + login_base_url: Optional[AnyUrl] # deprecated + login_next_url: HttpUrl # Needs to be a full URL since the backend is on the idp, not on https://eduid.se + login_request_other_url: Optional[ HttpUrl ] = None # Needs to be a full URL since the backend is on the idp, not on https://eduid.se - lookup_mobile_proofing_url: str - mobile_url: Optional[str] = None # should be replaced by phone_url - oidc_proofing_freja_url: str - oidc_proofing_url: str - orcid_url: str - password_service_url: Optional[str] = None # should be replaced by reset_password_url - personal_data_url: str - phone_url: str - reset_passwd_url: Optional[str] = None # should be replaced by reset_password_url - reset_password_url: str - security_url: str - svipe_url: Optional[str] # if not set the frontend component will not show - token_service_url: Optional[str] = None # should be replaced by authn_url + login_service_url: HttpUrl + lookup_mobile_proofing_service_url: HttpUrl + lookup_mobile_proofing_url: Optional[str] # deprecated + orcid_service_url: HttpUrl + orcid_url: Optional[str] # deprecated + personal_data_service_url: HttpUrl + personal_data_url: Optional[str] # deprecated + phone_service_url: HttpUrl + phone_url: Optional[str] # deprecated + reset_password_service_url: HttpUrl + reset_password_url: Optional[str] # deprecated + security_service_url: HttpUrl + security_url: Optional[str] # deprecated + svipe_service_url: Optional[HttpUrl] # if not set the frontend component will not show + svipe_url: Optional[str] # deprecated # Dashboard config - proofing_methods: list = Field(default=["letter", "lookup_mobile", "oidc", "eidas"]) default_country_code: int = 46 - token_verify_idp: str + proofing_methods: list = Field(default=["letter", "lookup_mobile", "oidc", "eidas"]) + token_verify_idp: HttpUrl # Signup config - tous: Optional[dict[str, str]] = None recaptcha_public_key: Optional[str] = None + tous: Optional[dict[str, str]] = None diff --git a/src/eduid/webapp/jsconfig/tests/data/config.yaml b/src/eduid/webapp/jsconfig/tests/data/config.yaml index a74f7942d..d6d68f826 100644 --- a/src/eduid/webapp/jsconfig/tests/data/config.yaml +++ b/src/eduid/webapp/jsconfig/tests/data/config.yaml @@ -6,26 +6,46 @@ eduid: jsconfig: app_name: jsconfig jsapps: - password_entropy: 12 - password_length: 10 + authn_service_url: 'https://dashboard.example.com/services/authn' authn_url: 'authn_url' - dashboard_url: 'dashboard_url' + dashboard_link: 'https://example.com/dashboard' + dashboard_url: 'https://example.com/dashboard' + eidas_service_url: 'https://dashboard.example.com/services/eidas' eidas_url: 'eidas_url' + emails_service_url: 'https://dashboard.example.com/services/email' emails_url: 'emails_url' + error_info_url: 'https://idp.example.com/services/idp/error_info' + faq_link: 'https://example.com/faq' + group_mgmt_service_url: 'https://dashboard.example.com/services/group_mgmt' group_mgmt_url: 'group_mgmt_url' + ladok_service_url: 'https://dashboard.example.com/services/ladok' ladok_url: 'ladok_url' + letter_proofing_service_url: 'https://dashboard.example.com/services/letter' letter_proofing_url: 'letter_proofing_url' - login_base_url: 'http://eduid.docker/login' - login_next_url: 'http://eduid.docker/login/next' + login_base_url: 'https://example.com/login' + login_next_url: 'https://idp.example.com/servics/idp/next' + login_request_other_url: 'https://idp.example.com/services/idp/other' + login_service_url: 'https://idp.example.com/servics/idp' + lookup_mobile_proofing_service_url: 'https://dashboard.example.com/services/mobile-proofing' lookup_mobile_proofing_url: 'lookup_mobile_proofing_url' - oidc_proofing_freja_url: 'oidc_proofing_freja_url' - oidc_proofing_url: 'oidc_proofing_url' + orcid_service_url: 'https://dashboard.example.com/services/orcid' orcid_url: 'orcid_url' + password_entropy: 12 + password_length: 10 + personal_data_service_url: 'https://dashboard.example.com/services/pdata' personal_data_url: 'personal_data_url' + phone_service_url: 'https://dashboard.example.com/services/phone' phone_url: 'phone_url' + recaptcha_public_key: 'public_key' + reset_password_link: 'https://example.com/reset-password' + reset_password_service_url: 'https://idp.example.com/servics/reset-password' reset_password_url: 'reset_password_url' + security_service_url: 'https://idp.example.com/servics/security' security_url: 'security_url' + sentry_dsn: 'sentry_dsn' + signup_link: 'https://example.com/signup' signup_url: 'signup_url' static_faq_url: 'static_faq_url' - token_verify_idp: 'token_verify_idp' - reset_password_link: 'reset_password_link' + svipe_service_url: 'https://dashboard.example.com/services/svipe' + svipe_url: 'svipe_url' + token_verify_idp: 'https://some-other-idp.example.com' diff --git a/src/eduid/webapp/jsconfig/tests/test_app.py b/src/eduid/webapp/jsconfig/tests/test_app.py index 82ed4df8c..2431b57b2 100644 --- a/src/eduid/webapp/jsconfig/tests/test_app.py +++ b/src/eduid/webapp/jsconfig/tests/test_app.py @@ -36,9 +36,15 @@ from typing import Any, Mapping, cast from eduid.common.config.parsers import load_config +from eduid.common.testing_base import normalised_data from eduid.webapp.common.api.testing import CSRFTestClient, EduidAPITestCase from eduid.webapp.jsconfig.app import JSConfigApp, jsconfig_init_app from eduid.webapp.jsconfig.settings.common import JSConfigConfig +from eduid.webapp.jsconfig.settings.jsapps import JsAppsConfig + +DASHBOARD_LINK = "https://dashboard.example.com" +PERSONAL_DATA_SERVICE_URL = "https://example.com/personal_data_url" +FAQ_LINK = "https://example.com/static_faq_url" class JSConfigTests(EduidAPITestCase[JSConfigApp]): @@ -63,53 +69,76 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: "server_name": "example.com", "testing": True, "jsapps": { - "password_entropy": 12, - "password_length": 10, + "authn_service_url": "https://dashboard.example.com/services/authn", "authn_url": "authn_url", - "dashboard_url": "dashboard_url", + "dashboard_link": "https://example.com/dashboard", + "dashboard_url": "https://example.com/dashboard", + "eidas_service_url": "https://dashboard.example.com/services/eidas", "eidas_url": "eidas_url", + "emails_service_url": "https://dashboard.example.com/services/email", "emails_url": "emails_url", + "error_info_url": "https://idp.example.com/services/idp/error_info", + "faq_link": "https://example.com/faq", + "group_mgmt_service_url": "https://dashboard.example.com/services/group_mgmt", "group_mgmt_url": "group_mgmt_url", + "ladok_service_url": "https://dashboard.example.com/services/ladok", "ladok_url": "ladok_url", + "letter_proofing_service_url": "https://dashboard.example.com/services/letter", "letter_proofing_url": "letter_proofing_url", - "login_base_url": "http://eduid.docker/login", - "login_next_url": "http://eduid.docker/login/next", + "login_base_url": "https://example.com/login", + "login_next_url": "https://idp.example.com/servics/idp/next", + "login_request_other_url": "https://idp.example.com/services/idp/other", + "login_service_url": "https://idp.example.com/servics/idp", + "lookup_mobile_proofing_service_url": "https://dashboard.example.com/services/mobile-proofing", "lookup_mobile_proofing_url": "lookup_mobile_proofing_url", - "oidc_proofing_freja_url": "oidc_proofing_freja_url", - "oidc_proofing_url": "oidc_proofing_url", + "orcid_service_url": "https://dashboard.example.com/services/orcid", "orcid_url": "orcid_url", + "password_entropy": 12, + "password_length": 10, + "personal_data_service_url": "https://dashboard.example.com/services/pdata", "personal_data_url": "personal_data_url", + "phone_service_url": "https://dashboard.example.com/services/phone", "phone_url": "phone_url", + "recaptcha_public_key": "public_key", + "reset_password_link": "https://example.com/reset-password", + "reset_password_service_url": "https://idp.example.com/servics/reset-password", "reset_password_url": "reset_password_url", + "security_service_url": "https://idp.example.com/servics/security", "security_url": "security_url", + "sentry_dsn": "sentry_dsn", + "signup_link": "https://example.com/signup", "signup_url": "signup_url", "static_faq_url": "static_faq_url", - "token_verify_idp": "token_verify_idp", - "reset_password_link": "reset_password_link", + "svipe_service_url": "https://dashboard.example.com/services/svipe", + "svipe_url": "svipe_url", + "token_verify_idp": "https://some-other-idp.example.com", }, } ) return config + def _validate_jsconfig(self, config_data: dict[str, Any]) -> None: + assert config_data["type"] == "GET_JSCONFIG_CONFIG_SUCCESS" + assert config_data["payload"].pop("success") is True # success is added by _make_payload but probably shouldn't + assert config_data["payload"]["csrf_token"] is not None + + config_data["payload"]["csrf_token"] = None # csrf_token is None when config is first loaded + assert normalised_data(self.app.conf.jsapps.dict()) == normalised_data(config_data["payload"]) + + def test_get_config(self): + eppn = self.test_user_data["eduPersonPrincipalName"] + with self.session_cookie(self.browser, eppn) as client: + response = client.get("http://example.com/config") + self.assertEqual(response.status_code, 200) + self._validate_jsconfig(json.loads(response.data)) + def test_get_dashboard_config(self): eppn = self.test_user_data["eduPersonPrincipalName"] with self.session_cookie(self.browser, eppn, subdomain="dashboard") as client: - response = client.get("http://dashboard.example.com/config") + response = client.get("http://dashboard.example.com/dashboard/config") self.assertEqual(response.status_code, 200) - - config_data = json.loads(response.data) - - assert config_data["type"] == "GET_JSCONFIG_CONFIG_SUCCESS" - assert config_data["payload"]["dashboard_url"] == "dashboard_url" - assert config_data["payload"]["personal_data_url"] == "personal_data_url" - assert config_data["payload"]["static_faq_url"] == "static_faq_url" - assert config_data["payload"]["available_languages"] == [["en", "English"], ["sv", "Svenska"]] - - assert config_data["payload"]["DASHBOARD_URL"] == "dashboard_url" - assert config_data["payload"]["PERSONAL_DATA_URL"] == "personal_data_url" - assert config_data["payload"]["STATIC_FAQ_URL"] == "static_faq_url" - assert config_data["payload"]["AVAILABLE_LANGUAGES"] == [["en", "English"], ["sv", "Svenska"]] + self._validate_jsconfig(json.loads(response.data)) def test_get_signup_config(self): eppn = self.test_user_data["eduPersonPrincipalName"] @@ -117,16 +146,7 @@ def test_get_signup_config(self): response = client.get("http://signup.example.com/signup/config") self.assertEqual(response.status_code, 200) - - config_data = json.loads(response.data) - - assert config_data["type"] == "GET_JSCONFIG_SIGNUP_CONFIG_SUCCESS" - assert config_data["payload"]["dashboard_url"] == "dashboard_url" - assert config_data["payload"]["static_faq_url"] == "static_faq_url" - assert config_data["payload"]["available_languages"] == [["en", "English"], ["sv", "Svenska"]] - assert config_data["payload"]["DASHBOARD_URL"] == "dashboard_url" - assert config_data["payload"]["STATIC_FAQ_URL"] == "static_faq_url" - assert config_data["payload"]["AVAILABLE_LANGUAGES"] == [["en", "English"], ["sv", "Svenska"]] + self._validate_jsconfig(json.loads(response.data)) def test_get_login_config(self): eppn = self.test_user_data["eduPersonPrincipalName"] @@ -134,12 +154,15 @@ def test_get_login_config(self): response = client.get("http://login.example.com/login/config") self.assertEqual(response.status_code, 200) + self._validate_jsconfig(json.loads(response.data)) - config_data = json.loads(response.data) + def test_get_errors_config(self): + eppn = self.test_user_data["eduPersonPrincipalName"] + with self.session_cookie(self.browser, eppn) as client: + response = client.get("http://example.com/errors/config") - assert config_data["type"] == "GET_JSCONFIG_LOGIN_CONFIG_SUCCESS" - assert config_data["payload"]["password_entropy"] == 12 - assert config_data["payload"]["password_length"] == 10 + self.assertEqual(response.status_code, 200) + self._validate_jsconfig(json.loads(response.data)) def test_jsapps_config_from_yaml(self): os.environ["EDUID_CONFIG_YAML"] = f"{self.data_dir}/config.yaml" diff --git a/src/eduid/webapp/jsconfig/views.py b/src/eduid/webapp/jsconfig/views.py index c7782926e..a14d2a1a0 100644 --- a/src/eduid/webapp/jsconfig/views.py +++ b/src/eduid/webapp/jsconfig/views.py @@ -29,9 +29,9 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # -from typing import Any +from enum import Enum -from flask import Blueprint +from flask import Blueprint, abort from eduid.webapp.common.api.decorators import MarshalWith from eduid.webapp.common.api.messages import FluxData, success_response @@ -42,105 +42,27 @@ jsconfig_views = Blueprint("jsconfig", __name__, url_prefix="") -def _fix_available_languages(available_languages: dict[str, str]) -> list[list[str]]: - # TODO: our frontend code should accept available_languages as map instead of list of lists - return [[key, value] for key, value in available_languages.items()] - - -def _fix_uppercase_config(config: dict[str, Any]): - # XXX the front app consumes some settings as upper case and some as lower - # case. We'll provide them all in both upper and lower case, to - # facilitate migration of the front app to lower case. - config_upper = {} - for k, v in config.items(): - config_upper[k.upper()] = v - config.update(config_upper) - return config - - -# TODO: remove when /dashboard/config is used @jsconfig_views.route("/config", methods=["GET"]) @MarshalWith(FluxStandardAction) -def get_dashboard_config_old() -> FluxData: - return get_dashboard_config() - - -@jsconfig_views.route("/dashboard/config", methods=["GET"]) -@MarshalWith(FluxStandardAction) -def get_dashboard_config() -> FluxData: +def get_config() -> FluxData: """ Configuration for the dashboard front app """ - config_dict = current_app.conf.jsapps.dict() - config_dict["csrf_token"] = session.get_csrf_token() - - # Fixes for frontend - if current_app.conf.fix_dashboard_available_languages: - config_dict["available_languages"] = _fix_available_languages(current_app.conf.jsapps.available_languages) - if current_app.conf.fix_dashboard_uppercase_config: - config_dict = _fix_uppercase_config(config_dict) - - return success_response(payload=config_dict) - -@jsconfig_views.route("/signup/config", methods=["GET"]) -@MarshalWith(FluxStandardAction) -def get_signup_config() -> FluxData: - """ - Configuration for the signup front app - """ config_dict = current_app.conf.jsapps.dict() config_dict["csrf_token"] = session.get_csrf_token() - # Fixes for frontend - if current_app.conf.fix_signup_available_languages: - config_dict["available_languages"] = _fix_available_languages(current_app.conf.jsapps.available_languages) - if current_app.conf.fix_signup_uppercase_config: - config_dict = _fix_uppercase_config(config_dict) - return success_response(payload=config_dict) -@jsconfig_views.route("/login/config", methods=["GET"]) +@jsconfig_views.route("//config", methods=["GET"]) @MarshalWith(FluxStandardAction) -def get_login_config() -> FluxData: - """ - Configuration for the login front app +def get_config_for(frontend_app: str) -> FluxData: """ - payload = { - "base_url": current_app.conf.jsapps.login_base_url, # OLD - remove when frontend is updated - "login_base_url": current_app.conf.jsapps.login_base_url, # NEW - "csrf_token": session.get_csrf_token(), - "dashboard_url": current_app.conf.jsapps.dashboard_url, - "eduid_site_name": current_app.conf.jsapps.eduid_site_name, - "eduid_site_url": current_app.conf.jsapps.eduid_site_url, - "eidas_url": current_app.conf.jsapps.eidas_url, - "environment": current_app.conf.jsapps.environment.value, - "mfa_auth_idp": current_app.conf.jsapps.token_verify_idp, - "next_url": current_app.conf.jsapps.login_next_url, - "password_entropy": current_app.conf.jsapps.password_entropy, - "password_length": current_app.conf.jsapps.password_length, - "password_service_url": current_app.conf.jsapps.password_service_url, - "request_other_url": current_app.conf.jsapps.request_other_url, - "reset_password_url": current_app.conf.jsapps.reset_password_url, - "sentry_dsn": current_app.conf.jsapps.sentry_dsn, - "signup_url": current_app.conf.jsapps.signup_url, - } - return success_response(payload=payload) - - -@jsconfig_views.route("/errors/config", methods=["GET"]) -@MarshalWith(FluxStandardAction) -def get_errors_config() -> FluxData: - """ - Configuration for the errors front app + Configuration for the dashboard front app """ - payload = { - "csrf_token": session.get_csrf_token(), - "dashboard_url": current_app.conf.jsapps.dashboard_url, - "eduid_site_name": current_app.conf.jsapps.eduid_site_name, - "eduid_site_url": current_app.conf.jsapps.eduid_site_url, - "environment": current_app.conf.jsapps.environment.value, - "error_info_url": current_app.conf.jsapps.error_info_url, - } - return success_response(payload=payload) + if frontend_app in ["dashboard", "signup", "login", "errors"]: + current_app.logger.info(f"requesting config for frontend app {frontend_app}") + return get_config() + current_app.logger.error(f"{frontend_app} not in the list of supported apps") + abort(404)