From 55fd4c14d0467a9ef1689ce9c72775632cd89579 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Wed, 11 Sep 2024 07:18:21 +0200 Subject: [PATCH] fixup! pro_connect: Add a new SSO --- itou/openid_connect/pro_connect/constants.py | 2 +- itou/templates/dashboard/edit_user_info.html | 10 +- .../includes/description.html | 2 +- itou/www/dashboard/views.py | 5 +- tests/openid_connect/pro_connect/test.py | 4 +- tests/openid_connect/pro_connect/tests.py | 110 +++++++++--------- tests/www/dashboard/test_edit_user_info.py | 10 +- 7 files changed, 78 insertions(+), 65 deletions(-) diff --git a/itou/openid_connect/pro_connect/constants.py b/itou/openid_connect/pro_connect/constants.py index a5a3850d8b5..5cbedc5b650 100644 --- a/itou/openid_connect/pro_connect/constants.py +++ b/itou/openid_connect/pro_connect/constants.py @@ -14,7 +14,7 @@ PRO_CONNECT_ENDPOINT_USERINFO = f"{settings.PRO_CONNECT_BASE_URL}/userinfo" PRO_CONNECT_ENDPOINT_LOGOUT = f"{settings.PRO_CONNECT_BASE_URL}/session/end" -# These expiration times have been chosen arbitrarily. +# This timeout (in seconds) has been chosen arbitrarily. PRO_CONNECT_TIMEOUT = 60 PRO_CONNECT_SESSION_KEY = "pro_connect" diff --git a/itou/templates/dashboard/edit_user_info.html b/itou/templates/dashboard/edit_user_info.html index 5048af160f8..b01f85f9842 100644 --- a/itou/templates/dashboard/edit_user_info.html +++ b/itou/templates/dashboard/edit_user_info.html @@ -67,9 +67,13 @@

Informations personnelles

Adresse e-mail : {{ user.email }} -

- Ces informations doivent être modifiées sur votre fournisseur d'identité. Nous vous invitons à contacter le support ProConnect pour avoir plus d'aide." -

+ {% if user_is_ft %} +

Ces informations doivent être modifiées par France Travail et seront mises à jour à votre prochaine connexion.

+ {% else %} +

+ Vous pouvez modifier votre informations personnelles sur ProConnect. Elles seront mises à jour à votre prochaine connexion. +

+ {% endif %}
{% bootstrap_form_errors form type="all" %} {% elif ic_account_url %} diff --git a/itou/templates/inclusion_connect/includes/description.html b/itou/templates/inclusion_connect/includes/description.html index fc41eb54032..06ef222e19c 100644 --- a/itou/templates/inclusion_connect/includes/description.html +++ b/itou/templates/inclusion_connect/includes/description.html @@ -7,7 +7,7 @@ {% if pro_connect_url %}

Créez un compte les emplois de l’inclusion avec ProConnect

-

ProConnect est une solution de connexion unique à de nombres services publics.

+

ProConnect est une solution de connexion unique à de nombreux services publics.

{% else %}

Créez un compte les emplois de l’inclusion avec Inclusion Connect

