From 7a4bdc852b90f0ad1256afd2d58316f39a577fe3 Mon Sep 17 00:00:00 2001 From: Antoine Augusti Date: Tue, 12 Dec 2023 08:57:21 +0100 Subject: [PATCH 01/25] =?UTF-8?q?Notifications=20:=20ajuste=20le=20contenu?= =?UTF-8?q?=20du=20rappel=20p=C3=A9riodique=20des=20notifs=20aux=20product?= =?UTF-8?q?eurs=20(#3653)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...dic_reminder_producers_notification_job.ex | 29 +-------- .../email/producer_with_subscriptions.html.md | 22 ++----- .../producer_without_subscriptions.html.md | 24 +++----- ...minder_producers_notification_job_test.exs | 61 ++----------------- 4 files changed, 18 insertions(+), 118 deletions(-) diff --git a/apps/transport/lib/jobs/periodic_reminder_producers_notification_job.ex b/apps/transport/lib/jobs/periodic_reminder_producers_notification_job.ex index 3d42bb6c59..bcf476e182 100644 --- a/apps/transport/lib/jobs/periodic_reminder_producers_notification_job.ex +++ b/apps/transport/lib/jobs/periodic_reminder_producers_notification_job.ex @@ -111,8 +111,6 @@ defmodule Transport.Jobs.PeriodicReminderProducersNotificationJob do |> Enum.filter(&DB.Dataset.is_active?/1) |> Enum.sort_by(fn %DB.Dataset{custom_title: custom_title} -> custom_title end) - contacts_in_orgs = contact |> other_contacts_in_orgs() - Transport.EmailSender.impl().send_mail( "transport.data.gouv.fr", Application.get_env(:transport, :contact_email), @@ -120,12 +118,7 @@ defmodule Transport.Jobs.PeriodicReminderProducersNotificationJob do Application.get_env(:transport, :contact_email), "Notifications pour vos données sur transport.data.gouv.fr", "", - Phoenix.View.render_to_string(TransportWeb.EmailView, "producer_without_subscriptions.html", %{ - manage_organization_url: contact |> manage_organization_url(), - datasets: datasets, - contacts_in_orgs: Enum.map_join(contacts_in_orgs, ", ", &DB.Contact.display_name/1), - has_other_contacts: not Enum.empty?(contacts_in_orgs) - }) + Phoenix.View.render_to_string(TransportWeb.EmailView, "producer_without_subscriptions.html", %{datasets: datasets}) ) DB.Notification.insert!(@notification_reason, contact.email) @@ -142,7 +135,6 @@ defmodule Transport.Jobs.PeriodicReminderProducersNotificationJob do "Rappel : vos notifications pour vos données sur transport.data.gouv.fr", "", Phoenix.View.render_to_string(TransportWeb.EmailView, "producer_with_subscriptions.html", %{ - manage_organization_url: contact |> manage_organization_url(), datasets_subscribed: contact |> datasets_subscribed_as_producer(), has_other_producers_subscribers: not Enum.empty?(other_producers_subscribers), other_producers_subscribers: Enum.map_join(other_producers_subscribers, ", ", &DB.Contact.display_name/1) @@ -152,14 +144,6 @@ defmodule Transport.Jobs.PeriodicReminderProducersNotificationJob do DB.Notification.insert!(@notification_reason, contact.email) end - def manage_organization_url(%DB.Contact{organizations: [%DB.Organization{id: org_id}]}) do - Application.fetch_env!(:transport, :datagouvfr_site) <> "/fr/admin/organization/#{org_id}/" - end - - def manage_organization_url(%DB.Contact{}) do - Application.fetch_env!(:transport, :datagouvfr_site) <> "/fr/admin/" - end - @spec datasets_subscribed_as_producer(DB.Contact.t()) :: [DB.Dataset.t()] def datasets_subscribed_as_producer(%DB.Contact{notification_subscriptions: subscriptions}) do subscriptions @@ -169,17 +153,6 @@ defmodule Transport.Jobs.PeriodicReminderProducersNotificationJob do |> Enum.sort_by(& &1.custom_title) end - @spec other_contacts_in_orgs(DB.Contact.t()) :: [DB.Contact.t()] - def other_contacts_in_orgs(%DB.Contact{id: contact_id} = contact) do - contact - |> DB.Repo.preload(organizations: [:contacts]) - |> Map.fetch!(:organizations) - |> Enum.flat_map(& &1.contacts) - |> Enum.uniq() - |> Enum.reject(&match?(%DB.Contact{id: ^contact_id}, &1)) - |> Enum.sort_by(&DB.Contact.display_name/1) - end - @spec subscribed_as_producer?(DB.Contact.t()) :: boolean() def subscribed_as_producer?(%DB.Contact{notification_subscriptions: subscriptions}) do Enum.any?(subscriptions, &match?(%DB.NotificationSubscription{role: :producer}, &1)) diff --git a/apps/transport/lib/transport_web/templates/email/producer_with_subscriptions.html.md b/apps/transport/lib/transport_web/templates/email/producer_with_subscriptions.html.md index c639edd489..7705861efd 100644 --- a/apps/transport/lib/transport_web/templates/email/producer_with_subscriptions.html.md +++ b/apps/transport/lib/transport_web/templates/email/producer_with_subscriptions.html.md @@ -1,13 +1,9 @@ Bonjour, -Vous gérez des données présentes sur transport.data.gouv.fr. - -## Gérer vos notifications - <%= if Enum.count(@datasets_subscribed) == 1 do %> -Vous êtes susceptible de recevoir des notifications pour le jeu de données <%= @datasets_subscribed |> hd() |> link_for_dataset() %>. +Vous êtes inscrit à des notifications pour le jeu de données <%= @datasets_subscribed |> hd() |> link_for_dataset() %>. <% else %> -Vous êtes susceptible de recevoir des notifications pour les jeux de données suivants : +Vous êtes inscrit à des notifications concernant les jeux de données suivants : <% end %> -Les notifications facilitent la gestion de vos données. Elles vous permettront d’être averti de l’expiration de vos ressources, des erreurs qu’elles peuvent contenir et de leur potentielle indisponibilité. - -Vous pouvez gérer ces notifications depuis [votre espace producteur](<%= TransportWeb.Router.Helpers.page_url(TransportWeb.Endpoint, :espace_producteur) %>) du Point d’Accès National. - -## Gérer les membres de votre organisation - -L’administrateur de votre organisation peut ajouter, modifier ou supprimer les différents membres depuis [votre espace d’administration data.gouv.fr](<%= @manage_organization_url %>). +Les notifications vous permettent d’être alerté en cas d’expiration, d’indisponibilité et d’erreurs de vos données. Rendez-vous sur votre [Espace Producteur](<%= TransportWeb.Router.Helpers.page_url(TransportWeb.Endpoint, :espace_producteur) %>) pour les gérer de manière autonome. <%= if @has_other_producers_subscribers do %> Les autres personnes inscrites à ces notifications sont : <%= @other_producers_subscribers %>. <% end %> -Chaque utilisateur peut paramétrer ses propres notifications depuis son espace producteur du PAN. - Nous restons disponibles pour vous accompagner si besoin. -À bientôt ! +À bientôt, + +L’équipe transport.data.gouv.fr diff --git a/apps/transport/lib/transport_web/templates/email/producer_without_subscriptions.html.md b/apps/transport/lib/transport_web/templates/email/producer_without_subscriptions.html.md index e1507647a0..8ab2bdc9d7 100644 --- a/apps/transport/lib/transport_web/templates/email/producer_without_subscriptions.html.md +++ b/apps/transport/lib/transport_web/templates/email/producer_without_subscriptions.html.md @@ -1,13 +1,9 @@ Bonjour, -Vous gérez des données présentes sur transport.data.gouv.fr. - -## Recevoir des notifications - <%= if Enum.count(@datasets) == 1 do %> -Vous gérez le jeu de données <%= @datasets |> hd() |> link_for_dataset() %>. +Le saviez-vous ? Il est possible de vous inscrire à des notifications concernant le jeu de données que vous gérez sur transport.data.gouv.fr, <%= @datasets |> hd() |> link_for_dataset() %>. <% else %> -Vous gérez les jeux de données suivants : +Le saviez-vous ? Il est possible de vous inscrire à des notifications concernant les jeux de données que vous gérez sur transport.data.gouv.fr : <% end %> -Pour vous faciliter la gestion de ces données, vous pouvez activer des notifications depuis [votre espace producteur](<%= TransportWeb.Router.Helpers.page_url(TransportWeb.Endpoint, :espace_producteur) %>) du Point d’Accès National. Elles vous permettront d’être averti de l’expiration de vos ressources, des erreurs qu’elles peuvent contenir et de leur potentielle indisponibilité. +Les notifications vous permettent d’être alerté en cas d’expiration, d’indisponibilité et d’erreurs de vos données. -## Gérer les membres de votre organisation - -L’administrateur de votre organisation peut ajouter, modifier ou supprimer les différents membres depuis [votre espace d’administration data.gouv.fr](<%= @manage_organization_url %>). - -<%= if @has_other_contacts do %> -Les autres personnes pouvant s’inscrire à ces notifications et s’étant déjà connectées sont : <%= @contacts_in_orgs %>. -<% end %> - -Chaque utilisateur peut paramétrer ses propres notifications depuis son espace producteur du PAN. +Pour vous inscrire, rien de plus simple : rendez-vous sur votre [Espace Producteur](<%= TransportWeb.Router.Helpers.page_url(TransportWeb.Endpoint, :espace_producteur) %>) dans le menu “Recevoir des notifications”. Nous restons disponibles pour vous accompagner si besoin. -À bientôt ! +À bientôt, + +L’équipe transport.data.gouv.fr diff --git a/apps/transport/test/transport/jobs/periodic_reminder_producers_notification_job_test.exs b/apps/transport/test/transport/jobs/periodic_reminder_producers_notification_job_test.exs index 9e043e87cc..d4a94c381b 100644 --- a/apps/transport/test/transport/jobs/periodic_reminder_producers_notification_job_test.exs +++ b/apps/transport/test/transport/jobs/periodic_reminder_producers_notification_job_test.exs @@ -104,28 +104,6 @@ defmodule Transport.Test.Transport.Jobs.PeriodicReminderProducersNotificationJob |> PeriodicReminderProducersNotificationJob.subscribed_as_producer?() end - test "other_contacts_in_orgs" do - org_id = Ecto.UUID.generate() - - contact = - insert_contact(%{ - organizations: [ - sample_org(%{"id" => org_id}), - sample_org() - ] - }) - - %DB.Contact{id: other_contact_id} = - insert_contact(%{ - organizations: [ - sample_org(%{"id" => org_id}) - ] - }) - - assert [%DB.Contact{id: ^other_contact_id}] = - PeriodicReminderProducersNotificationJob.other_contacts_in_orgs(contact) - end - test "other_producers_subscribers" do producer_1 = insert_contact() %DB.Contact{id: producer_2_id} = producer_2 = insert_contact() @@ -216,7 +194,7 @@ defmodule Transport.Test.Transport.Jobs.PeriodicReminderProducersNotificationJob assert subject == "Rappel : vos notifications pour vos données sur transport.data.gouv.fr" assert html =~ - ~s(Vous êtes susceptible de recevoir des notifications pour le jeu de données #{dataset.custom_title}) + ~s(Vous êtes inscrit à des notifications pour le jeu de données #{dataset.custom_title}) assert html =~ "Les autres personnes inscrites à ces notifications sont : Marina Loiseau." end) @@ -237,15 +215,6 @@ defmodule Transport.Test.Transport.Jobs.PeriodicReminderProducersNotificationJob ] }) - insert_contact(%{ - first_name: "Marina", - last_name: "Loiseau", - organizations: [ - sample_org(%{"id" => org_id}), - sample_org() - ] - }) - dataset = insert(:dataset, custom_title: "Super JDD", organization_id: org_id) refute contact @@ -263,12 +232,10 @@ defmodule Transport.Test.Transport.Jobs.PeriodicReminderProducersNotificationJob assert subject == "Notifications pour vos données sur transport.data.gouv.fr" assert html =~ - ~s(Vous gérez le jeu de données #{dataset.custom_title}) - - assert html =~ "Pour vous faciliter la gestion de ces données, vous pouvez activer des notifications" + ~s(Il est possible de vous inscrire à des notifications concernant le jeu de données que vous gérez sur transport.data.gouv.fr, #{dataset.custom_title}) assert html =~ - "Les autres personnes pouvant s’inscrire à ces notifications et s’étant déjà connectées sont : Marina Loiseau." + ~s(Pour vous inscrire, rien de plus simple : rendez-vous sur votre Espace Producteur) end) assert :ok == perform_job(PeriodicReminderProducersNotificationJob, %{"contact_id" => contact.id}) @@ -286,27 +253,7 @@ defmodule Transport.Test.Transport.Jobs.PeriodicReminderProducersNotificationJob perform_job(PeriodicReminderProducersNotificationJob, %{"contact_id" => contact.id}) end - describe "manage_organization_url" do - test "single org" do - org_id = Ecto.UUID.generate() - contact = %{organizations: [sample_org(%{"id" => org_id})]} |> insert_contact() |> DB.Repo.preload(:organizations) - assert 1 == contact.organizations |> Enum.count() - - assert "https://demo.data.gouv.fr/fr/admin/organization/#{org_id}/" == - contact |> PeriodicReminderProducersNotificationJob.manage_organization_url() - end - - test "multiple orgs" do - contact = %{organizations: [sample_org(), sample_org()]} |> insert_contact() |> DB.Repo.preload(:organizations) - - assert 2 == contact.organizations |> Enum.count() - - assert "https://demo.data.gouv.fr/fr/admin/" == - contact |> PeriodicReminderProducersNotificationJob.manage_organization_url() - end - end - - defp sample_org(%{} = args \\ %{}) do + defp sample_org(%{} = args) do Map.merge( %{ "acronym" => nil, From abc05ee11869ab102da71d19f959b485619e7e8a Mon Sep 17 00:00:00 2001 From: Antoine Augusti Date: Tue, 12 Dec 2023 09:03:00 +0100 Subject: [PATCH 02/25] Backoffice : ajout doc/config pour plug rate limiter (#3652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Backoffice : ajout doc/config pour plug rate limiter * Améliorations suite à PR --------- Co-authored-by: Thibaut Barrère --- .../live/backoffice/rate_limiter_live.ex | 69 ++++++++++++ .../backoffice/rate_limiter_live.html.heex | 101 ++++++++++++++++++ apps/transport/lib/transport_web/router.ex | 4 + .../templates/backoffice/page/index.html.heex | 4 + apps/transport/priv/gettext/backoffice.pot | 4 + .../priv/gettext/en/LC_MESSAGES/backoffice.po | 4 + .../priv/gettext/fr/LC_MESSAGES/backoffice.po | 4 + .../live_views/rate_limiter_live_test.exs | 25 +++++ 8 files changed, 215 insertions(+) create mode 100644 apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.ex create mode 100644 apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.html.heex create mode 100644 apps/transport/test/transport_web/live_views/rate_limiter_live_test.exs diff --git a/apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.ex b/apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.ex new file mode 100644 index 0000000000..733581f8a7 --- /dev/null +++ b/apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.ex @@ -0,0 +1,69 @@ +defmodule TransportWeb.Backoffice.RateLimiterLive do + use Phoenix.LiveView + import TransportWeb.Backoffice.JobsLive, only: [ensure_admin_auth_or_redirect: 3] + import TransportWeb.Router.Helpers, only: [static_path: 2] + import Helpers, only: [format_number: 1] + + @impl true + def mount(_params, %{"current_user" => current_user} = _session, socket) do + {:ok, + ensure_admin_auth_or_redirect(socket, current_user, fn socket -> + if connected?(socket), do: schedule_next_update_data() + + socket + |> assign(%{ + phoenix_ddos_max_2min_requests: env_value_to_int("PHOENIX_DDOS_MAX_2MIN_REQUESTS"), + phoenix_ddos_max_1hour_requests: env_value_to_int("PHOENIX_DDOS_MAX_1HOUR_REQUESTS"), + phoenix_ddos_safelist_ips: env_value_to_list("PHOENIX_DDOS_SAFELIST_IPS"), + phoenix_ddos_blocklist_ips: env_value_to_list("PHOENIX_DDOS_BLOCKLIST_IPS"), + log_user_agent: env_value("LOG_USER_AGENT"), + block_user_agent_keywords: env_value_to_list("BLOCK_USER_AGENT_KEYWORDS"), + allow_user_agents: env_value_to_list("ALLOW_USER_AGENTS") + }) + |> update_data() + end)} + end + + @impl true + def handle_info(:update_data, socket) do + schedule_next_update_data() + {:noreply, update_data(socket)} + end + + defp schedule_next_update_data do + Process.send_after(self(), :update_data, 1000) + end + + defp update_data(socket) do + assign(socket, + last_updated_at: (Time.utc_now() |> Time.truncate(:second) |> to_string()) <> " UTC", + ips_in_jail: ips_in_jail() + ) + end + + @impl true + def handle_event("bail_ip_from_jail", %{"ip" => ip}, socket) do + # See https://github.com/xward/phoenix_ddos/blob/feb07469ce318214cddb8e88ac18b5f94b3e31f2/lib/phoenix_ddos/core/jail.ex#L36 + PhoenixDDoS.Jail.bail_out(ip) + {:noreply, socket} + end + + defp ips_in_jail do + # See https://github.com/xward/phoenix_ddos/blob/master/lib/phoenix_ddos/core/jail.ex + {:ok, keys} = Cachex.keys(:phoenix_ddos_jail) + keys |> Enum.reject(&String.starts_with?(&1, "suspicious_")) + end + + defp env_value(env_value), do: System.get_env(env_value) + + defp env_value_to_int(env_name) do + env_name |> System.get_env("500") |> Integer.parse() |> elem(0) + end + + defp env_value_to_list(env_name) do + case System.get_env(env_name, "") do + "" -> "" + value -> value + end + end +end diff --git a/apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.html.heex b/apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.html.heex new file mode 100644 index 0000000000..851f63ba25 --- /dev/null +++ b/apps/transport/lib/transport_web/live/backoffice/rate_limiter_live.html.heex @@ -0,0 +1,101 @@ +
+
+

Plug

+

+ Le plug qui gère la logique de rate limiting, bloquer ou autoriser des requêtes est dans TransportWeb.Plugs.RateLimiter. +

+

+ Le comptage des requêtes et le blocage d'une adresse IP est effectué par la librairie phoenix_ddos. Le backend est Cachex, qui utilise la RAM. Ainsi un redémarrage de l'application + prod-site + réinitialise les compteurs et les adresses IPs bloquées. +

+ +

Configuration

+

+ Plusieurs variables d'environnement permettent de configurer le fonctionnement. Il faut changer ces variables pour ajuster le comportement puis redémarrer l'application. +

+ +

Volume de requêtes

+
    +
  • + PHOENIX_DDOS_MAX_2MIN_REQUESTS + : nombre de requêtes max autorisée par IP par période de 2 minutes. Valeur actuelle : <%= format_number( + @phoenix_ddos_max_2min_requests + ) %> +
  • +
  • + PHOENIX_DDOS_MAX_1HOUR_REQUESTS + : nombre de requêtes max autorisée par IP par période d'1 heure. Valeur actuelle : <%= format_number( + @phoenix_ddos_max_1hour_requests + ) %> +
  • +
+ +

Adresses IPs autorisées ou bloquées

+ +
    +
  • + PHOENIX_DDOS_SAFELIST_IPS + : liste d'adresses IP toujours autorisées. Les valeurs doivent être séparées par des |. Valeur actuelle : + <%= @phoenix_ddos_safelist_ips %> +
  • +
  • + PHOENIX_DDOS_BLOCKLIST_IPS + : liste d'adresses IP toujours bloquées. Les valeurs doivent être séparées par des |. Valeur actuelle : + <%= @phoenix_ddos_blocklist_ips %> +
  • +
+ +

User-Agents

+ +
    +
  • + LOG_USER_AGENT + : active ou désactive le fait de logguer les user agents. Valeurs possible : true + ou false. Valeur actuelle : <%= @log_user_agent %> +
  • +
  • + ALLOW_USER_AGENTS + : user agents toujours autorisés. Les valeurs doivent être séparées par des |. Valeur actuelle : + <%= @allow_user_agents %> +
  • +
  • + BLOCK_USER_AGENT_KEYWORDS + : user agents toujours bloqués. Les valeurs doivent être séparées par des |. Valeur actuelle : + <%= @block_user_agent_keywords %> +
  • +
+ +

Adresses IPs bloquées

+

+ phoenix_ddos + est la dépendance qui gère l'ajout/le retrait de la jail. +

+ +

+ Personne n'est bloqué actuellement. +

+ +
0}> +

+ Adresses IPs actuellement dans la jail : +

+ +
    + <%= for ip <- @ips_in_jail do %> +
  • + <%= ip %> + +
  • + <% end %> +
+
+

Dernière mise à jour: <%= @last_updated_at %>

+
+
+