Merge pull request #6346 from betagouv/add_encrypted_api_particulier_token

Add encrypted api particulier token
This commit is contained in:
LeSim 2021-07-30 11:28:45 +02:00 committed by GitHub
commit aa0bd47269
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 113 additions and 8 deletions

View file

@ -0,0 +1,21 @@
module EncryptableConcern
extend ActiveSupport::Concern
class_methods do
def attr_encrypted(*attributes)
attributes.each do |attribute|
define_method("#{attribute}=".to_sym) do |value|
self.public_send(
"encrypted_#{attribute}=".to_sym,
EncryptionService.new.encrypt(value)
)
end
define_method(attribute) do
value = self.public_send("encrypted_#{attribute}".to_sym)
EncryptionService.new.decrypt(value) if value.present?
end
end
end
end
end

View file

@ -18,6 +18,7 @@
# duree_conservation_dossiers_dans_ds :integer # duree_conservation_dossiers_dans_ds :integer
# duree_conservation_dossiers_hors_ds :integer # duree_conservation_dossiers_hors_ds :integer
# durees_conservation_required :boolean default(TRUE) # durees_conservation_required :boolean default(TRUE)
# encrypted_api_particulier_token :string
# euro_flag :boolean default(FALSE) # euro_flag :boolean default(FALSE)
# experts_require_administrateur_invitation :boolean default(FALSE) # experts_require_administrateur_invitation :boolean default(FALSE)
# for_individual :boolean default(FALSE) # for_individual :boolean default(FALSE)
@ -47,6 +48,7 @@
class Procedure < ApplicationRecord class Procedure < ApplicationRecord
include ProcedureStatsConcern include ProcedureStatsConcern
include EncryptableConcern
include Discard::Model include Discard::Model
self.discard_column = :hidden_at self.discard_column = :hidden_at
@ -56,6 +58,9 @@ class Procedure < ApplicationRecord
MAX_DUREE_CONSERVATION_EXPORT = 3.hours MAX_DUREE_CONSERVATION_EXPORT = 3.hours
MIN_WEIGHT = 350000 MIN_WEIGHT = 350000
attr_encrypted :api_particulier_token
has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure
belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false
belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true
@ -262,6 +267,7 @@ class Procedure < ApplicationRecord
if: -> { new_record? || created_at > Date.new(2020, 11, 13) } if: -> { new_record? || created_at > Date.new(2020, 11, 13) }
validates :api_entreprise_token, jwt_token: true, allow_blank: true validates :api_entreprise_token, jwt_token: true, allow_blank: true
validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/, message: "n'est pas un jeton valide" }, allow_blank: true
before_save :update_juridique_required before_save :update_juridique_required
after_initialize :ensure_path_exists after_initialize :ensure_path_exists
@ -440,6 +446,7 @@ class Procedure < ApplicationRecord
if is_different_admin if is_different_admin
procedure.administrateurs = [admin] procedure.administrateurs = [admin]
procedure.api_entreprise_token = nil procedure.api_entreprise_token = nil
procedure.encrypted_api_particulier_token = nil
else else
procedure.administrateurs = administrateurs procedure.administrateurs = administrateurs
end end

View file

@ -0,0 +1,17 @@
class EncryptionService
def initialize
len = ActiveSupport::MessageEncryptor.key_len
salt = Rails.application.secrets.encryption_service_salt
password = Rails.application.secrets.secret_key_base
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, len)
@encryptor = ActiveSupport::MessageEncryptor.new(key)
end
def encrypt(value)
value.blank? ? nil : @encryptor.encrypt_and_sign(value)
end
def decrypt(value)
value.blank? ? nil : @encryptor.decrypt_and_verify(value)
end
end

View file

@ -112,3 +112,6 @@ API_EDUCATION_URL="https://data.education.gouv.fr/api/records/1.0"
# Modifier le nb de tentatives de relance de job si echec # Modifier le nb de tentatives de relance de job si echec
# MAX_ATTEMPTS_JOBS=25 # MAX_ATTEMPTS_JOBS=25
# MAX_ATTEMPTS_API_ENTREPRISE_JOBS=5 # MAX_ATTEMPTS_API_ENTREPRISE_JOBS=5
# Clé de chriffrement des données sensibles en base
ENCRYPTION_SERVICE_SALT=""

View file

@ -11,6 +11,7 @@
# if you're sharing your code publicly. # if you're sharing your code publicly.
defaults: &defaults defaults: &defaults
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
encryption_service_salt: <%= ENV["ENCRYPTION_SERVICE_SALT"] %>
signing_key: <%= ENV["SIGNING_KEY"] %> signing_key: <%= ENV["SIGNING_KEY"] %>
otp_secret_key: <%= ENV["OTP_SECRET_KEY"] %> otp_secret_key: <%= ENV["OTP_SECRET_KEY"] %>
basic_auth: basic_auth:
@ -75,6 +76,7 @@ development:
test: test:
<<: *defaults <<: *defaults
secret_key_base: aa52abc3f3a629d04a61e9899a24c12f52b24c679cbf45f8ec0cdcc64ab9526d673adca84212882dff3911ac98e0c32ec4729ca7b3429ba18ef4dfd1bd18bc7a secret_key_base: aa52abc3f3a629d04a61e9899a24c12f52b24c679cbf45f8ec0cdcc64ab9526d673adca84212882dff3911ac98e0c32ec4729ca7b3429ba18ef4dfd1bd18bc7a
encryption_service_salt: QUDyMoXyw2YXU8pHnpts3w9MyMpsMQ6BgP62obgCf7PQv
signing_key: aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017 signing_key: aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017
otp_secret_key: 78ddda3679dc0ba2c99f50bcff04f49d862358dbeb7ead50368fdd6de14392be884ee10a204a0375b4b382e1a842fafe40d7858b7ab4796ec3a67c518d31112b otp_secret_key: 78ddda3679dc0ba2c99f50bcff04f49d862358dbeb7ead50368fdd6de14392be884ee10a204a0375b4b382e1a842fafe40d7858b7ab4796ec3a67c518d31112b
api_entreprise: api_entreprise:

View file

@ -0,0 +1,5 @@
class AddEncryptedAPIParticulierTokenToProcedures < ActiveRecord::Migration[6.0]
def change
add_column :procedures, :encrypted_api_particulier_token, :string
end
end

View file

@ -599,6 +599,7 @@ ActiveRecord::Schema.define(version: 2021_07_27_172504) do
t.bigint "draft_revision_id" t.bigint "draft_revision_id"
t.bigint "published_revision_id" t.bigint "published_revision_id"
t.boolean "allow_expert_review", default: true, null: false t.boolean "allow_expert_review", default: true, null: false
t.string "encrypted_api_particulier_token"
t.boolean "experts_require_administrateur_invitation", default: false t.boolean "experts_require_administrateur_invitation", default: false
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state" t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id" t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id"

View file

@ -206,18 +206,25 @@ describe Procedure do
end end
end end
context 'api_entreprise_token' do context 'when juridique_required is false' do
let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" } let(:procedure) { build(:procedure, juridique_required: false, cadre_juridique: nil) }
let(:invalid_token) { 'plouf' }
it { is_expected.to allow_value(valid_token).for(:api_entreprise_token) } it { expect(procedure.valid?).to eq(true) }
it { is_expected.not_to allow_value(invalid_token).for(:api_entreprise_token) }
end end
end end
context 'when juridique_required is false' do context 'api_entreprise_token' do
let(:procedure) { build(:procedure, juridique_required: false, cadre_juridique: nil) } let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" }
let(:invalid_token) { 'plouf' }
it { is_expected.to allow_value(valid_token).for(:api_entreprise_token) }
it { is_expected.not_to allow_value(invalid_token).for(:api_entreprise_token) }
end
it { expect(procedure.valid?).to eq(true) } context 'api_particulier_token' do
let(:valid_token) { "3841b13fa8032ed3c31d160d3437a76a" }
let(:invalid_token) { 'jet0n 1nvalide' }
it { is_expected.to allow_value(valid_token).for(:api_particulier_token) }
it { is_expected.not_to allow_value(invalid_token).for(:api_particulier_token) }
end end
context 'monavis' do context 'monavis' do

View file

@ -0,0 +1,42 @@
describe EncryptionService do
describe "#encrypt" do
subject { EncryptionService.new.encrypt(value) }
context "with a nil value" do
let(:value) { nil }
it { expect(subject).to be_nil }
end
context "with a string value" do
let(:value) { "The quick brown fox jumps over the lazy dog" }
it { expect(subject).to be_instance_of(String) }
it { expect(subject).to be_present }
it { expect(subject).not_to eq(value) }
end
end
describe "#decrypt" do
subject { EncryptionService.new.decrypt(encrypted_value) }
context "with a nil value" do
let(:encrypted_value) { nil }
it { expect(subject).to be_nil }
end
context "with a string value" do
let (:value) { "The quick brown fox jumps over the lazy dog" }
let(:encrypted_value) { EncryptionService.new.encrypt(value) }
it { expect(subject).to eq(value) }
end
context "with an invalid value" do
let(:encrypted_value) { "Gur dhvpx oebja sbk whzcf bire gur ynml qbt" }
it { expect { subject }.to raise_exception StandardError }
end
end
end