From a729518e60c226ef81cfae73c3c2a7ba3ec0ef58 Mon Sep 17 00:00:00 2001 From: Victor Mours Date: Thu, 21 Nov 2024 11:02:08 +0100 Subject: [PATCH] Allow read only authentication for visioplainte --- .env.sample | 1 + .../api/visioplainte/base_controller.rb | 48 ++++++++++++------- .../api/visioplainte/creneaux_controller.rb | 2 + docs/interconnexions/visioplainte.md | 7 +++ .../api/visioplainte/authentication_spec.rb | 45 +++++++++++++---- spec/swagger_helper.rb | 4 ++ swagger/visioplainte/api.json | 2 +- 7 files changed, 84 insertions(+), 25 deletions(-) diff --git a/.env.sample b/.env.sample index 0d0957a8d..dba5c232c 100644 --- a/.env.sample +++ b/.env.sample @@ -96,6 +96,7 @@ AGENT_CONNECT_RDVAN_CLIENT_SECRET="voir https://vaultwarden.incubateur.net/#/vau # INCLUSIONCONNECT_DISABLED=true VISIOPLAINTE_API_KEY="visioplainte-api-test-key-123456" +VISIOPLAINTE_API_KEY_READ_ONLY="visioplainte-api-test-key-read-only-456789" COOP_MEDIATION_NUMERIQUE_API_KEY="coop-mediation-numerique-api-test-key-123456" diff --git a/app/controllers/api/visioplainte/base_controller.rb b/app/controllers/api/visioplainte/base_controller.rb index b37d88a53..58065bf5e 100644 --- a/app/controllers/api/visioplainte/base_controller.rb +++ b/app/controllers/api/visioplainte/base_controller.rb @@ -8,22 +8,6 @@ class Api::Visioplainte::BaseController < ActionController::Base # rubocop:disab GENDARMERIE_SERVICE_NAME = "Gendarmerie Nationale".freeze - def authenticate_with_api_key - authorized = ActiveSupport::SecurityUtils.secure_compare( - request.headers["X-VISIOPLAINTE-API-KEY"] || "", - ENV.fetch("VISIOPLAINTE_API_KEY") - ) - - unless authorized - render( - status: :unauthorized, - json: { - errors: ["Authentification invalide"], - } - ) - end - end - def reset # On met plusieurs guard clauses de sécurité pour s'assurer qu'on ne peut appeler cette méthode destructive que sur la staging return unless ENV["RDV_SOLIDARITES_INSTANCE_NAME"] == "STAGING" @@ -47,4 +31,36 @@ def reset load Rails.root.join("db/seeds/visioplainte.rb") head :ok end + + protected + + def allow_authentication_with_read_only_api_key + @read_only_api_key_allowed = true + end + + def authenticate_with_api_key + authorized = ActiveSupport::SecurityUtils.secure_compare( + request.headers["X-VISIOPLAINTE-API-KEY"] || "", + ENV.fetch("VISIOPLAINTE_API_KEY") + ) || authorized_with_read_only_api_key? + + unless authorized + render( + status: :unauthorized, + json: { + errors: ["Authentification invalide"], + } + ) + end + end + + def authorized_with_read_only_api_key? + return false unless @read_only_api_key_allowed + return false if ENV["VISIOPLAINTE_API_KEY_READ_ONLY"].blank? + + ActiveSupport::SecurityUtils.secure_compare( + request.headers["X-VISIOPLAINTE-API-KEY"], + ENV["VISIOPLAINTE_API_KEY_READ_ONLY"] + ) + end end diff --git a/app/controllers/api/visioplainte/creneaux_controller.rb b/app/controllers/api/visioplainte/creneaux_controller.rb index 652d8c271..5dc1aec5f 100644 --- a/app/controllers/api/visioplainte/creneaux_controller.rb +++ b/app/controllers/api/visioplainte/creneaux_controller.rb @@ -4,6 +4,8 @@ class Api::Visioplainte::CreneauxController < Api::Visioplainte::BaseController before_action :validate_date_debut before_action :validate_date_range, only: [:index] + prepend_before_action :allow_authentication_with_read_only_api_key + def index creneaux = CreneauxSearch::ForUser.new( motif: motif, diff --git a/docs/interconnexions/visioplainte.md b/docs/interconnexions/visioplainte.md index 3870cc298..1221ca698 100644 --- a/docs/interconnexions/visioplainte.md +++ b/docs/interconnexions/visioplainte.md @@ -17,6 +17,7 @@ Ils et elles ne peuvent donc pas se connecter à RDV Service Public. Un autre besoin identifié est un environnement de staging pouvant être réinitialisé sur demande. + ## Avancement En date d’octobre 2024, nous avons fini de développer la première version de l’API. @@ -27,3 +28,9 @@ Les équipes de SensioLabs nous indiquent travailler sur l’intégration de leu L’API est implémenté par des contrôleurs tous regroupés dans `app/controllers/api/visioplainte`. Les appels à notre API sont authentifiées via un header `X-VISIOPLAINTE-API-KEY`. + +## Intégration avec MaSécurité + +Pour éviter de démarrer un processus de visioplainte alors qu'il n'y a aucun créneau de disponible, l'équipe qui gère le site MaSécurité a aussi besoin d'accéder à l'application pour savoir s'il existe des créneaux. +Nous avons donc mis en place la possibilité de se connecter à l'api avec une clé avec des permissions en lecture seule, pour avoir le minimum de permissions. +Il y a donc deux clés d'api, celle en lecture-écriture pour Visioplainte, et celle en lecture seule pour MaSécurité. diff --git a/spec/requests/api/visioplainte/authentication_spec.rb b/spec/requests/api/visioplainte/authentication_spec.rb index 88d252083..bcedb7e1a 100644 --- a/spec/requests/api/visioplainte/authentication_spec.rb +++ b/spec/requests/api/visioplainte/authentication_spec.rb @@ -1,14 +1,14 @@ RSpec.describe "Authentification" do + let(:creneaux_params) do + { + service: "Gendarmerie", + date_debut: "2024-08-19", + date_fin: "2024-08-25", + } + end + context "when the api key is configured properly" do stub_env_with(VISIOPLAINTE_API_KEY: "visioplainte-api-test-key-123456") - let(:creneaux_params) do - { - service: "Gendarmerie", - date_debut: "2024-08-19", - date_fin: "2024-08-25", - } - end - context "without the api key header" do before do get "/api/visioplainte/creneaux" @@ -55,4 +55,33 @@ expect(response).to be_nil end end + + context "with a read-only api key" do + stub_env_with(VISIOPLAINTE_API_KEY_READ_ONLY: "visioplainte-api-test-key-read-only-456789") + stub_env_with(VISIOPLAINTE_API_KEY: "visioplainte-api-test-key-123456") + + context "on an endpoint that only reads data" do + before do + load Rails.root.join("db/seeds/visioplainte.rb") + get "/api/visioplainte/creneaux", headers: { "X-VISIOPLAINTE-API-KEY": "visioplainte-api-test-key-read-only-456789" }, params: creneaux_params + end + + it "returns a 200 response" do + expect(response.status).to eq 200 + expect(response.parsed_body.keys).to eq ["creneaux"] + end + end + + context "on an endpoint that modifies data" do + before do + load Rails.root.join("db/seeds/visioplainte.rb") + post "/api/visioplainte/rdvs", headers: { "X-VISIOPLAINTE-API-KEY": "visioplainte-api-test-key-read-only-456789" } + end + + it "returns a 401 response" do + expect(response.status).to eq 401 + expect(response.parsed_body).to eq({ "errors" => ["Authentification invalide"] }) + end + end + end end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 05976af37..6e8e3f619 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -35,6 +35,10 @@ curl --request GET --url "https://demo.rdv.anct.gouv.fr/api/visioplainte/creneaux" --header "X-VISIOPLAINTE-API-KEY: LA_CLE_D_API" ``` + ## Clé d'api en lecture seule + + Il est possible d'avoir une clé d'api qui n'a des permissions qu'en lecture seule sur les créneaux. L'authentification se fait via le même header. + # Réinitialisation des données sur l'environnement de staging La staging de RDV Service Public a vocation à être utilisée par l'environnement de qualification du téléservice Visioplainte. diff --git a/swagger/visioplainte/api.json b/swagger/visioplainte/api.json index 2bee91d60..a2ad27668 100644 --- a/swagger/visioplainte/api.json +++ b/swagger/visioplainte/api.json @@ -3,7 +3,7 @@ "info": { "title": "API RDV Service Public pour Visioplainte", "version": "v1", - "description": "# Authentification\n\nL'authentification à l'api se fait en passant la clé d'api dans le header `X-VISIOPLAINTE-API-KEY`.\n\nPar exemple:\n```\ncurl --request GET --url \"https://demo.rdv.anct.gouv.fr/api/visioplainte/creneaux\" --header \"X-VISIOPLAINTE-API-KEY: LA_CLE_D_API\"\n```\n\n# Réinitialisation des données sur l'environnement de staging\n\nLa staging de RDV Service Public a vocation à être utilisée par l'environnement de qualification du téléservice Visioplainte.\nAfin de faciliter les tests sur l'environnement de staging, un endpoint permet de réinitialiser les données.\nCet endpoint n'est disponible qu'en staging, pas en production, ni en démo, ni en local.\n\nCet appel ce fait avec un POST sur le path /api/visioplainte/reset, par exemple :\n```\ncurl -X POST --url \"https://staging.rdv-service-public.fr/api/visioplainte/reset\" --header \"X-VISIOPLAINTE-API-KEY: LA_CLE_D_API\"\n```\n" + "description": "# Authentification\n\nL'authentification à l'api se fait en passant la clé d'api dans le header `X-VISIOPLAINTE-API-KEY`.\n\nPar exemple:\n```\ncurl --request GET --url \"https://demo.rdv.anct.gouv.fr/api/visioplainte/creneaux\" --header \"X-VISIOPLAINTE-API-KEY: LA_CLE_D_API\"\n```\n\n## Clé d'api en lecture seule\n\nIl est possible d'avoir une clé d'api qui n'a des permissions qu'en lecture seule sur les créneaux. L'authentification se fait via le même header.\n\n# Réinitialisation des données sur l'environnement de staging\n\nLa staging de RDV Service Public a vocation à être utilisée par l'environnement de qualification du téléservice Visioplainte.\nAfin de faciliter les tests sur l'environnement de staging, un endpoint permet de réinitialiser les données.\nCet endpoint n'est disponible qu'en staging, pas en production, ni en démo, ni en local.\n\nCet appel ce fait avec un POST sur le path /api/visioplainte/reset, par exemple :\n```\ncurl -X POST --url \"https://staging.rdv-service-public.fr/api/visioplainte/reset\" --header \"X-VISIOPLAINTE-API-KEY: LA_CLE_D_API\"\n```\n" }, "paths": { "/api/visioplainte/creneaux": {