diff --git a/itou/www/dashboard/views.py b/itou/www/dashboard/views.py index e8a464c68cb..09015b664d1 100644 --- a/itou/www/dashboard/views.py +++ b/itou/www/dashboard/views.py @@ -284,6 +284,7 @@ def edit_user_info(request, template_name="dashboard/edit_user_info.html"): "form": form, "prev_url": prev_url, "ic_account_url": ic_account_url, + "user_is_ft": request.user.email.endswith(global_constants.FRANCE_TRAVAIL_EMAIL_SUFFIX), } return render(request, template_name, context) @@ -403,7 +404,7 @@ def dispatch(self, request, *args, **kwargs): return HttpResponseRedirect(reverse("dashboard:index")) return super().dispatch(request, *args, **kwargs) - def _get_inclusion_connect_base_params(self): + def _get_params(self): params = { "user_kind": self.request.user.kind, "previous_url": self.request.get_full_path(), @@ -416,7 +417,7 @@ def _get_inclusion_connect_base_params(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - params = self._get_inclusion_connect_base_params() + params = self._get_params() inclusion_connect_url = add_url_params(reverse("inclusion_connect:activate_account"), params) pro_connect_url = ( add_url_params(reverse("pro_connect:authorize"), params) if settings.PRO_CONNECT_BASE_URL else None diff --git a/tests/openid_connect/pro_connect/test.py b/tests/openid_connect/pro_connect/test.py index f84a79df59b..8d8e9c1e901 100644 --- a/tests/openid_connect/pro_connect/test.py +++ b/tests/openid_connect/pro_connect/test.py @@ -8,8 +8,8 @@ TEST_SETTINGS = { "PRO_CONNECT_BASE_URL": "https://pro.connect.fake", - "PRO_CONNECT_CLIENT_ID": "IC_CLIENT_ID_123", - "PRO_CONNECT_CLIENT_SECRET": "IC_CLIENT_SECRET_123", + "PRO_CONNECT_CLIENT_ID": "PC_CLIENT_ID_123", + "PRO_CONNECT_CLIENT_SECRET": "PC_CLIENT_SECRET_123", "PRO_CONNECT_FT_IDP_HINT": "xxxxxx", } diff --git a/tests/openid_connect/pro_connect/tests.py b/tests/openid_connect/pro_connect/tests.py index d29a3adc3e0..7b99e83a377 100644 --- a/tests/openid_connect/pro_connect/tests.py +++ b/tests/openid_connect/pro_connect/tests.py @@ -165,16 +165,16 @@ def test_create_user_from_user_info(self): Similar to france_connect.tests.FranceConnectTest.test_create_django_user but with more tests. """ - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) - assert not User.objects.filter(username=ic_user_data.username).exists() - assert not User.objects.filter(email=ic_user_data.email).exists() + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + assert not User.objects.filter(username=pc_user_data.username).exists() + assert not User.objects.filter(email=pc_user_data.email).exists() now = timezone.now() # Because external_data_source_history is a JSONField # dates are actually stored as strings in the database now_str = json.loads(DjangoJSONEncoder().encode(now)) with mock.patch("django.utils.timezone.now", return_value=now): - user, created = ic_user_data.create_or_update_user() + user, created = pc_user_data.create_or_update_user() assert created assert user.email == OIDC_USERINFO["email"] assert user.last_name == OIDC_USERINFO["usual_name"] @@ -189,7 +189,7 @@ def test_create_user_from_user_info(self): "source": "PC", "created_at": now_str, } - for field in dataclasses.fields(ic_user_data) + for field in dataclasses.fields(pc_user_data) ] assert sorted(user.external_data_source_history, key=itemgetter("field_name")) == sorted( expected, key=itemgetter("field_name") @@ -200,13 +200,13 @@ def test_create_user_from_user_info_with_already_existing_id(self): If there already is an existing user with this ProConnect id, we do not create it again, we use it and we update it. """ - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) PrescriberFactory( - username=ic_user_data.username, + username=pc_user_data.username, last_name="will_be_forgotten", identity_provider=users_enums.IdentityProvider.PRO_CONNECT, ) - user, created = ic_user_data.create_or_update_user() + user, created = pc_user_data.create_or_update_user() assert not created assert user.last_name == OIDC_USERINFO["usual_name"] assert user.first_name == OIDC_USERINFO["given_name"] @@ -217,36 +217,36 @@ def test_create_user_from_user_info_with_already_existing_id_but_from_other_sso( If there already is an existing user with this ProConnect id, but it comes from another SSO. The email is also different, so it will crash while trying to create a new user. """ - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) PrescriberFactory( - username=ic_user_data.username, + username=pc_user_data.username, last_name="will_be_forgotten", identity_provider=users_enums.IdentityProvider.DJANGO, email="random@email.com", ) with pytest.raises(ValidationError): - ic_user_data.create_or_update_user() + pc_user_data.create_or_update_user() def test_join_org(self): # New membership. organization = PrescriberPoleEmploiFactory() assert organization.active_members.count() == 0 - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) - user, _ = ic_user_data.create_or_update_user() - ic_user_data.join_org(user=user, safir=organization.code_safir_pole_emploi) + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + user, _ = pc_user_data.create_or_update_user() + pc_user_data.join_org(user=user, safir=organization.code_safir_pole_emploi) assert organization.active_members.count() == 1 assert organization.has_admin(user) # User is already a member. - ic_user_data.join_org(user=user, safir=organization.code_safir_pole_emploi) + pc_user_data.join_org(user=user, safir=organization.code_safir_pole_emploi) assert organization.active_members.count() == 1 assert organization.has_admin(user) # Oganization does not exist. safir = "12345" with pytest.raises(PrescriberOrganization.DoesNotExist), self.assertLogs() as logs: - ic_user_data.join_org(user=user, safir=safir) + pc_user_data.join_org(user=user, safir=safir) assert f"Organization with SAFIR {safir} does not exist. Unable to add user {user.email}." in logs.output[0] assert organization.active_members.count() == 1 @@ -257,9 +257,9 @@ def test_get_existing_user_with_same_email_django(self): If there already is an existing django user with email ProConnect sent us, we do not create it again, We user it and we update it with the data form the identity_provider. """ - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) - PrescriberFactory(email=ic_user_data.email, identity_provider=users_enums.IdentityProvider.DJANGO) - user, created = ic_user_data.create_or_update_user() + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + PrescriberFactory(email=pc_user_data.email, identity_provider=users_enums.IdentityProvider.DJANGO) + user, created = pc_user_data.create_or_update_user() assert not created assert user.last_name == OIDC_USERINFO["usual_name"] assert user.first_name == OIDC_USERINFO["given_name"] @@ -268,17 +268,17 @@ def test_get_existing_user_with_same_email_django(self): def test_update_user_from_user_info(self): user = PrescriberFactory(**dataclasses.asdict(ProConnectPrescriberData.from_user_info(OIDC_USERINFO))) - ic_user = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + pc_user = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) - new_ic_user = ProConnectPrescriberData( - first_name="Jean", last_name="Gabin", username=ic_user.username, email="jean@lestontons.fr" + new_pc_user = ProConnectPrescriberData( + first_name="Jean", last_name="Gabin", username=pc_user.username, email="jean@lestontons.fr" ) now = timezone.now() # Because external_data_source_history is a JSONField # dates are actually stored as strings in the database now_str = json.loads(DjangoJSONEncoder().encode(now)) with mock.patch("django.utils.timezone.now", return_value=now): - user, created = new_ic_user.create_or_update_user() + user, created = new_pc_user.create_or_update_user() assert not created user.refresh_from_db() @@ -289,31 +289,31 @@ def test_update_user_from_user_info(self): "source": "PC", "created_at": now_str, } - for field in dataclasses.fields(ic_user) + for field in dataclasses.fields(pc_user) ] assert sorted(user.external_data_source_history, key=itemgetter("field_name")) == sorted( expected, key=itemgetter("field_name") ) def test_create_or_update_prescriber_raise_too_many_kind_exception(self): - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) for kind in [UserKind.JOB_SEEKER, UserKind.EMPLOYER, UserKind.LABOR_INSPECTOR]: - user = UserFactory(username=ic_user_data.username, email=ic_user_data.email, kind=kind) + user = UserFactory(username=pc_user_data.username, email=pc_user_data.email, kind=kind) with pytest.raises(InvalidKindException): - ic_user_data.create_or_update_user() + pc_user_data.create_or_update_user() user.delete() def test_create_or_update_employer_raise_too_many_kind_exception(self): - ic_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) + pc_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) for kind in [UserKind.JOB_SEEKER, UserKind.PRESCRIBER, UserKind.LABOR_INSPECTOR]: - user = UserFactory(username=ic_user_data.username, email=ic_user_data.email, kind=kind) + user = UserFactory(username=pc_user_data.username, email=pc_user_data.email, kind=kind) with pytest.raises(InvalidKindException): - ic_user_data.create_or_update_user() + pc_user_data.create_or_update_user() user.delete() @@ -346,8 +346,8 @@ def test_authorize_endpoint_with_params(self): response = self.client.get(url, follow=False) # TODO(alaurent) put back login_hint when ProConnect allow it assert f"login_hint={quote(email)}" not in response.url - ic_state = ProConnectState.get_from_state(self.client.session[constants.PRO_CONNECT_SESSION_KEY]["state"]) - assert ic_state.data["user_email"] == email + pc_state = ProConnectState.get_from_state(self.client.session[constants.PRO_CONNECT_SESSION_KEY]["state"]) + assert pc_state.data["user_email"] == email def test_authorize_check_user_kind(self): forbidden_user_kinds = [UserKind.ITOU_STAFF, UserKind.LABOR_INSPECTOR, UserKind.JOB_SEEKER] @@ -426,8 +426,8 @@ def test_callback_existing_django_user(self): @respx.mock def test_callback_allows_employer_on_prescriber_login_only(self): - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) - user = UserFactory(username=ic_user_data.username, email=ic_user_data.email, kind=UserKind.EMPLOYER) + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + user = UserFactory(username=pc_user_data.username, email=pc_user_data.email, kind=UserKind.EMPLOYER) response = mock_oauth_dance( self.client, @@ -448,8 +448,8 @@ def test_callback_allows_employer_on_prescriber_login_only(self): @respx.mock def test_callback_allows_prescriber_on_employer_login_only(self): - ic_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) - user = UserFactory(username=ic_user_data.username, email=ic_user_data.email, kind=UserKind.PRESCRIBER) + pc_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) + user = UserFactory(username=pc_user_data.username, email=pc_user_data.email, kind=UserKind.PRESCRIBER) response = mock_oauth_dance( self.client, @@ -470,8 +470,8 @@ def test_callback_allows_prescriber_on_employer_login_only(self): @respx.mock def test_callback_refuses_job_seekers(self): - ic_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) - user = UserFactory(username=ic_user_data.username, email=ic_user_data.email, kind=UserKind.JOB_SEEKER) + pc_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) + user = UserFactory(username=pc_user_data.username, email=pc_user_data.email, kind=UserKind.JOB_SEEKER) expected_redirect_url = add_url_params( reverse("pro_connect:logout"), {"redirect_url": reverse("search:employers_home")} @@ -499,10 +499,10 @@ def test_callback_refuses_job_seekers(self): @respx.mock def test_callback_redirect_prescriber_on_too_many_kind_exception(self): - ic_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) + pc_user_data = ProConnectPrescriberData.from_user_info(OIDC_USERINFO) for kind in [UserKind.JOB_SEEKER, UserKind.LABOR_INSPECTOR]: - user = UserFactory(username=ic_user_data.username, email=ic_user_data.email, kind=kind) + user = UserFactory(username=pc_user_data.username, email=pc_user_data.email, kind=kind) response = mock_oauth_dance( self.client, UserKind.PRESCRIBER, @@ -517,10 +517,10 @@ def test_callback_redirect_prescriber_on_too_many_kind_exception(self): @respx.mock def test_callback_redirect_employer_on_too_many_kind_exception(self): - ic_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) + pc_user_data = ProConnectEmployerData.from_user_info(OIDC_USERINFO) for kind in [UserKind.JOB_SEEKER, UserKind.LABOR_INSPECTOR]: - user = UserFactory(username=ic_user_data.username, email=ic_user_data.email, kind=kind) + user = UserFactory(username=pc_user_data.username, email=pc_user_data.email, kind=kind) # Don't check redirection as the user isn't an siae member yet, so it won't work. response = mock_oauth_dance( self.client, @@ -671,21 +671,21 @@ def test_callback_ft_users_unknown_safir_already_in_org(self): class ProConnectSessionTest(ProConnectBaseTestCase): def test_start_session(self): - ic_session = ProConnectSession() - assert ic_session.key == constants.PRO_CONNECT_SESSION_KEY + pc_session = ProConnectSession() + assert pc_session.key == constants.PRO_CONNECT_SESSION_KEY expected_keys = ["token", "state"] - ic_session_dict = ic_session.asdict() + pc_session_dict = pc_session.asdict() for key in expected_keys: with self.subTest(key): - assert key in ic_session_dict.keys() - assert ic_session_dict[key] is None + assert key in pc_session_dict.keys() + assert pc_session_dict[key] is None request = RequestFactory().get("/") middleware = SessionMiddleware(lambda x: x) middleware.process_request(request) request.session.save() - ic_session.bind_to_request(request) + pc_session.bind_to_request(request) assert request.session.get(constants.PRO_CONNECT_SESSION_KEY) @@ -877,9 +877,9 @@ def test_happy_path(self): response = self.client.get(url_from_map, follow=True) # Starting point of both the oauth_dance and `mock_oauth_dance()`. - ic_endpoint = response.redirect_chain[-1][0] - assert ic_endpoint.startswith(constants.PRO_CONNECT_ENDPOINT_AUTHORIZE) - assert f"idp_hint={constants.PRO_CONNECT_FT_IDP_HINT}" in ic_endpoint + pc_endpoint = response.redirect_chain[-1][0] + assert pc_endpoint.startswith(constants.PRO_CONNECT_ENDPOINT_AUTHORIZE) + assert f"idp_hint={constants.PRO_CONNECT_FT_IDP_HINT}" in pc_endpoint response = mock_oauth_dance( self.client, @@ -912,8 +912,8 @@ def test_create_user(self): response = self.client.get(url_from_map, follow=True) # Starting point of both the oauth_dance and `mock_oauth_dance()`. - ic_endpoint = response.redirect_chain[-1][0] - assert ic_endpoint.startswith(constants.PRO_CONNECT_ENDPOINT_AUTHORIZE) + pc_endpoint = response.redirect_chain[-1][0] + assert pc_endpoint.startswith(constants.PRO_CONNECT_ENDPOINT_AUTHORIZE) response = mock_oauth_dance( self.client, @@ -946,8 +946,8 @@ def test_create_user_organization_not_found(self): response = self.client.get(url_from_map, follow=True) # Starting point of both the oauth_dance and `mock_oauth_dance()`. - ic_endpoint = response.redirect_chain[-1][0] - assert ic_endpoint.startswith(constants.PRO_CONNECT_ENDPOINT_AUTHORIZE) + pc_endpoint = response.redirect_chain[-1][0] + assert pc_endpoint.startswith(constants.PRO_CONNECT_ENDPOINT_AUTHORIZE) response = mock_oauth_dance( self.client, diff --git a/tests/www/dashboard/test_edit_user_info.py b/tests/www/dashboard/test_edit_user_info.py index 422eb64a624..2644e7fdee0 100644 --- a/tests/www/dashboard/test_edit_user_info.py +++ b/tests/www/dashboard/test_edit_user_info.py @@ -9,10 +9,12 @@ from freezegun import freeze_time from itou.cities.models import City +from itou.prescribers.enums import PrescriberOrganizationKind from itou.users.enums import IdentityProvider, LackOfNIRReason, LackOfPoleEmploiId, Title from itou.users.models import User from itou.utils.mocks.address_format import mock_get_geocoding_data_by_ban_api_resolved from tests.openid_connect.inclusion_connect.test import InclusionConnectBaseTestCase +from tests.prescribers.factories import PrescriberMembershipFactory from tests.users.factories import ( JobSeekerFactory, PrescriberFactory, @@ -486,7 +488,13 @@ def test_edit_as_prescriber(self): self.assertContains(response, f"Prénom : {original_user.first_name.title()}") self.assertContains(response, f"Nom : {original_user.last_name.upper()}") self.assertContains(response, f"Adresse e-mail : {original_user.email}") - self.assertContains(response, "Ces informations doivent être modifiées sur votre fournisseur d'identité") + self.assertContains(response, "Vous pouvez modifier votre informations personnelles sur") + + original_user.email = "bob@francetravail.fr" + original_user.save() + PrescriberMembershipFactory(user=original_user, organization__kind=PrescriberOrganizationKind.PE) + response = self.client.get(url) + self.assertContains(response, "Ces informations doivent être modifiées par France Travail") post_data = { "email": "notbob@notsaintclair.com",