commit
93c7a9631a
12 changed files with 170 additions and 9 deletions
21
app/models/concerns/encryptable_concern.rb
Normal file
21
app/models/concerns/encryptable_concern.rb
Normal 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
|
|
@ -18,6 +18,7 @@
|
|||
# duree_conservation_dossiers_dans_ds :integer
|
||||
# duree_conservation_dossiers_hors_ds :integer
|
||||
# durees_conservation_required :boolean default(TRUE)
|
||||
# encrypted_api_particulier_token :string
|
||||
# euro_flag :boolean default(FALSE)
|
||||
# experts_require_administrateur_invitation :boolean default(FALSE)
|
||||
# for_individual :boolean default(FALSE)
|
||||
|
@ -47,6 +48,7 @@
|
|||
|
||||
class Procedure < ApplicationRecord
|
||||
include ProcedureStatsConcern
|
||||
include EncryptableConcern
|
||||
|
||||
include Discard::Model
|
||||
self.discard_column = :hidden_at
|
||||
|
@ -56,6 +58,9 @@ class Procedure < ApplicationRecord
|
|||
MAX_DUREE_CONSERVATION_EXPORT = 3.hours
|
||||
|
||||
MIN_WEIGHT = 350000
|
||||
|
||||
attr_encrypted :api_particulier_token
|
||||
|
||||
has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure
|
||||
belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false
|
||||
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) }
|
||||
|
||||
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
|
||||
after_initialize :ensure_path_exists
|
||||
|
@ -440,6 +446,7 @@ class Procedure < ApplicationRecord
|
|||
if is_different_admin
|
||||
procedure.administrateurs = [admin]
|
||||
procedure.api_entreprise_token = nil
|
||||
procedure.encrypted_api_particulier_token = nil
|
||||
else
|
||||
procedure.administrateurs = administrateurs
|
||||
end
|
||||
|
|
17
app/services/encryption_service.rb
Normal file
17
app/services/encryption_service.rb
Normal 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
|
|
@ -95,6 +95,10 @@ class PiecesJustificativesService
|
|||
def attached?
|
||||
true
|
||||
end
|
||||
|
||||
def record_type
|
||||
'Fake'
|
||||
end
|
||||
end
|
||||
|
||||
def self.generate_dossier_export(dossier)
|
||||
|
|
|
@ -25,14 +25,19 @@ class ProcedureArchiveService
|
|||
tmp_file = Tempfile.new(['tc', '.zip'])
|
||||
|
||||
Zip::OutputStream.open(tmp_file) do |zipfile|
|
||||
bug_reports = ''
|
||||
files.each do |attachment, pj_filename|
|
||||
zipfile.put_next_entry("procedure-#{@procedure.id}/#{pj_filename}")
|
||||
begin
|
||||
zipfile.puts(attachment.download)
|
||||
rescue
|
||||
raise "Problem while trying to attach #{pj_filename}"
|
||||
bug_reports += "Impossible de récupérer le fichier #{pj_filename}\n"
|
||||
end
|
||||
end
|
||||
if !bug_reports.empty?
|
||||
zipfile.put_next_entry("LISEZMOI.txt")
|
||||
zipfile.puts(bug_reports)
|
||||
end
|
||||
end
|
||||
|
||||
archive.file.attach(io: File.open(tmp_file), filename: archive.filename(@procedure))
|
||||
|
|
|
@ -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
|
||||
# MAX_ATTEMPTS_JOBS=25
|
||||
# MAX_ATTEMPTS_API_ENTREPRISE_JOBS=5
|
||||
|
||||
# Clé de chriffrement des données sensibles en base
|
||||
ENCRYPTION_SERVICE_SALT=""
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
# if you're sharing your code publicly.
|
||||
defaults: &defaults
|
||||
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
|
||||
encryption_service_salt: <%= ENV["ENCRYPTION_SERVICE_SALT"] %>
|
||||
signing_key: <%= ENV["SIGNING_KEY"] %>
|
||||
otp_secret_key: <%= ENV["OTP_SECRET_KEY"] %>
|
||||
basic_auth:
|
||||
|
@ -75,6 +76,7 @@ development:
|
|||
test:
|
||||
<<: *defaults
|
||||
secret_key_base: aa52abc3f3a629d04a61e9899a24c12f52b24c679cbf45f8ec0cdcc64ab9526d673adca84212882dff3911ac98e0c32ec4729ca7b3429ba18ef4dfd1bd18bc7a
|
||||
encryption_service_salt: QUDyMoXyw2YXU8pHnpts3w9MyMpsMQ6BgP62obgCf7PQv
|
||||
signing_key: aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017
|
||||
otp_secret_key: 78ddda3679dc0ba2c99f50bcff04f49d862358dbeb7ead50368fdd6de14392be884ee10a204a0375b4b382e1a842fafe40d7858b7ab4796ec3a67c518d31112b
|
||||
api_entreprise:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddEncryptedAPIParticulierTokenToProcedures < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :procedures, :encrypted_api_particulier_token, :string
|
||||
end
|
||||
end
|
|
@ -599,6 +599,7 @@ ActiveRecord::Schema.define(version: 2021_07_27_172504) do
|
|||
t.bigint "draft_revision_id"
|
||||
t.bigint "published_revision_id"
|
||||
t.boolean "allow_expert_review", default: true, null: false
|
||||
t.string "encrypted_api_particulier_token"
|
||||
t.boolean "experts_require_administrateur_invitation", default: false
|
||||
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
|
||||
t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id"
|
||||
|
|
|
@ -206,18 +206,25 @@ describe Procedure do
|
|||
end
|
||||
end
|
||||
|
||||
context 'api_entreprise_token' do
|
||||
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) }
|
||||
context 'when juridique_required is false' do
|
||||
let(:procedure) { build(:procedure, juridique_required: false, cadre_juridique: nil) }
|
||||
|
||||
it { expect(procedure.valid?).to eq(true) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when juridique_required is false' do
|
||||
let(:procedure) { build(:procedure, juridique_required: false, cadre_juridique: nil) }
|
||||
context 'api_entreprise_token' do
|
||||
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
|
||||
|
||||
context 'monavis' do
|
||||
|
|
42
spec/services/encryption_service_spec.rb
Normal file
42
spec/services/encryption_service_spec.rb
Normal 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
|
|
@ -53,6 +53,48 @@ describe ProcedureArchiveService do
|
|||
end
|
||||
expect(archive.file.attached?).to be_truthy
|
||||
end
|
||||
|
||||
context 'with a missing file' do
|
||||
let(:pj) do
|
||||
PiecesJustificativesService::FakeAttachment.new(
|
||||
file: StringIO.new('coucou'),
|
||||
filename: "export-dossier.pdf",
|
||||
name: 'pdf_export_for_instructeur',
|
||||
id: 1,
|
||||
created_at: Time.zone.now
|
||||
)
|
||||
end
|
||||
|
||||
let(:bad_pj) do
|
||||
PiecesJustificativesService::FakeAttachment.new(
|
||||
file: nil,
|
||||
filename: "cni.png",
|
||||
name: 'cni.png',
|
||||
id: 2,
|
||||
created_at: Time.zone.now
|
||||
)
|
||||
end
|
||||
|
||||
let(:documents) { [pj, bad_pj] }
|
||||
before do
|
||||
allow(PiecesJustificativesService).to receive(:liste_documents).and_return(documents)
|
||||
end
|
||||
|
||||
it 'collect files without raising exception' do
|
||||
expect { service.collect_files_archive(archive, instructeur) }.not_to raise_exception
|
||||
end
|
||||
|
||||
it 'add bug report to archive' do
|
||||
service.collect_files_archive(archive, instructeur)
|
||||
|
||||
archive.file.open do |f|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
expect(files.size).to be 4
|
||||
expect(files.last.filename).to include("LISEZMOI")
|
||||
expect(extract(f, files.last)).to match(/Impossible de .*cni.*png/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for all months' do
|
||||
|
@ -80,4 +122,9 @@ describe ProcedureArchiveService do
|
|||
Timecop.freeze(Time.zone.local(year, month, 5))
|
||||
create(:dossier, :accepte, :with_attestation, procedure: procedure)
|
||||
end
|
||||
|
||||
def extract(zip_file, zip_entry)
|
||||
extractor = zip_entry.extractor_from(zip_file)
|
||||
extractor.extract
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue