def setUpTestData(cls): cls.entry1 = Journal.objects.create(action="connect_aidant", initiator="ABC") cls.aidant_thierry = AidantFactory( username="******", email="*****@*****.**", first_name="Thierry", last_name="Martin", organisation=OrganisationFactory(name="Commune de Vernon"), ) cls.usager_ned = UsagerFactory(given_name="Ned", family_name="Flanders") cls.first_mandat = MandatFactory( organisation=cls.aidant_thierry.organisation, usager=cls.usager_ned, expiration_date=timezone.now() + timedelta(days=6), ) cls.first_autorisation = AutorisationFactory( mandat=cls.first_mandat, demarche="Revenus", ) Journal.log_autorisation_creation(cls.first_autorisation, aidant=cls.aidant_thierry) cls.mandat_thierry_ned_365 = MandatFactory( organisation=cls.aidant_thierry.organisation, usager=cls.usager_ned, expiration_date=timezone.now() + timedelta(days=365), )
def generate_log_entries(self, result, request): super().generate_log_entries(result, request) Journal.log_toitp_card_import( request.user, result.totals[RowResult.IMPORT_TYPE_NEW], result.totals[RowResult.IMPORT_TYPE_UPDATE], )
def confirm_autorisation_cancelation(request, usager_id, autorisation_id): aidant = request.user try: autorisation = aidant.get_active_autorisations_for_usager( usager_id).get(pk=autorisation_id) except Autorisation.DoesNotExist: django_messages.error( request, "Cette autorisation est introuvable ou inaccessible.") return redirect("espace_aidant_home") if request.method == "POST": form = request.POST if form: autorisation.revocation_date = timezone.now() autorisation.save(update_fields=["revocation_date"]) Journal.log_autorisation_cancel(autorisation, aidant) return redirect( "autorisation_cancelation_success", usager_id=usager_id, autorisation_id=autorisation_id, ) return render( request, "aidants_connect_web/mandat_auths_cancellation/" "confirm_autorisation_cancelation.html", { "aidant": aidant, "usager": aidant.get_usager(usager_id), "autorisation": autorisation, }, )
def usagers_mandats_autorisations_cancel_confirm( request, usager_id, mandat_id, autorisation_id ): aidant = request.user usager = aidant.get_usager(usager_id) if not usager: django_messages.error(request, "Cet usager est introuvable ou inaccessible.") return redirect("espace_aidant_home") autorisation = usager.get_autorisation(mandat_id, autorisation_id) if not autorisation: django_messages.error( request, "Cette autorisation est introuvable ou inaccessible." ) return redirect("espace_aidant_home") if autorisation.is_revoked: django_messages.error(request, "L'autorisation a été révoquée") return redirect("espace_aidant_home") if autorisation.is_expired: django_messages.error(request, "L'autorisation a déjà expiré") return redirect("espace_aidant_home") if request.method == "POST": form = request.POST if form: autorisation.revocation_date = timezone.now() autorisation.save(update_fields=["revocation_date"]) Journal.log_autorisation_cancel(autorisation, aidant) django_messages.success( request, "L'autorisation a été révoquée avec succès !" ) return redirect("usager_details", usager_id=usager.id) else: return render( request, "aidants_connect_web/usagers_mandats_autorisations_cancel_confirm.html", { "aidant": aidant, "usager": usager, "autorisation": autorisation, "error": "Erreur lors de l'annulation de l'autorisation.", }, ) return render( request, "aidants_connect_web/usagers_mandats_autorisations_cancel_confirm.html", { "aidant": aidant, "usager": usager, "autorisation": autorisation, "error": "Erreur lors de l'annulation de l'autorisation.", }, )
def validate_aidant_carte_totp(request, aidant_id): responsable: Aidant = request.user aidant = get_object_or_404(Aidant, pk=aidant_id) if not responsable.can_see_aidant(aidant): raise Http404 if not hasattr(aidant, "carte_totp"): django_messages.error( request, ( "Impossible de trouver une carte Aidants Connect associée au compte de " f"{aidant.get_full_name()}." "Vous devez d’abord lier une carte à son compte." ), ) return redirect( "espace_responsable_aidant", aidant_id=aidant.id, ) if request.method == "POST": form = CarteTOTPValidationForm(request.POST) else: form = CarteTOTPValidationForm() if form.is_valid(): token = form.cleaned_data["otp_token"] totp_device = TOTPDevice.objects.get( key=aidant.carte_totp.seed, user_id=aidant.id ) valid = totp_device.verify_token(token) if valid: with transaction.atomic(): totp_device.tolerance = 1 totp_device.confirmed = True totp_device.save() Journal.log_card_validation( responsable, aidant, aidant.carte_totp.serial_number ) django_messages.success( request, ( "Tout s’est bien passé, le compte de " f"{aidant.get_full_name()} est prêt !" ), ) return redirect( "espace_responsable_aidant", aidant_id=aidant.id, ) else: django_messages.error(request, "Ce code n’est pas valide.") return render( request, "aidants_connect_web/espace_responsable/validate-carte-totp.html", {"aidant": aidant, "organisation": organisation, "form": form}, )
def associate_aidant_carte_totp(request, aidant_id): responsable: Aidant = request.user aidant = get_object_or_404(Aidant, pk=aidant_id) if not responsable.can_see_aidant(aidant): raise Http404 if hasattr(aidant, "carte_totp"): django_messages.error( request, ( f"Le compte de {aidant.get_full_name()} est déjà lié à une carte " "Aidants Connect. Vous devez d’abord retirer la carte de son compte " "avant de pouvoir en lier une nouvelle." ), ) return redirect( "espace_responsable_aidant", aidant_id=aidant.id, ) if request.method == "GET": form = CarteOTPSerialNumberForm() if request.method == "POST": form = CarteOTPSerialNumberForm(request.POST) if form.is_valid(): serial_number = form.cleaned_data["serial_number"] try: carte_totp = CarteTOTP.objects.get(serial_number=serial_number) with transaction.atomic(): carte_totp.aidant = aidant carte_totp.save() totp_device = carte_totp.createTOTPDevice() totp_device.save() Journal.log_card_association(responsable, aidant, serial_number) return redirect( "espace_responsable_validate_totp", aidant_id=aidant.id, ) except Exception: django_messages.error( request, "Une erreur s’est produite lors de la sauvegarde de la carte.", ) # todo send exception to Sentry return render( request, "aidants_connect_web/espace_responsable/write-carte-totp-sn.html", { "aidant": aidant, "organisation": organisation, "responsable": responsable, "form": form, }, )
def user_info(request): auth_header = request.META.get("HTTP_AUTHORIZATION") if not auth_header: log.info("403: Missing auth header") return HttpResponseForbidden() pattern = re.compile(r"^Bearer\s([A-Z-a-z-0-9-_/-]+)$") if not pattern.match(auth_header): log.info("Auth header has wrong format") return HttpResponseForbidden() auth_token = auth_header[7:] auth_token_hash = make_password(auth_token, settings.FC_AS_FI_HASH_SALT) try: connection = Connection.objects.get(access_token=auth_token_hash) if connection.is_expired: log.info("connection has expired at user_info") return render(request, "408.html", status=408) except ObjectDoesNotExist: log.info( "403: /user_info No connection corresponds to the access_token") log.info(auth_token) return HttpResponseForbidden() usager = model_to_dict( connection.usager, fields=[ "birthcountry", "birthdate", "birthplace", "creation_date", "email", "family_name", "gender", "given_name", "preferred_username", "sub", ], ) birthdate = usager["birthdate"] birthplace = usager["birthplace"] birthcountry = usager["birthcountry"] usager["birthplace"] = str(birthplace) usager["birthcountry"] = str(birthcountry) usager["birthdate"] = str(birthdate) Journal.log_autorisation_use( aidant=connection.aidant, usager=connection.usager, demarche=connection.demarche, access_token=connection.access_token, autorisation=connection.autorisation, ) return JsonResponse(usager, safe=False)
def renew_mandat(request, usager_id): aidant: Aidant = request.user usager: Usager = aidant.get_usager(usager_id) if not usager: django_messages.error(request, "Cet usager est introuvable ou inaccessible.") return redirect("espace_aidant_home") form = MandatForm() if request.method == "GET": return render( request, "aidants_connect_web/new_mandat/renew_mandat.html", {"aidant": aidant, "form": form}, ) else: form = MandatForm(request.POST) if form.is_valid(): data = form.cleaned_data access_token = make_password(token_urlsafe(64), settings.FC_AS_FI_HASH_SALT) connection = Connection.objects.create( aidant=aidant, organisation=aidant.organisation, connection_type="FS", access_token=access_token, usager=usager, demarches=data["demarche"], duree_keyword=data["duree"], mandat_is_remote=data["is_remote"], ) duree = AuthorizationDurations.duration(connection.duree_keyword) Journal.log_init_renew_mandat( aidant=aidant, usager=usager, demarches=connection.demarches, duree=duree, is_remote_mandat=connection.mandat_is_remote, access_token=connection.access_token, ) request.session["connection"] = connection.pk return redirect("new_mandat_recap") else: return render( request, "aidants_connect_web/new_mandat/renew_mandat.html", {"aidant": aidant, "form": form}, )
def test_log_autorisation_creation_complete(self): autorisation = AutorisationFactory( mandat=self.mandat_thierry_ned_365, demarche="logement", ) Journal.log_autorisation_creation(autorisation, self.aidant_thierry) journal_entries = Journal.objects.all() self.assertEqual(len(journal_entries), 3) last_entry = journal_entries.last() self.assertEqual(last_entry.action, "create_autorisation") self.assertIn("Ned Flanders", last_entry.usager) self.assertEqual(last_entry.autorisation, autorisation.id)
def test_logging_of_aidant_conection(self): entry = Journal.log_connection(aidant=self.aidant_thierry) self.assertEqual(len(Journal.objects.all()), 3) self.assertEqual(entry.action, "connect_aidant") self.assertEqual( entry.initiator, "Thierry Martin - Commune de Vernon - [email protected]")
def test_a_create_attestation_journal_entry_can_be_created(self): demarches = ["transports", "logement"] expiration_date = timezone.now() + timedelta(days=6) entry = Journal.log_attestation_creation( aidant=self.aidant_thierry, usager=self.usager_ned, demarches=demarches, duree=6, is_remote_mandat=False, access_token="fjfgjfdkldlzlsmqqxxcn", attestation_hash=generate_attestation_hash(self.aidant_thierry, self.usager_ned, demarches, expiration_date), ) self.assertEqual(len(Journal.objects.all()), 3) self.assertEqual(entry.action, "create_attestation") attestation_string = ";".join([ str(self.aidant_thierry.id), date.today().isoformat(), "logement,transports", expiration_date.date().isoformat(), str(self.aidant_thierry.organisation.id), generate_file_sha256_hash(settings.MANDAT_TEMPLATE_PATH), self.usager_ned.sub, ]) self.assertTrue( validate_attestation_hash(attestation_string, entry.attestation_hash))
def test_a_franceconnect_usager_journal_entry_can_be_created(self): entry = Journal.log_franceconnection_usager( aidant=self.aidant_thierry, usager=self.usager_ned, ) self.assertEqual(len(Journal.objects.all()), 3) self.assertEqual(entry.action, "franceconnect_usager")
def remove_card_from_aidant(request, aidant_id): responsable: Aidant = request.user aidant = get_object_or_404(Aidant, pk=aidant_id) if not responsable.can_see_aidant(aidant): raise Http404 if not aidant.has_a_carte_totp: return redirect( "espace_responsable_aidant", aidant_id=aidant.id, ) form = RemoveCardFromAidantForm(request.POST) if not form.is_valid(): raise Exception("Invalid form for card/aidant dissociation") data = form.cleaned_data reason = data.get("reason") if reason == "autre": reason = data.get("other_reason") sn = aidant.carte_totp.serial_number with transaction.atomic(): carte = CarteTOTP.objects.get(serial_number=sn) try: device = TOTPDevice.objects.get(key=carte.seed, user=aidant) device.delete() except TOTPDevice.DoesNotExist: pass carte.aidant = None carte.save() Journal.log_card_dissociation(responsable, aidant, sn, reason) django_messages.success( request, ( f"Tout s’est bien passé, la carte {sn} a été séparée du compte " f"de l’aidant {aidant.get_full_name()}." ), ) return redirect( "espace_responsable_aidant", aidant_id=aidant.id, )
def get_user_info(connection: Connection) -> tuple: fc_base = settings.FC_AS_FS_BASE_URL fc_user_info = python_request.get( f"{fc_base}/userinfo?schema=openid", headers={"Authorization": f"Bearer {connection.access_token}"}, ) user_info = fc_user_info.json() if user_info.get("birthplace") == "": user_info["birthplace"] = None usager_sub = generate_sha256_hash( f"{user_info['sub']}{settings.FC_AS_FI_HASH_SALT}".encode()) try: usager = Usager.objects.get(sub=usager_sub) if usager.email != user_info.get("email"): usager.email = user_info.get("email") usager.save() Journal.log_update_email_usager(aidant=connection.aidant, usager=usager) return usager, None except Usager.DoesNotExist: try: usager = Usager.objects.create( given_name=user_info.get("given_name"), family_name=user_info.get("family_name"), birthdate=user_info.get("birthdate"), gender=user_info.get("gender"), birthplace=user_info.get("birthplace"), birthcountry=user_info.get("birthcountry"), sub=usager_sub, email=user_info.get("email"), ) return usager, None except IntegrityError as e: log.error("Error happened in Recap") log.error(e) return None, f"The FranceConnect ID is not complete: {e}"
def __dissociate_from_aidant_post(self, request, object_id): try: object = CarteTOTP.objects.get(id=object_id) aidant = object.aidant if aidant is None: self.message_user( request, f"Aucun aidant n’est associé à la carte {object.serial_number}.", messages.ERROR, ) return HttpResponseRedirect( reverse( "otpadmin:aidants_connect_web_cartetotp_changelist")) totp_devices = TOTPDevice.objects.filter(user=aidant, key=object.seed) for d in totp_devices: d.delete() object.aidant = None object.save() Journal.log_card_dissociation(request.user, aidant, object.serial_number, "Admin action") self.message_user(request, "Tout s'est bien passé.") return HttpResponseRedirect( reverse( "otpadmin:aidants_connect_web_cartetotp_change", kwargs={"object_id": object_id}, )) except Exception: logger.exception( "An error occured while trying to dissociate an aidant" "from their carte TOTP") self.message_user( request, "Quelque chose s’est mal passé durant l'opération.", messages.ERROR, ) return HttpResponseRedirect( reverse("otpadmin:aidants_connect_web_cartetotp_changelist"))
def test_log_autorisation_use_complete(self): entry = Journal.log_autorisation_use( aidant=self.aidant_thierry, usager=self.usager_ned, demarche="transports", access_token="fjfgjfdkldlzlsmqqxxcn", autorisation=self.first_autorisation, ) self.assertEqual(len(Journal.objects.all()), 3) self.assertEqual(entry.action, "use_autorisation") self.assertEqual(entry.demarche, "transports")
def test_it_is_impossible_to_delete_an_existing_entry(self): entry = Journal.log_autorisation_use( aidant=self.aidant_thierry, usager=self.usager_ned, demarche="transports", access_token="fjfgjfdkldlzlsmqqxxcn", autorisation=self.first_autorisation, ) self.assertRaises(NotImplementedError, entry.delete) self.assertEqual( Journal.objects.get(id=entry.id).demarche, "transports")
def activity_check(request): next_page = request.GET.get("next", settings.LOGIN_REDIRECT_URL) if not url_has_allowed_host_and_scheme( next_page, allowed_hosts={request.get_host()}, require_https=True): log.warning( "[Aidants Connect] an unsafe URL was used through the activity check" ) return HttpResponseNotFound() aidant = request.user if request.method == "POST": form = OTPForm(aidant=aidant, data=request.POST) if form.is_valid(): Journal.log_activity_check(aidant) return redirect(next_page) else: form = OTPForm(request.user) return render(request, "login/activity_check.html", { "form": form, "aidant": aidant })
def log_connection_on_login(sender, user: Aidant, request, **kwargs): Journal.log_connection(user)
def test_updating_revoked_autorisation_for_same_organisation(self): # first session : creating the autorisation self.client.force_login(self.aidant_thierry) mandat_builder_1 = Connection.objects.create( usager=self.test_usager, demarches=["papiers"], duree_keyword="SHORT" ) session = self.client.session session["connection"] = mandat_builder_1.id session.save() # trigger the mandat creation self.client.post( "/creation_mandat/recapitulatif/", data={"personal_data": True, "brief": True, "otp_token": "123456"}, ) self.assertEqual(Autorisation.objects.count(), 1) last_journal_entry = Journal.objects.last() self.assertEqual(last_journal_entry.action, "create_autorisation") # revoke self.client.post( "/creation_mandat/recapitulatif/", data={"personal_data": True, "brief": True, "otp_token": "123456"}, ) last_autorisation = Autorisation.objects.last() last_autorisation.revocation_date = timezone.now() last_autorisation.save(update_fields=["revocation_date"]) Journal.log_autorisation_cancel(last_autorisation, self.aidant_thierry) # second session : 'updating' the autorisation self.client.force_login(self.aidant_thierry) mandat_builder_2 = Connection.objects.create( usager=self.test_usager, demarches=["papiers", "logement"], duree_keyword="LONG", ) session = self.client.session session["connection"] = mandat_builder_2.id session.save() # trigger the mandat creation self.client.post( "/creation_mandat/recapitulatif/", data={"personal_data": True, "brief": True, "otp_token": "223456"}, ) self.assertEqual(Autorisation.objects.count(), 3) last_usager_organisation_papiers_autorisations = Autorisation.objects.filter( demarche="papiers", mandat__usager=self.test_usager, mandat__organisation=self.aidant_thierry.organisation, ).order_by("-mandat__creation_date") new_papiers_autorisation = last_usager_organisation_papiers_autorisations[0] old_papiers_autorisation = last_usager_organisation_papiers_autorisations[1] self.assertEqual(new_papiers_autorisation.duration_for_humans, 365) self.assertFalse(old_papiers_autorisation.is_expired) self.assertTrue(old_papiers_autorisation.is_revoked) last_journal_entries = Journal.objects.all().order_by("-id") self.assertEqual(last_journal_entries[0].action, "create_autorisation") self.assertEqual(last_journal_entries[0].demarche, "papiers") self.assertEqual(last_journal_entries[1].action, "create_autorisation") self.assertEqual(last_journal_entries[1].demarche, "logement") self.assertEqual(last_journal_entries[2].action, "create_attestation") self.assertEqual(last_journal_entries[2].demarche, "logement,papiers") self.assertEqual( len(self.aidant_thierry.get_active_demarches_for_usager(self.test_usager)), 2, )
def on_login(sender, user, request, **kwargs): Journal.log_connection(user)
def fc_callback(request): fc_base = settings.FC_AS_FS_BASE_URL fc_callback_uri = f"{settings.FC_AS_FS_CALLBACK_URL}/callback" fc_callback_uri_logout = f"{settings.FC_AS_FS_CALLBACK_URL}/logout-callback" fc_id = settings.FC_AS_FS_ID fc_secret = settings.FC_AS_FS_SECRET state = request.GET.get("state") try: connection = Connection.objects.get(state=state) except Connection.DoesNotExist: log.info("FC as FS - This state does not seem to exist") log.info(state) return HttpResponseForbidden() if connection.is_expired: log.info("408: FC connection has expired.") return render(request, "408.html", status=408) code = request.GET.get("code") if not code: log.info("403: No code has been provided.") return HttpResponseForbidden() token_url = f"{fc_base}/token" payload = { "grant_type": "authorization_code", "redirect_uri": fc_callback_uri, "client_id": fc_id, "client_secret": fc_secret, "code": code, } headers = {"Accept": "application/json"} request_for_token = python_request.post(token_url, data=payload, headers=headers) content = request_for_token.json() connection.access_token = content.get("access_token") connection.save() fc_id_token = content.get("id_token") try: decoded_token = jwt.decode( fc_id_token, settings.FC_AS_FS_SECRET, audience=settings.FC_AS_FS_ID, algorithm="HS256", ) except ExpiredSignatureError: log.info("403: token signature has expired.") return HttpResponseForbidden() if connection.nonce != decoded_token.get("nonce"): log.info("403: The nonce is different than the one expected.") return HttpResponseForbidden() if connection.is_expired: log.info("408: FC connection has expired.") return render(request, "408.html", status=408) usager, error = get_user_info(connection) if error: django_messages.error(request, error) return redirect("espace_aidant_home") connection.usager = usager connection.save() Journal.log_franceconnection_usager( aidant=connection.aidant, usager=connection.usager, ) logout_base = f"{fc_base}/logout" logout_id_token = f"id_token_hint={fc_id_token}" logout_state = f"state={state}" logout_redirect = f"post_logout_redirect_uri={fc_callback_uri_logout}" logout_url = f"{logout_base}?{logout_id_token}&{logout_state}&{logout_redirect}" return redirect(logout_url)
def test_log_autorisation_cancel_complete(self): entry = Journal.log_autorisation_cancel( autorisation=self.first_autorisation, aidant=self.aidant_thierry) self.assertEqual(len(Journal.objects.all()), 3) self.assertEqual(entry.action, "cancel_autorisation")
def new_mandat_recap(request): connection = Connection.objects.get(pk=request.session["connection"]) aidant = request.user usager = connection.usager demarches_description = [ humanize_demarche_names(demarche) for demarche in connection.demarches ] duree = connection.get_duree_keyword_display() is_remote = connection.mandat_is_remote if request.method == "GET": form = RecapMandatForm(aidant) return render( request, "aidants_connect_web/new_mandat/new_mandat_recap.html", { "aidant": aidant, "usager": usager, "demarches": demarches_description, "duree": duree, "is_remote": is_remote, "form": form, }, ) else: form = RecapMandatForm(aidant=aidant, data=request.POST) if form.is_valid(): now = timezone.now() expiration_date = { "SHORT": now + timedelta(days=1), "LONG": now + timedelta(days=365), "EUS_03_20": settings.ETAT_URGENCE_2020_LAST_DAY, } mandat_expiration_date = expiration_date.get( connection.duree_keyword) days_before_expiration_date = { "SHORT": 1, "LONG": 365, "EUS_03_20": 1 + (settings.ETAT_URGENCE_2020_LAST_DAY - now).days, } mandat_duree = days_before_expiration_date.get( connection.duree_keyword) try: # Add a Journal 'create_attestation' action connection.demarches.sort() Journal.log_attestation_creation( aidant=aidant, usager=usager, demarches=connection.demarches, duree=mandat_duree, is_remote_mandat=connection.mandat_is_remote, access_token=connection.access_token, attestation_hash=generate_attestation_hash( aidant, usager, connection.demarches, mandat_expiration_date), ) # Create a mandat mandat = Mandat.objects.create( organisation=aidant.organisation, usager=usager, duree_keyword=connection.duree_keyword, expiration_date=mandat_expiration_date, is_remote=connection.mandat_is_remote, ) # This loop creates one `autorisation` object per `démarche` in the form for demarche in connection.demarches: # Revoke existing demarche autorisation(s) similar_active_autorisations = Autorisation.objects.active( ).filter( mandat__organisation=aidant.organisation, mandat__usager=usager, demarche=demarche, ) for similar_active_autorisation in similar_active_autorisations: similar_active_autorisation.revocation_date = now similar_active_autorisation.save( update_fields=["revocation_date"]) Journal.log_autorisation_cancel( similar_active_autorisation, aidant) # Create new demarche autorisation autorisation = Autorisation.objects.create( mandat=mandat, demarche=demarche, last_renewal_token=connection.access_token, ) Journal.log_autorisation_creation(autorisation, aidant) except AttributeError as error: log.error("Error happened in Recap") log.error(error) django_messages.error( request, f"Error with Usager attribute : {error}") return redirect("espace_aidant_home") except IntegrityError as error: log.error("Error happened in Recap") log.error(error) django_messages.error(request, f"No Usager was given : {error}") return redirect("espace_aidant_home") return redirect("new_mandat_success") else: return render( request, "aidants_connect_web/new_mandat/new_mandat_recap.html", { "aidant": aidant, "usager": usager, "demarche": demarches_description, "duree": duree, "form": form, }, )
def confirm_mandat_cancelation(request, mandat_id): aidant: Aidant = request.user try: mandat = Mandat.objects.get(pk=mandat_id, organisation=aidant.organisation) except Mandat.DoesNotExist: django_messages.error(request, "Ce mandat est introuvable ou inaccessible.") return redirect("espace_aidant_home") usager = mandat.usager remaining_autorisations = [] if mandat.is_active: for autorisation in mandat.autorisations.filter(revocation_date=None): remaining_autorisations.append( humanize_demarche_names(autorisation.demarche)) if request.method == "POST": if request.POST: autorisation_in_mandat = Autorisation.objects.filter( mandat=mandat) for autorisation in autorisation_in_mandat: if not autorisation.revocation_date: autorisation.revocation_date = ( autorisation.revocation_date) = timezone.now() autorisation.save(update_fields=["revocation_date"]) Journal.log_autorisation_cancel(autorisation, aidant) Journal.log_mandat_cancel(mandat, aidant) return redirect("mandat_cancelation_success", mandat_id=mandat.id) else: return render( request, "aidants_connect_web/mandat_auths_cancellation/" "confirm_mandat_cancellation.html", { "aidant": aidant, "usager_name": usager.get_full_name(), "usager_id": usager.id, "mandat": mandat, "remaining_autorisations": remaining_autorisations, "error": "Une erreur s'est produite lors " "de la révocation du mandat", }, ) return render( request, "aidants_connect_web/mandat_auths_cancellation/" "confirm_mandat_cancellation.html", { "aidant": aidant, "usager_name": usager.get_full_name(), "usager_id": usager.id, "mandat": mandat, "remaining_autorisations": remaining_autorisations, }, )
def __associate_to_aidant_post(self, request, object_id): def redirect_to_list(): return HttpResponseRedirect( reverse("otpadmin:aidants_connect_web_cartetotp_changelist")) def redirect_to_object(object_id): return HttpResponseRedirect( reverse( "otpadmin:aidants_connect_web_cartetotp_change", kwargs={"object_id": object_id}, )) def redirect_to_try_again(object_id): return HttpResponseRedirect( reverse( "otpadmin:aidants_connect_web_carte_totp_associate", kwargs={"object_id": object_id}, )) if request.POST["aidant"].isnumeric(): target_aidant_id = int(request.POST["aidant"]) else: self.message_user(request, "L'identifiant de l'aidant est obligatoire.", messages.ERROR) return redirect_to_try_again(object_id) carte = CarteTOTP.objects.get(id=object_id) try: # Check if we are trying to associate the card with another aidant: BAD if carte.aidant is not None: if target_aidant_id != carte.aidant.id: self.message_user( request, f"La carte {carte} est déjà associée à un autre aidant.", messages.ERROR, ) return redirect_to_list() # link card with aidant target_aidant = Aidant.objects.get(id=target_aidant_id) if target_aidant.has_a_carte_totp and carte.aidant != target_aidant: self.message_user( request, f"L’aidant {target_aidant} a déjà une carte TOTP. " "Vous ne pouvez pas le lier à celle-ci en plus.", messages.ERROR, ) return redirect_to_try_again(object_id) carte.aidant = target_aidant carte.save() # check if totp devices need to be created totp_devices = TOTPDevice.objects.filter(user=target_aidant, key=carte.seed) if totp_devices.count() > 0: self.message_user( request, "Tout s'est bien passé. Le TOTP Device existait déjà.") return redirect_to_object(object_id) else: # No Device exists: crate the TOTP Device and save everything new_device = carte.createTOTPDevice(confirmed=True) new_device.save() Journal.log_card_association(request.user, target_aidant, carte.serial_number) self.message_user( request, f"Tout s'est bien passé. La carte {carte} a été associée à " f"{target_aidant} et un TOTP Device a été créé.", ) return redirect_to_list() except Aidant.DoesNotExist: self.message_user( request, f"Aucun aidant n’existe avec l'ID {target_aidant_id}. " "Veuillez corriger votre saisie.", messages.ERROR, ) return redirect_to_try_again(object_id) except Exception as e: logger.exception( "An error occured while trying to associate an aidant" "with a new TOTP.") self.message_user( request, f"Quelque chose s’est mal passé durant l'opération. {e}", messages.ERROR, ) return HttpResponseRedirect( reverse("otpadmin:aidants_connect_web_cartetotp_changelist"))
def new_mandat_recap(request): connection_id = request.session.get("connection") if not connection_id: log.error("No connection id found in session") return redirect("espace_aidant_home") connection = Connection.objects.get(pk=connection_id) aidant = request.user usager = connection.usager demarches_description = [ humanize_demarche_names(demarche) for demarche in connection.demarches ] duree = connection.get_duree_keyword_display() is_remote = connection.mandat_is_remote if request.method == "GET": form = RecapMandatForm(aidant) return render( request, "aidants_connect_web/new_mandat/new_mandat_recap.html", { "aidant": aidant, "usager": usager, "demarches": demarches_description, "duree": duree, "is_remote": is_remote, "form": form, }, ) else: form = RecapMandatForm(aidant=aidant, data=request.POST) if form.is_valid(): now = timezone.now() durationkw = connection.duree_keyword mandat_expiration_date = AuthorizationDurations.expiration( durationkw, now) mandat_duree = AuthorizationDurations.duration(durationkw, now) try: connection.demarches.sort() # Create a mandat mandat = Mandat.objects.create( organisation=aidant.organisation, usager=usager, duree_keyword=connection.duree_keyword, expiration_date=mandat_expiration_date, is_remote=connection.mandat_is_remote, ) # Add a Journal 'create_attestation' action Journal.log_attestation_creation( aidant=aidant, usager=usager, demarches=connection.demarches, duree=mandat_duree, is_remote_mandat=connection.mandat_is_remote, access_token=connection.access_token, attestation_hash=generate_attestation_hash( aidant, usager, connection.demarches, mandat_expiration_date), mandat=mandat, ) # This loop creates one `autorisation` object per `démarche` in the form for demarche in connection.demarches: # Revoke existing demarche autorisation(s) similar_active_autorisations = Autorisation.objects.active( ).filter( mandat__organisation=aidant.organisation, mandat__usager=usager, demarche=demarche, ) for similar_active_autorisation in similar_active_autorisations: similar_active_autorisation.revoke(aidant=aidant, revocation_date=now) # Create new demarche autorisation autorisation = Autorisation.objects.create( mandat=mandat, demarche=demarche, last_renewal_token=connection.access_token, ) Journal.log_autorisation_creation(autorisation, aidant) except AttributeError as error: log.error("Error happened in Recap") log.error(error) django_messages.error( request, f"Error with Usager attribute : {error}") return redirect("espace_aidant_home") except IntegrityError as error: log.error("Error happened in Recap") log.error(error) django_messages.error(request, f"No Usager was given : {error}") return redirect("espace_aidant_home") return redirect("new_mandat_success") else: return render( request, "aidants_connect_web/new_mandat/new_mandat_recap.html", { "aidant": aidant, "usager": usager, "demarche": demarches_description, "duree": duree, "form": form, }, )
def get_user_info(connection: Connection) -> tuple: fc_base = settings.FC_AS_FS_BASE_URL fc_user_info = python_request.get( f"{fc_base}/userinfo?schema=openid", headers={"Authorization": f"Bearer {connection.access_token}"}, ) user_info = fc_user_info.json() user_phone = connection.user_phone if len( connection.user_phone) > 0 else None if user_info.get("birthplace") == "": user_info["birthplace"] = None user_sub = user_info.get("sub") if not user_sub: return None, "Unable to find sub in FC user info" usager_sub = generate_sha256_hash( f"{user_sub}{settings.FC_AS_FI_HASH_SALT}".encode()) try: usager = Usager.objects.get(sub=usager_sub) if usager.email != user_info.get("email"): usager.email = user_info.get("email") usager.save() Journal.log_update_email_usager(aidant=connection.aidant, usager=usager) if user_phone is not None and usager.phone != user_phone: usager.phone = user_phone Journal.log_update_phone_usager(aidant=connection.aidant, usager=usager) usager.save() if not usager.preferred_username and user_info.get( "preferred_username"): usager.preferred_username = user_info.get("preferred_username") usager.save() return usager, None except Usager.DoesNotExist: kwargs = { "given_name": user_info.get("given_name"), "family_name": user_info.get("family_name"), "birthdate": user_info.get("birthdate"), "gender": user_info.get("gender"), "birthplace": user_info.get("birthplace"), "birthcountry": user_info.get("birthcountry"), "preferred_username": user_info.get("preferred_username"), "sub": usager_sub, "email": user_info.get("email"), } if user_phone is not None: kwargs["phone"] = user_phone try: usager = Usager.objects.create(**kwargs) return usager, None except IntegrityError as e: log.error("Error happened in Recap") log.error(e) return None, f"The FranceConnect ID is not complete: {e}"
def fc_callback(request): def fc_error(log_msg): log.error(log_msg) django_messages.error( request, "Nous avons rencontré une erreur en tentant d'interagir avec " "France Connect. C'est probabablement temporaire. Pouvez-vous réessayer " "votre requête ?", ) return redirect("new_mandat") fc_base = settings.FC_AS_FS_BASE_URL fc_callback_uri = f"{settings.FC_AS_FS_CALLBACK_URL}/callback" fc_callback_uri_logout = f"{settings.FC_AS_FS_CALLBACK_URL}/logout-callback" fc_id = settings.FC_AS_FS_ID fc_secret = settings.FC_AS_FS_SECRET state = request.GET.get("state") try: connection = Connection.objects.get(state=state) except Connection.DoesNotExist: return fc_error( f"FC as FS - This state does not seem to exist: {state}") if request.GET.get("error"): return fc_error(f"FranceConnect returned an error: " f"{request.GET.get('error_description')}") if connection.is_expired: return fc_error("408: FC connection has expired.") code = request.GET.get("code") if not code: return fc_error("FC AS FS: no code has been provided") token_url = f"{fc_base}/token" payload = { "grant_type": "authorization_code", "redirect_uri": fc_callback_uri, "client_id": fc_id, "client_secret": fc_secret, "code": code, } headers = {"Accept": "application/json"} request_for_token = python_request.post(token_url, data=payload, headers=headers) try: content = request_for_token.json() except ValueError: # not a valid JSON return fc_error( f"Request to {token_url} failed. Status code: " f"{request_for_token.status_code}, body: {request_for_token.text}") connection.access_token = content.get("access_token") if connection.access_token is None: return fc_error( f"No access_token return when requesting {token_url}. JSON response: " f"{repr(content)}") connection.save() fc_id_token = content.get("id_token") try: decoded_token = jwt.decode( fc_id_token, settings.FC_AS_FS_SECRET, audience=settings.FC_AS_FS_ID, algorithms=["HS256"], ) except ExpiredSignatureError: return fc_error("403: token signature has expired.") if connection.nonce != decoded_token.get("nonce"): return fc_error( "FC as FS: The nonce is different than the one expected") if connection.is_expired: log.info("408: FC connection has expired.") return render(request, "408.html", status=408) usager, error = get_user_info(connection) if error: return fc_error(error) connection.usager = usager connection.save() Journal.log_franceconnection_usager( aidant=connection.aidant, usager=connection.usager, ) logout_base = f"{fc_base}/logout" logout_id_token = f"id_token_hint={fc_id_token}" logout_state = f"state={state}" logout_redirect = f"post_logout_redirect_uri={fc_callback_uri_logout}" logout_url = f"{logout_base}?{logout_id_token}&{logout_state}&{logout_redirect}" return redirect(logout_url)