Skip to content

Commit

Permalink
fixup! pro_connect: Add a new SSO
Browse files Browse the repository at this point in the history
  • Loading branch information
tonial committed Sep 12, 2024
1 parent dc6904d commit 55fd4c1
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 65 deletions.
2 changes: 1 addition & 1 deletion itou/openid_connect/pro_connect/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions itou/templates/dashboard/edit_user_info.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,13 @@ <h2>Informations personnelles</h2>
Adresse e-mail : <strong>{{ user.email }}</strong>
</li>
</ul>
<p>
Ces informations doivent être modifiées sur votre fournisseur d'identité. Nous vous invitons à <a href="mailto:support@moncomptepro.beta.gouv.fr">contacter le support ProConnect</a> pour avoir plus d'aide."
</p>
{% if user_is_ft %}
<p>Ces informations doivent être modifiées par France Travail et seront mises à jour à votre prochaine connexion.</p>
{% else %}
<p>
Vous pouvez modifier votre informations personnelles sur <a href="https://app.moncomptepro.beta.gouv.fr/personal-information">ProConnect</a>. Elles seront mises à jour à votre prochaine connexion.
</p>
{% endif %}
<hr class="my-4">
{% bootstrap_form_errors form type="all" %}
{% elif ic_account_url %}
Expand Down
2 changes: 1 addition & 1 deletion itou/templates/inclusion_connect/includes/description.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{% if pro_connect_url %}

<h2>Créez un compte les emplois de l’inclusion avec ProConnect</h2>
<p class="mt-5">ProConnect est une solution de connexion unique à de nombres services publics.</p>
<p class="mt-5">ProConnect est une solution de connexion unique à de nombreux services publics.</p>
{% else %}
<h2>Créez un compte les emplois de l’inclusion avec Inclusion Connect</h2>
<p class="mt-5">
Expand Down
5 changes: 3 additions & 2 deletions itou/www/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/openid_connect/pro_connect/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

Expand Down
110 changes: 55 additions & 55 deletions tests/openid_connect/pro_connect/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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")
Expand All @@ -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"]
Expand All @@ -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
Expand All @@ -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"]
Expand All @@ -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()
Expand All @@ -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()

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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")}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 55fd4c1

Please sign in to comment.