From 3a88f206ec7e4542539b6c2d112c02cd86a69f43 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 12 Jun 2019 16:36:08 +0000 Subject: [PATCH 01/12] javascript: fix dependancies compilation using yarn 1.16.0 --- yarn.lock | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/yarn.lock b/yarn.lock index cc2d47945..3417a9550 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3533,9 +3533,9 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.8.tgz#57ea5320f762cd4696e5e8e87120eccc8b11cacf" - integrity sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA== + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== dependencies: nan "^2.12.1" node-pre-gyp "^0.12.0" @@ -4788,16 +4788,6 @@ lodash._reinterpolate@~3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= - -lodash.clonedeep@^4.3.2: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.get@^4.0, lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -4813,11 +4803,6 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.mergewith@^4.6.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" - integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== - lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" @@ -5197,11 +5182,16 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@^2.10.0, nan@^2.12.1: +nan@^2.12.1: version "2.13.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw== +nan@^2.13.2: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -5324,9 +5314,9 @@ node-releases@^1.1.14: semver "^5.3.0" node-sass@^4.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" - integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA== + version "4.12.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017" + integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -5335,12 +5325,10 @@ node-sass@^4.11.0: get-stdin "^4.0.1" glob "^7.0.3" in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" + lodash "^4.17.11" meow "^3.7.0" mkdirp "^0.5.1" - nan "^2.10.0" + nan "^2.13.2" node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0" From 2a22a522c8e9c1f0cf5afffaf0b0434c63802fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20L=C3=A9tang?= Date: Mon, 17 Jun 2019 12:27:12 +0200 Subject: [PATCH 02/12] fix duplicate content --- CONTRIBUTING.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a51ae6d8..99c8a343f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,4 @@ Si vous souhaitez adapter demarches-simplifiees.fr à votre besoin, nous vous re Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation. -Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation. - -Totefois, le ministère des armées a déployé une instance au sein de leur intranet. Nous proposons aux acteurs qui sont interessés de les mettre en relation avec eux afin de disposer d'un retour d'expérience, et bénéficier de leur retour. +Toutefois, le ministère des armées a déployé une instance au sein de leur intranet. Nous proposons aux acteurs qui sont interessés de les mettre en relation avec eux afin de disposer d'un retour d'expérience, et bénéficier de leur retour. From 51aacabf1308b95ab91a00c63fa1fd3cf65256a6 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 29 May 2019 16:28:27 +0000 Subject: [PATCH 03/12] models: fix touch not propagating when using nested attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes, when using nested attributes, touch doesn’t propagate to parent relationships. (see https://github.com/rails/rails/issues/26726) Specifically, this happens in our app when updating a dossier with only new attachements (but without changing the value of any fields). To work around this, we need to define the parent relationship explicitely. This is good practice anyway. Fix #3906 --- app/models/avis.rb | 2 +- app/models/champ.rb | 2 +- app/models/commentaire.rb | 2 +- app/models/dossier.rb | 10 ++--- app/models/piece_justificative.rb | 2 +- .../users/dossiers_controller_spec.rb | 43 +++++++++++++++++-- spec/factories/procedure.rb | 8 ++++ 7 files changed, 57 insertions(+), 12 deletions(-) diff --git a/app/models/avis.rb b/app/models/avis.rb index 7203870ea..251b80647 100644 --- a/app/models/avis.rb +++ b/app/models/avis.rb @@ -1,7 +1,7 @@ class Avis < ApplicationRecord include EmailSanitizableConcern - belongs_to :dossier, touch: true + belongs_to :dossier, inverse_of: :avis, touch: true belongs_to :gestionnaire belongs_to :claimant, class_name: 'Gestionnaire' diff --git a/app/models/champ.rb b/app/models/champ.rb index 06bc65871..91835499f 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -1,5 +1,5 @@ class Champ < ApplicationRecord - belongs_to :dossier, touch: true + belongs_to :dossier, inverse_of: :champs, touch: true belongs_to :type_de_champ, inverse_of: :champ belongs_to :parent, class_name: 'Champ' has_many :commentaires diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index 5a49b6e84..68b571164 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -1,5 +1,5 @@ class Commentaire < ApplicationRecord - belongs_to :dossier, touch: true + belongs_to :dossier, inverse_of: :commentaires, touch: true belongs_to :piece_justificative belongs_to :user diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 3bd702d47..698e265be 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -19,18 +19,18 @@ class Dossier < ApplicationRecord has_one :individual, dependent: :destroy has_one :attestation, dependent: :destroy - has_many :pieces_justificatives, dependent: :destroy + has_many :pieces_justificatives, inverse_of: :dossier, dependent: :destroy has_one_attached :justificatif_motivation - has_many :champs, -> { root.public_only.ordered }, dependent: :destroy - has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', dependent: :destroy - has_many :commentaires, dependent: :destroy + has_many :champs, -> { root.public_only.ordered }, inverse_of: :dossier, dependent: :destroy + has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy + has_many :commentaires, inverse_of: :dossier, dependent: :destroy has_many :invites, dependent: :destroy has_many :follows, -> { active } has_many :previous_follows, -> { inactive }, class_name: 'Follow' has_many :followers_gestionnaires, through: :follows, source: :gestionnaire has_many :previous_followers_gestionnaires, -> { distinct }, through: :previous_follows, source: :gestionnaire - has_many :avis, dependent: :destroy + has_many :avis, inverse_of: :dossier, dependent: :destroy has_many :dossier_operation_logs, dependent: :destroy diff --git a/app/models/piece_justificative.rb b/app/models/piece_justificative.rb index 20a95260c..48e4876fc 100644 --- a/app/models/piece_justificative.rb +++ b/app/models/piece_justificative.rb @@ -1,5 +1,5 @@ class PieceJustificative < ApplicationRecord - belongs_to :dossier, touch: true + belongs_to :dossier, inverse_of: :pieces_justificatives, touch: true belongs_to :type_de_piece_justificative has_one :commentaire diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index 9d68d2003..1da3997af 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -386,9 +386,11 @@ describe Users::DossiersController, type: :controller do describe '#update_brouillon' do before { sign_in(user) } + let!(:dossier) { create(:dossier, user: user) } let(:first_champ) { dossier.champs.first } let(:value) { 'beautiful value' } + let(:now) { Time.zone.parse('01/01/2100') } let(:submit_payload) do { id: dossier.id, @@ -402,7 +404,11 @@ describe Users::DossiersController, type: :controller do end let(:payload) { submit_payload } - subject { patch :update_brouillon, params: payload } + subject do + Timecop.freeze(now) do + patch :update_brouillon, params: payload + end + end context 'when the dossier cannot be updated by the user' do let!(:dossier) { create(:dossier, :en_instruction, user: user) } @@ -421,6 +427,7 @@ describe Users::DossiersController, type: :controller do expect(response).to redirect_to(merci_dossier_path(dossier)) expect(first_champ.reload.value).to eq('beautiful value') + expect(dossier.reload.updated_at.year).to eq(2100) expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction)) end @@ -549,9 +556,15 @@ describe Users::DossiersController, type: :controller do describe '#update' do before { sign_in(user) } - let!(:dossier) { create(:dossier, :en_construction, user: user) } + + let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_piece_justificative) } + let!(:dossier) { create(:dossier, :en_construction, user: user, procedure: procedure) } let(:first_champ) { dossier.champs.first } + let(:piece_justificative_champ) { dossier.champs.last } let(:value) { 'beautiful value' } + let(:file) { Rack::Test::UploadedFile.new("./spec/fixtures/files/piece_justificative_0.pdf", 'application/pdf') } + let(:now) { Time.zone.parse('01/01/2100') } + let(:submit_payload) do { id: dossier.id, @@ -565,7 +578,11 @@ describe Users::DossiersController, type: :controller do end let(:payload) { submit_payload } - subject { patch :update, params: payload } + subject do + Timecop.freeze(now) do + patch :update, params: payload + end + end context 'when the dossier cannot be updated by the user' do let!(:dossier) { create(:dossier, :en_instruction, user: user) } @@ -584,8 +601,28 @@ describe Users::DossiersController, type: :controller do expect(response).to redirect_to(demande_dossier_path(dossier)) expect(first_champ.reload.value).to eq('beautiful value') + expect(dossier.reload.updated_at.year).to eq(2100) expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction)) end + + context 'when only files champs are modified' do + let(:submit_payload) do + { + id: dossier.id, + dossier: { + champs_attributes: { + id: piece_justificative_champ.id, + piece_justificative_file: file + } + } + } + end + + it 'updates the dossier modification date' do + subject + expect(dossier.reload.updated_at.year).to eq(2100) + end + end end context 'when the update fails' do diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb index 98488e884..8c62169b3 100644 --- a/spec/factories/procedure.rb +++ b/spec/factories/procedure.rb @@ -127,6 +127,14 @@ FactoryBot.define do end end + trait :with_piece_justificative do + after(:build) do |procedure, _evaluator| + type_de_champ = create(:type_de_champ_piece_justificative) + procedure.types_de_champ << type_de_champ + end + end + + # Deprecated trait :with_two_type_de_piece_justificative do after(:build) do |procedure, _evaluator| rib = create(:type_de_piece_justificative, :rib, order_place: 1) From 7e80b8a4dc1b4838df2f9032c7818a5efa4bbcd1 Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Wed, 12 Jun 2019 17:10:53 +0000 Subject: [PATCH 04/12] Enable the Rails/InverseOf cop and add missing `inverse_of` --- .rubocop.yml | 2 +- app/models/champs/repetition_champ.rb | 2 +- app/models/dossier.rb | 4 ++-- app/models/gestionnaire.rb | 6 +++--- app/models/procedure.rb | 6 +++--- app/models/type_de_champ.rb | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 307bc088d..445c3400a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -726,7 +726,7 @@ Rails/HttpStatus: Enabled: false Rails/InverseOf: - Enabled: false + Enabled: true Rails/LexicallyScopedActionFilter: Enabled: false diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb index 7f8313e16..50295a6f6 100644 --- a/app/models/champs/repetition_champ.rb +++ b/app/models/champs/repetition_champ.rb @@ -1,5 +1,5 @@ class Champs::RepetitionChamp < Champ - has_many :champs, -> { ordered }, foreign_key: :parent_id, dependent: :destroy + has_many :champs, -> { ordered }, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy accepts_nested_attributes_for :champs, allow_destroy: true diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 698e265be..3449517ec 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -26,8 +26,8 @@ class Dossier < ApplicationRecord has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy has_many :commentaires, inverse_of: :dossier, dependent: :destroy has_many :invites, dependent: :destroy - has_many :follows, -> { active } - has_many :previous_follows, -> { inactive }, class_name: 'Follow' + has_many :follows, -> { active }, inverse_of: :dossier + has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier has_many :followers_gestionnaires, through: :follows, source: :gestionnaire has_many :previous_followers_gestionnaires, -> { distinct }, through: :previous_follows, source: :gestionnaire has_many :avis, inverse_of: :dossier, dependent: :destroy diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb index 24a7b5498..c5ab8b7c2 100644 --- a/app/models/gestionnaire.rb +++ b/app/models/gestionnaire.rb @@ -12,12 +12,12 @@ class Gestionnaire < ApplicationRecord has_many :assign_to, dependent: :destroy has_many :procedures, through: :assign_to - has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo' + has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo', inverse_of: :gestionnaire has_many :procedures_with_email_notifications, through: :assign_to_with_email_notifications, source: :procedure has_many :dossiers, -> { state_not_brouillon }, through: :procedures - has_many :follows, -> { active } - has_many :previous_follows, -> { inactive }, class_name: 'Follow' + has_many :follows, -> { active }, inverse_of: :gestionnaire + has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :gestionnaire has_many :followed_dossiers, through: :follows, source: :dossier has_many :previously_followed_dossiers, -> { distinct }, through: :previous_follows, source: :dossier has_many :avis diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 0cfe9a71d..c1eddeba0 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -5,9 +5,9 @@ class Procedure < ApplicationRecord MAX_DUREE_CONSERVATION = 36 - has_many :types_de_piece_justificative, -> { ordered }, dependent: :destroy - has_many :types_de_champ, -> { root.public_only.ordered }, dependent: :destroy - has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', dependent: :destroy + has_many :types_de_piece_justificative, -> { ordered }, inverse_of: :procedure, dependent: :destroy + has_many :types_de_champ, -> { root.public_only.ordered }, inverse_of: :procedure, dependent: :destroy + has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', inverse_of: :procedure, dependent: :destroy has_many :dossiers, dependent: :restrict_with_exception has_many :deleted_dossiers, dependent: :destroy diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index f3b9efe86..924672ec9 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -32,7 +32,7 @@ class TypeDeChamp < ApplicationRecord belongs_to :procedure belongs_to :parent, class_name: 'TypeDeChamp' - has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', dependent: :destroy + has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', inverse_of: :parent, dependent: :destroy store_accessor :options, :cadastres, :quartiers_prioritaires, :parcelles_agricoles, :old_pj delegate :tags_for_template, to: :dynamic_type From ab92d6b90617621bbb94102416bb08657fac2c49 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Mon, 17 Jun 2019 13:42:47 +0200 Subject: [PATCH 05/12] doc: clarify wording in CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99c8a343f..295a988ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,6 @@ demarches-simplifiees.fr est **compliqué à héberger**. Parmi les problématiq Si vous souhaitez adapter demarches-simplifiees.fr à votre besoin, nous vous recommandons de **proposer vos modifications à la base de code principale** (par exemple en créant une issue) **plutôt que d’héberger une autre instance vous-même**. -Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation. +Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d’assurer de support technique concernant votre installation. -Toutefois, le ministère des armées a déployé une instance au sein de leur intranet. Nous proposons aux acteurs qui sont interessés de les mettre en relation avec eux afin de disposer d'un retour d'expérience, et bénéficier de leur retour. +Toutefois, certains acteurs (le ministère des armées, l’administration autonome en Polynésie française) ont déployé des instances séparées. Nous proposons aux personnes intéressées de les mettre en relation avec ces acteurs existants, afin de disposer d’un retour d’expérience, et de bénéficier de leur retour. From e16cb731c54c437582e0b18a6da21287926ebeca Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Tue, 14 May 2019 15:19:25 +0200 Subject: [PATCH 06/12] =?UTF-8?q?Add=20poor=20man=E2=80=99s=20ASN1=20parsi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 2 +- app/lib/asn1/timestamp.rb | 25 ++++++++++++++++++ .../files/bill_signature/signature.der | Bin 0 -> 2186 bytes spec/lib/asn1/timestamp_spec.rb | 19 +++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/lib/asn1/timestamp.rb create mode 100644 spec/fixtures/files/bill_signature/signature.der create mode 100644 spec/lib/asn1/timestamp_spec.rb diff --git a/.editorconfig b/.editorconfig index 16ff5720c..111c6ec90 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,5 +10,5 @@ indent_size = 2 indent_style = space trim_trailing_whitespace = true -[*.{ico,keep,pdf,svg}] +[*.{ico,keep,pdf,svg,der}] insert_final_newline = false diff --git a/app/lib/asn1/timestamp.rb b/app/lib/asn1/timestamp.rb new file mode 100644 index 000000000..6dd65e4db --- /dev/null +++ b/app/lib/asn1/timestamp.rb @@ -0,0 +1,25 @@ +class ASN1::Timestamp + ## Poor man’s rfc3161 timestamp decoding + # This works, as of 2019-05, for timestamps delivered by the universign POST api. + # We should properly access the ASN1 contents using the sequence and tags structure. + # However: + # * It’s hard to do right. + # * We currently don’t require it for proper operation; timestamps are never exposed to users. + # * There’s an ongoing PR https://github.com/ruby/openssl/pull/204 for proper timestamp decoding in the ruby openssl library; let’s use OpenSSL::TS once it exists. + + def self.timestampInfo(asn1timestamp) + asn1 = OpenSSL::ASN1.decode(asn1timestamp) + tstInfo = OpenSSL::ASN1.decode(asn1.value[1].value[0].value[2].value[1].value[0].value) + tstInfo + end + + def self.signature_time(asn1timestamp) + tstInfo = timestampInfo(asn1timestamp) + tstInfo.value[4].value + end + + def self.signed_digest(asn1timestamp) + tstInfo = timestampInfo(asn1timestamp) + tstInfo.value[2].value[1].value.unpack1('H*') + end +end diff --git a/spec/fixtures/files/bill_signature/signature.der b/spec/fixtures/files/bill_signature/signature.der new file mode 100644 index 0000000000000000000000000000000000000000..9adcc9a7e05eee7338a8c5d92edc781a19c02da8 GIT binary patch literal 2186 zcmd5-dpJ~S7@u?I7&91Te=0A|7-tzzxVgO=ly=?_x@ghk#k6t zFLJm!V^l~LF2~600Y*-RAtI^;s3b~c4i}0gA~2*v02pLNQhy7jXhTRj2I(Ofq=P3k zVF)6rS(6|Hn($MBVSs*g8KGAh3-jZ@9zIPE;0#$ggoot++~~4kiEO?`hJL}M_^=v0 zlW>0gXm{9L-fXTSljB2E8Sv#-V+J%PKplY{3!wHazy!7|rrioa#J^X*sWKgGK<5A} z5-}Jd(&)j0gtS;uYC@cVzAPa*HdP#zoRT1jQ=W_IfXPN@0V5?;okjxyFzI$IN0jM^ zvh9=+8>IsiWmb)*?adcZw_HN% zIJxUTsQw?C`S-`n29|%aV){?4c)HVZX6=EAJ{9K|1&&M>&I4GXPi5Lq3qKvhxU9^k zvxG~Kh(TIP9jd_?1QDdpgovJzz=!jbcDU;7sqR}nPwK|bjHzEZsB0JK)*PSHUzm`a zws@DvkW1(F1s45Re(L)z-Rs2loZ>F))h9*1C+%J=F;P(BGgdsUDsA4^aH%x@L8tkH z2D}C6X||E1Y3Ht1>(9r!l3mBwS6QYLBv$4U<}s&(GbfkTGhSamL&o;Y zgF9P#T~?Xvls2igWTX>XItK7omM}%!z&zkw?pW@SWB(Q$E#iNKeSb4ToEjA& z5(>o;N?qCFP@t)-qXj`z2ttHavw)s5wot|zKx!DU+RtCH%G>aG;t zj$^5HJN9!c8iO3~#BGtwy{?6#82lr^;5OU|fn@t9^N+uGwrOhv-x4!$-+gCx6e3Ff zV2c38`y7IrA#~0f!)I!HYh)!u8=|LpMM2)@WNyx(%)$HB0-IY&^j$#GFs%Trfl8`? zQyQaIiN1g66 zc9k1T&a&HFW4}l*F(Z%d*BI$@VAQA9MvxeG`wAN9-q~Ip;4yHZv2X#HU@kAZA3*kT zuuSZc=kdl*N9;9R)M-1_P*EDaHB;PY;AWP`XAL>E@AhxIV9l6s8<`|)rS7xtG1;%3 tVzE)@hIu}rO8!l1uNJvrhITX`9=2p( Date: Thu, 6 Jun 2019 10:47:51 +0200 Subject: [PATCH 07/12] Add Universign timestamp API query --- app/lib/universign/api.rb | 40 ++++++++++++++++++++++++ config/env.example | 3 ++ config/initializers/urls.rb | 1 + config/secrets.yml | 4 +++ spec/fixtures/cassettes/universign.yml | 43 ++++++++++++++++++++++++++ spec/lib/universign/api_spec.rb | 11 +++++++ 6 files changed, 102 insertions(+) create mode 100644 app/lib/universign/api.rb create mode 100644 spec/fixtures/cassettes/universign.yml create mode 100644 spec/lib/universign/api_spec.rb diff --git a/app/lib/universign/api.rb b/app/lib/universign/api.rb new file mode 100644 index 000000000..eeb987ca1 --- /dev/null +++ b/app/lib/universign/api.rb @@ -0,0 +1,40 @@ +class Universign::API + ## Universign Timestamp POST API + # Official documentation is at https://help.universign.com/hc/fr/articles/360000898965-Guide-d-intégration-horodatage + + def self.ensure_properly_configured! + if userpwd.blank? + raise StandardError, 'Universign API is not properly configured' + end + end + + def self.timestamp(data) + ensure_properly_configured! + + response = Typhoeus.post( + UNIVERSIGN_API_URL, + userpwd: userpwd, + body: body(data) + ) + + if response.success? + response.body + else + raise StandardError, "Universign timestamp query failed: #{response.status_message}" + end + end + + private + + def self.body(data) + { + 'hashAlgo': 'SHA256', + 'withCert': 'true', + 'hashValue': data + } + end + + def self.userpwd + Rails.application.secrets.universign[:userpwd] + end +end diff --git a/config/env.example b/config/env.example index b1aa41f94..84fd7aeb7 100644 --- a/config/env.example +++ b/config/env.example @@ -66,3 +66,6 @@ TRUSTED_NETWORKS="" SKYLIGHT_AUTHENTICATION_KEY="" LOGRAGE_ENABLED="disabled" + +UNIVERSIGN_API_URL="" +UNIVERSIGN_USERPWD="" diff --git a/config/initializers/urls.rb b/config/initializers/urls.rb index 92a27a505..585f11740 100644 --- a/config/initializers/urls.rb +++ b/config/initializers/urls.rb @@ -7,6 +7,7 @@ API_GEO_SANDBOX_URL = ENV.fetch("API_GEO_SANDBOX_URL", "https://sandbox.geo.api. HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2") PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1") SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2") +UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/tsa/post/") # Internal URLs FOG_BASE_URL = "https://static.demarches-simplifiees.fr" diff --git a/config/secrets.yml b/config/secrets.yml index 9a74a71cc..7f3166b96 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -64,6 +64,8 @@ defaults: &defaults crisp: enabled: <%= ENV['CRISP_ENABLED'] == 'enabled' %> client_key: <%= ENV['CRISP_CLIENT_KEY'] %> + universign: + userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %> @@ -90,6 +92,8 @@ test: token_endpoint: https://bidon.com/endpoint userinfo_endpoint: https://bidon.com/endpoint logout_endpoint: https://bidon.com/endpoint + universign: + userpwd: 'fake:fake' # Do not keep production secrets in the repository, # instead read values from the environment. diff --git a/spec/fixtures/cassettes/universign.yml b/spec/fixtures/cassettes/universign.yml new file mode 100644 index 000000000..3f84c27d9 --- /dev/null +++ b/spec/fixtures/cassettes/universign.yml @@ -0,0 +1,43 @@ +--- +http_interactions: +- request: + method: post + uri: https://ws.universign.eu/tsa/post/ + body: + encoding: UTF-8 + string: hashAlgo=SHA256&hashValue=d28d6c7742c6c77025a104b81750ce8dc5f7dba2d01d9b5f4cad828f02b2324b&withCert=true + headers: + User-Agent: + - demarches-simplifiees.fr + Expect: + - '' + Authorization: + - Basic cmllbiBpY2kK= + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 17 May 2019 10:05:12 GMT + Server: + - Apache + Cache-Control: + - no-cache + Pragma: + - no-cache + Content-Type: + - application/octet-stream + Set-Cookie: + - CGSESSIONID=A3D72C1B0D07C073CFE68597B3FBF1B6E639C351;Path=/;Version=0 + X-Ua-Compatible: + - IE=Edge + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: !binary |- + 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 + http_version: + recorded_at: Fri, 17 May 2019 10:05:12 GMT +recorded_with: VCR 4.0.0 diff --git a/spec/lib/universign/api_spec.rb b/spec/lib/universign/api_spec.rb new file mode 100644 index 000000000..124076ba5 --- /dev/null +++ b/spec/lib/universign/api_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Universign::API do + describe '.request_timestamp', vcr: { cassette_name: 'universign' } do + subject { described_class.timestamp(digest) } + + let(:digest) { Digest::SHA256.hexdigest("CECI EST UN HASH") } + + it { is_expected.not_to be_nil } + end +end From f355f849a6740259338e7450af78dc721f716572 Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Mon, 17 Jun 2019 11:01:41 +0200 Subject: [PATCH 08/12] Add BillSignature Model --- app/models/bill_signature.rb | 84 ++++++++++ app/models/dossier_operation_log.rb | 1 + config/locales/models/bill_signature/fr.yml | 27 +++ .../20190616141702_create_bill_signature.rb | 10 ++ db/schema.rb | 11 +- spec/factories/bill_signature.rb | 6 + spec/factories/dossier_operation_log.rb | 5 + .../files/bill_signature/serialized.json | 1 + spec/models/bill_signature_spec.rb | 158 ++++++++++++++++++ 9 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 app/models/bill_signature.rb create mode 100644 config/locales/models/bill_signature/fr.yml create mode 100644 db/migrate/20190616141702_create_bill_signature.rb create mode 100644 spec/factories/bill_signature.rb create mode 100644 spec/factories/dossier_operation_log.rb create mode 100644 spec/fixtures/files/bill_signature/serialized.json create mode 100644 spec/models/bill_signature_spec.rb diff --git a/app/models/bill_signature.rb b/app/models/bill_signature.rb new file mode 100644 index 000000000..cef20c232 --- /dev/null +++ b/app/models/bill_signature.rb @@ -0,0 +1,84 @@ +class BillSignature < ApplicationRecord + has_many :dossier_operation_logs + + has_one_attached :serialized + has_one_attached :signature + + validate :check_bill_digest + validate :check_serialized_bill_contents + validate :check_signature_contents + + def self.build_with_operations(operations, day) + bill = new(dossier_operation_logs: operations) + + bill.serialize_operations(day) + + bill + end + + def serialize_operations(day) + self.serialized.attach( + io: StringIO.new(operations_bill_json), + filename: "demarches-simplifiees-operations-#{day.to_date.iso8601}.json", + content_type: 'application/json', + metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } + ) + + self.digest = operations_bill_digest + end + + def operations_bill + dossier_operation_logs.map { |op| [op.id.to_s, op.digest] }.to_h + end + + def operations_bill_json + operations_bill.to_json + end + + def operations_bill_digest + Digest::SHA256.hexdigest(operations_bill_json) + end + + def set_signature(signature, day) + self.signature.attach( + io: StringIO.new(signature), + filename: "demarches-simplifiees-signature-#{day.to_date.iso8601}.der", + content_type: 'application/x-x509-ca-cert' + ) + end + + # Validations + def check_bill_digest + if self.digest != self.operations_bill_digest + errors.add(:digest) + end + end + + def check_serialized_bill_contents + if !self.serialized.attached? + errors.add(:serialized, :blank) + return + end + + if JSON.parse(self.serialized.download) != self.operations_bill + errors.add(:serialized) + end + end + + def check_signature_contents + if !self.signature.attached? + errors.add(:signature, :blank) + return + end + + timestamp_signature_date = ASN1::Timestamp.signature_time(self.signature.download) + if timestamp_signature_date > Time.zone.now + errors.add(:signature, :invalid_date) + end + + timestamp_signed_digest = ASN1::Timestamp.signed_digest(self.signature.download) + if timestamp_signed_digest != self.digest + errors.add(:signature) + end + end +end diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index e517dcb36..a66492ace 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -12,6 +12,7 @@ class DossierOperationLog < ApplicationRecord belongs_to :dossier has_one_attached :serialized + belongs_to :bill_signature, optional: true def self.create_and_serialize(params) dossier = params.fetch(:dossier) diff --git a/config/locales/models/bill_signature/fr.yml b/config/locales/models/bill_signature/fr.yml new file mode 100644 index 000000000..8e7a1c744 --- /dev/null +++ b/config/locales/models/bill_signature/fr.yml @@ -0,0 +1,27 @@ +fr: + activerecord: + attributes: + bill_signature: + dossier_operation_logs: + one: opération + other: opérations + digest: empreinte + serialized: liasse + signature: signature + errors: + models: + bill_signature: + attributes: + digest: + invalid: 'ne correspond pas à la liasse' + serialized: + blank: 'doit être rempli' + invalid: 'ne correspond pas aux opérations' + signature: + blank: 'doit être rempli' + invalid: 'ne correspond pas à l’empreinte' + invalid_date: 'ne doit pas être dans le futur' + models: + bill_signature: + one: Horodatage + other: Horodatages diff --git a/db/migrate/20190616141702_create_bill_signature.rb b/db/migrate/20190616141702_create_bill_signature.rb new file mode 100644 index 000000000..95b0b9a2e --- /dev/null +++ b/db/migrate/20190616141702_create_bill_signature.rb @@ -0,0 +1,10 @@ +class CreateBillSignature < ActiveRecord::Migration[5.2] + def change + create_table :bill_signatures do |t| + t.string :digest + t.timestamps + end + + add_reference :dossier_operation_logs, :bill_signature, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 0a29eae67..f44e8df90 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_06_07_124156) do +ActiveRecord::Schema.define(version: 2019_06_16_141702) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -145,6 +145,12 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do t.index ["gestionnaire_id"], name: "index_avis_on_gestionnaire_id" end + create_table "bill_signatures", force: :cascade do |t| + t.string "digest" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "champs", id: :serial, force: :cascade do |t| t.string "value" t.integer "type_de_champ_id" @@ -224,7 +230,9 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do t.datetime "keep_until" t.datetime "executed_at" t.text "digest" + t.bigint "bill_signature_id" t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_id" + t.index ["bill_signature_id"], name: "index_dossier_operation_logs_on_bill_signature_id" t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id" t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id" t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until" @@ -617,6 +625,7 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do add_foreign_key "closed_mails", "procedures" add_foreign_key "commentaires", "dossiers" add_foreign_key "dossier_operation_logs", "administrations" + add_foreign_key "dossier_operation_logs", "bill_signatures" add_foreign_key "dossier_operation_logs", "dossiers" add_foreign_key "dossier_operation_logs", "gestionnaires" add_foreign_key "dossiers", "users" diff --git a/spec/factories/bill_signature.rb b/spec/factories/bill_signature.rb new file mode 100644 index 000000000..363aafb8f --- /dev/null +++ b/spec/factories/bill_signature.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :bill_signature do + serialized { Rack::Test::UploadedFile.new("./spec/fixtures/files/bill_signature/serialized.json", 'application/json') } + signature { Rack::Test::UploadedFile.new("./spec/fixtures/files/bill_signature/signature.der", 'application/x-x509-ca-cert') } + end +end diff --git a/spec/factories/dossier_operation_log.rb b/spec/factories/dossier_operation_log.rb new file mode 100644 index 000000000..ff6a80a53 --- /dev/null +++ b/spec/factories/dossier_operation_log.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :dossier_operation_log do + operation { :passer_en_instruction } + end +end diff --git a/spec/fixtures/files/bill_signature/serialized.json b/spec/fixtures/files/bill_signature/serialized.json new file mode 100644 index 000000000..87852f06f --- /dev/null +++ b/spec/fixtures/files/bill_signature/serialized.json @@ -0,0 +1 @@ +{"dossier1": "hash1", "dossier2": "hash2"} diff --git a/spec/models/bill_signature_spec.rb b/spec/models/bill_signature_spec.rb new file mode 100644 index 000000000..1456a97e4 --- /dev/null +++ b/spec/models/bill_signature_spec.rb @@ -0,0 +1,158 @@ +require 'rails_helper' + +RSpec.describe BillSignature, type: :model do + describe 'validations' do + describe 'check_bill_digest' do + before do + subject.dossier_operation_logs = dossier_operation_logs + subject.digest = digest + subject.valid? + end + + context 'no operations' do + let(:dossier_operation_logs) { [] } + + context 'correct digest' do + let(:digest) { Digest::SHA256.hexdigest('{}') } + + it { expect(subject.errors.details[:digest]).to be_empty } + end + + context 'bad digest' do + let(:digest) { 'baadf00d' } + + it { expect(subject.errors.details[:digest]).to eq [error: :invalid] } + end + end + + context 'operations set, good digest' do + let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] } + + context 'correct digest' do + let(:digest) { Digest::SHA256.hexdigest('{"1234":"abcd"}') } + + it { expect(subject.errors.details[:digest]).to be_empty } + end + + context 'bad digest' do + let(:digest) { 'baadf00d' } + + it { expect(subject.errors.details[:digest]).to eq [error: :invalid] } + end + end + end + + describe 'check_serialized_bill_contents' do + before do + subject.dossier_operation_logs = dossier_operation_logs + subject.serialized.attach(io: StringIO.new(serialized), filename: 'file') if serialized.present? + subject.valid? + end + + context 'no operations' do + let(:dossier_operation_logs) { [] } + let(:serialized) { '{}' } + + it { expect(subject.errors.details[:serialized]).to be_empty } + end + + context 'operations set' do + let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] } + let(:serialized) { '{"1234":"abcd"}' } + + it { expect(subject.errors.details[:serialized]).to be_empty } + end + + context 'serialized not set' do + let(:dossier_operation_logs) { [] } + let(:serialized) { nil } + + it { expect(subject.errors.details[:serialized]).to eq [error: :blank] } + end + end + + describe 'check_signature_contents' do + before do + subject.signature.attach(io: StringIO.new(signature), filename: 'file') if signature.present? + allow(ASN1::Timestamp).to receive(:signature_time).and_return(signature_time) + allow(ASN1::Timestamp).to receive(:signed_digest).and_return(signed_digest) + subject.digest = digest + subject.valid? + end + + context 'correct signature' do + let(:signature) { 'signature' } + let(:signature_time) { 1.day.ago } + let(:digest) { 'abcd' } + let(:signed_digest) { 'abcd' } + + it { expect(subject.errors.details[:signature]).to be_empty } + end + + context 'signature not set' do + let(:signature) { nil } + let(:signature_time) { 1.day.ago } + let(:digest) { 'abcd' } + let(:signed_digest) { 'abcd' } + + it { expect(subject.errors.details[:signature]).to eq [error: :blank] } + end + + context 'wrong signature time' do + let(:signature) { 'signature' } + let(:signature_time) { 1.day.from_now } + let(:digest) { 'abcd' } + let(:signed_digest) { 'abcd' } + + it { expect(subject.errors.details[:signature]).to eq [error: :invalid_date] } + end + + context 'wrong signature digest' do + let(:signature) { 'signature' } + let(:signature_time) { 1.day.ago } + let(:digest) { 'abcd' } + let(:signed_digest) { 'dcba' } + + it { expect(subject.errors.details[:signature]).to eq [error: :invalid] } + end + end + end + + describe '.build_with_operations' do + subject { described_class.build_with_operations(dossier_operation_logs, Date.new(1871, 03, 18)) } + + context 'no operations' do + let(:dossier_operation_logs) { [] } + + it { expect(subject.operations_bill).to eq({}) } + it { expect(subject.digest).to eq(Digest::SHA256.hexdigest('{}')) } + it { expect(subject.serialized.download).to eq('{}') } + it { expect(subject.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } + end + + context 'one operation' do + let(:dossier_operation_logs) do + [build(:dossier_operation_log, id: '1234', digest: 'abcd')] + end + + it { expect(subject.operations_bill).to eq({ '1234' => 'abcd' }) } + it { expect(subject.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd"}')) } + it { expect(subject.serialized.download).to eq('{"1234":"abcd"}') } + it { expect(subject.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } + end + + context 'several operations' do + let(:dossier_operation_logs) do + [ + build(:dossier_operation_log, id: '1234', digest: 'abcd'), + build(:dossier_operation_log, id: '5678', digest: 'dcba') + ] + end + + it { expect(subject.operations_bill).to eq({ '1234' => 'abcd', '5678' => 'dcba' }) } + it { expect(subject.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd","5678":"dcba"}')) } + it { expect(subject.serialized.download).to eq('{"1234":"abcd","5678":"dcba"}') } + it { expect(subject.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } + end + end +end From ad3553f0be03a6b0581747243968ee233305c2f0 Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Tue, 14 May 2019 15:19:25 +0200 Subject: [PATCH 09/12] Add BillSignature Service --- app/services/bill_signature_service.rb | 16 +++++++++ spec/services/bill_signature_service_spec.rb | 37 ++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 app/services/bill_signature_service.rb create mode 100644 spec/services/bill_signature_service_spec.rb diff --git a/app/services/bill_signature_service.rb b/app/services/bill_signature_service.rb new file mode 100644 index 000000000..f8640a4b1 --- /dev/null +++ b/app/services/bill_signature_service.rb @@ -0,0 +1,16 @@ +class BillSignatureService + def self.grouped_unsigned_operation_until(date) + unsigned_operations = DossierOperationLog + .where(bill_signature: nil) + .where('executed_at < ?', date) + + unsigned_operations.group_by { |e| e.executed_at.to_date } + end + + def self.sign_operations(operations, day) + bill = BillSignature.build_with_operations(operations, day) + signature = Universign::API.timestamp(bill.digest) + bill.set_signature(signature, day) + bill.save! + end +end diff --git a/spec/services/bill_signature_service_spec.rb b/spec/services/bill_signature_service_spec.rb new file mode 100644 index 000000000..2a88478df --- /dev/null +++ b/spec/services/bill_signature_service_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe BillSignatureService do + describe ".grouped_unsigned_operation_until" do + subject { BillSignatureService.grouped_unsigned_operation_until(date).length } + + let(:date) { Date.today } + + context "when operations of several days need to be signed" do + before do + create :dossier_operation_log, executed_at: 3.days.ago + create :dossier_operation_log, executed_at: 2.days.ago + create :dossier_operation_log, executed_at: 1.day.ago + end + + it { is_expected.to eq 3 } + end + + context "when operations on a single day need to be signed" do + before do + create :dossier_operation_log, executed_at: 1.day.ago + create :dossier_operation_log, executed_at: 1.day.ago + end + + it { is_expected.to eq 1 } + end + + context "when there are no operations to be signed" do + before do + create :dossier_operation_log, created_at: 1.day.ago, bill_signature: build(:bill_signature) + create :dossier_operation_log, created_at: 1.day.from_now + end + + it { is_expected.to eq 0 } + end + end +end From 925edb01c7ef5ea810c2ad424758366941b76e2b Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Tue, 4 Jun 2019 10:37:05 +0200 Subject: [PATCH 10/12] Add OperationsSignatureJob --- README.md | 1 + app/jobs/operations_signature_job.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 app/jobs/operations_signature_job.rb diff --git a/README.md b/README.md index e8267b183..f113289d4 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later + OperationsSignatureJob.set(cron: "0 6 * * *").perform_later ### Voir les emails envoyés en local diff --git a/app/jobs/operations_signature_job.rb b/app/jobs/operations_signature_job.rb new file mode 100644 index 000000000..092aec740 --- /dev/null +++ b/app/jobs/operations_signature_job.rb @@ -0,0 +1,15 @@ +class OperationsSignatureJob < ApplicationJob + queue_as :cron + + def perform(*args) + last_midnight = Time.zone.today.beginning_of_day + operations_by_day = BillSignatureService.grouped_unsigned_operation_until(last_midnight) + operations_by_day.each do |day, operations| + begin + BillSignatureService.sign_operations(operations, day) + rescue + raise # let errors show up on Sentry and delayed_jobs + end + end + end +end From eb592f8ddf4b6b5577b64c5b9e388178730dc613 Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Fri, 17 May 2019 10:43:50 +0200 Subject: [PATCH 11/12] Add manager controller for bill signatures --- .../manager/bill_signatures_controller.rb | 4 ++ app/dashboards/bill_signature_dashboard.rb | 22 ++++++ app/fields/attachment_field.rb | 11 +++ .../fields/attachment_field/_index.html.haml | 1 + .../bill_signatures/_collection.html.erb | 68 +++++++++++++++++++ config/locales/models/bill_signature/fr.yml | 7 ++ config/routes.rb | 2 + 7 files changed, 115 insertions(+) create mode 100644 app/controllers/manager/bill_signatures_controller.rb create mode 100644 app/dashboards/bill_signature_dashboard.rb create mode 100644 app/fields/attachment_field.rb create mode 100644 app/views/fields/attachment_field/_index.html.haml create mode 100644 app/views/manager/bill_signatures/_collection.html.erb diff --git a/app/controllers/manager/bill_signatures_controller.rb b/app/controllers/manager/bill_signatures_controller.rb new file mode 100644 index 000000000..8e454c083 --- /dev/null +++ b/app/controllers/manager/bill_signatures_controller.rb @@ -0,0 +1,4 @@ +module Manager + class BillSignaturesController < Manager::ApplicationController + end +end diff --git a/app/dashboards/bill_signature_dashboard.rb b/app/dashboards/bill_signature_dashboard.rb new file mode 100644 index 000000000..91e5d7d1f --- /dev/null +++ b/app/dashboards/bill_signature_dashboard.rb @@ -0,0 +1,22 @@ +require "administrate/base_dashboard" + +class BillSignatureDashboard < Administrate::BaseDashboard + ATTRIBUTE_TYPES = { + dossier_operation_logs: Field::HasMany, + id: Field::Number, + digest: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime, + serialized: AttachmentField, + signature: AttachmentField + }.freeze + + COLLECTION_ATTRIBUTES = [ + :id, + :created_at, + :dossier_operation_logs, + :digest, + :serialized, + :signature + ].freeze +end diff --git a/app/fields/attachment_field.rb b/app/fields/attachment_field.rb new file mode 100644 index 000000000..2022fb355 --- /dev/null +++ b/app/fields/attachment_field.rb @@ -0,0 +1,11 @@ +require "administrate/field/base" + +class AttachmentField < Administrate::Field::Base + def to_s + data.filename.to_s + end + + def blob_path + Rails.application.routes.url_helpers.rails_blob_path(data) + end +end diff --git a/app/views/fields/attachment_field/_index.html.haml b/app/views/fields/attachment_field/_index.html.haml new file mode 100644 index 000000000..d29687a35 --- /dev/null +++ b/app/views/fields/attachment_field/_index.html.haml @@ -0,0 +1 @@ += link_to(field.to_s, field.blob_path) diff --git a/app/views/manager/bill_signatures/_collection.html.erb b/app/views/manager/bill_signatures/_collection.html.erb new file mode 100644 index 000000000..aea9f28c7 --- /dev/null +++ b/app/views/manager/bill_signatures/_collection.html.erb @@ -0,0 +1,68 @@ +<%# +# Collection + +This partial is used on the `index` and `show` pages +to display a collection of resources in an HTML table. + +## Local variables: + +- `collection_presenter`: + An instance of [Administrate::Page::Collection][1]. + The table presenter uses `ResourceDashboard::COLLECTION_ATTRIBUTES` to determine + the columns displayed in the table +- `resources`: + An ActiveModel::Relation collection of resources to be displayed in the table. + By default, the number of resources is limited by pagination + or by a hard limit to prevent excessive page load times + +[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection +%> + + + + + <% collection_presenter.attribute_types.each do |attr_name, attr_type| %> + + <% end %> + <% [valid_action?(:edit, collection_presenter.resource_name), + valid_action?(:destroy, collection_presenter.resource_name)].count(true).times do %> + + <% end %> + + + + + <% resources.each do |resource| %> + + <% collection_presenter.attributes_for(resource).each do |attribute| %> + + <% end %> + + <% end %> + +
+ <%= link_to(sanitized_order_params(page, collection_field_name).merge( + collection_presenter.order_params_for(attr_name, key: collection_field_name) + )) do %> + <%= t( + "helpers.label.#{collection_presenter.resource_name}.#{attr_name}", + default: attr_name.to_s, + ).titleize %> + <% if collection_presenter.ordered_by?(attr_name) %> + + + + <% end %> + <% end %> +
+ <%= render_field attribute %> +
diff --git a/config/locales/models/bill_signature/fr.yml b/config/locales/models/bill_signature/fr.yml index 8e7a1c744..b614025fe 100644 --- a/config/locales/models/bill_signature/fr.yml +++ b/config/locales/models/bill_signature/fr.yml @@ -25,3 +25,10 @@ fr: bill_signature: one: Horodatage other: Horodatages + helpers: + label: + bill_signature: + dossier_operation_logs: opérations + digest: empreinte + serialized: liasse + signature: signature diff --git a/config/routes.rb b/config/routes.rb index da1c66ecc..e6006507c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,6 +37,8 @@ Rails.application.routes.draw do resources :demandes, only: [:index] + resources :bill_signatures, only: [:index] + resources :services, only: [:index, :show] post 'demandes/create_administrateur' From c6066449020471b563af7c173dbaddbb6aa4745e Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Mon, 17 Jun 2019 14:38:17 +0200 Subject: [PATCH 12/12] Tweak codestyle in specs, following review --- spec/models/bill_signature_spec.rb | 100 +++++++++++++++-------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/spec/models/bill_signature_spec.rb b/spec/models/bill_signature_spec.rb index 1456a97e4..30157cbfe 100644 --- a/spec/models/bill_signature_spec.rb +++ b/spec/models/bill_signature_spec.rb @@ -2,146 +2,148 @@ require 'rails_helper' RSpec.describe BillSignature, type: :model do describe 'validations' do + subject(:bill_signature) { BillSignature.new } + describe 'check_bill_digest' do before do - subject.dossier_operation_logs = dossier_operation_logs - subject.digest = digest - subject.valid? + bill_signature.dossier_operation_logs = dossier_operation_logs + bill_signature.digest = digest + bill_signature.valid? end - context 'no operations' do + context 'when there are no operations' do let(:dossier_operation_logs) { [] } - context 'correct digest' do + context 'when the digest is correct' do let(:digest) { Digest::SHA256.hexdigest('{}') } - it { expect(subject.errors.details[:digest]).to be_empty } + it { expect(bill_signature.errors.details[:digest]).to be_empty } end - context 'bad digest' do + context 'when the digest is incorrect' do let(:digest) { 'baadf00d' } - it { expect(subject.errors.details[:digest]).to eq [error: :invalid] } + it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] } end end - context 'operations set, good digest' do + context 'when the signature has operations' do let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] } - context 'correct digest' do + context 'when the digest is correct' do let(:digest) { Digest::SHA256.hexdigest('{"1234":"abcd"}') } - it { expect(subject.errors.details[:digest]).to be_empty } + it { expect(bill_signature.errors.details[:digest]).to be_empty } end - context 'bad digest' do + context 'when the digest is incorrect' do let(:digest) { 'baadf00d' } - it { expect(subject.errors.details[:digest]).to eq [error: :invalid] } + it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] } end end end describe 'check_serialized_bill_contents' do before do - subject.dossier_operation_logs = dossier_operation_logs - subject.serialized.attach(io: StringIO.new(serialized), filename: 'file') if serialized.present? - subject.valid? + bill_signature.dossier_operation_logs = dossier_operation_logs + bill_signature.serialized.attach(io: StringIO.new(serialized), filename: 'file') if serialized.present? + bill_signature.valid? end - context 'no operations' do + context 'when there are no operations' do let(:dossier_operation_logs) { [] } let(:serialized) { '{}' } - it { expect(subject.errors.details[:serialized]).to be_empty } + it { expect(bill_signature.errors.details[:serialized]).to be_empty } end - context 'operations set' do + context 'when the signature has operations' do let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] } let(:serialized) { '{"1234":"abcd"}' } - it { expect(subject.errors.details[:serialized]).to be_empty } + it { expect(bill_signature.errors.details[:serialized]).to be_empty } end - context 'serialized not set' do + context 'when serialized isn’t set' do let(:dossier_operation_logs) { [] } let(:serialized) { nil } - it { expect(subject.errors.details[:serialized]).to eq [error: :blank] } + it { expect(bill_signature.errors.details[:serialized]).to eq [error: :blank] } end end describe 'check_signature_contents' do before do - subject.signature.attach(io: StringIO.new(signature), filename: 'file') if signature.present? + bill_signature.signature.attach(io: StringIO.new(signature), filename: 'file') if signature.present? allow(ASN1::Timestamp).to receive(:signature_time).and_return(signature_time) allow(ASN1::Timestamp).to receive(:signed_digest).and_return(signed_digest) - subject.digest = digest - subject.valid? + bill_signature.digest = digest + bill_signature.valid? end - context 'correct signature' do + context 'when the signature is correct' do let(:signature) { 'signature' } let(:signature_time) { 1.day.ago } let(:digest) { 'abcd' } let(:signed_digest) { 'abcd' } - it { expect(subject.errors.details[:signature]).to be_empty } + it { expect(bill_signature.errors.details[:signature]).to be_empty } end - context 'signature not set' do + context 'when the signature isn’t set' do let(:signature) { nil } let(:signature_time) { 1.day.ago } let(:digest) { 'abcd' } let(:signed_digest) { 'abcd' } - it { expect(subject.errors.details[:signature]).to eq [error: :blank] } + it { expect(bill_signature.errors.details[:signature]).to eq [error: :blank] } end - context 'wrong signature time' do + context 'when the signature time is in the future' do let(:signature) { 'signature' } let(:signature_time) { 1.day.from_now } let(:digest) { 'abcd' } let(:signed_digest) { 'abcd' } - it { expect(subject.errors.details[:signature]).to eq [error: :invalid_date] } + it { expect(bill_signature.errors.details[:signature]).to eq [error: :invalid_date] } end - context 'wrong signature digest' do + context 'when the signature doesn’t match the digest' do let(:signature) { 'signature' } let(:signature_time) { 1.day.ago } let(:digest) { 'abcd' } let(:signed_digest) { 'dcba' } - it { expect(subject.errors.details[:signature]).to eq [error: :invalid] } + it { expect(bill_signature.errors.details[:signature]).to eq [error: :invalid] } end end end describe '.build_with_operations' do - subject { described_class.build_with_operations(dossier_operation_logs, Date.new(1871, 03, 18)) } + subject(:bill_signature) { described_class.build_with_operations(dossier_operation_logs, Date.new(1871, 03, 18)) } - context 'no operations' do + context 'when there are no operations' do let(:dossier_operation_logs) { [] } - it { expect(subject.operations_bill).to eq({}) } - it { expect(subject.digest).to eq(Digest::SHA256.hexdigest('{}')) } - it { expect(subject.serialized.download).to eq('{}') } - it { expect(subject.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } + it { expect(bill_signature.operations_bill).to eq({}) } + it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{}')) } + it { expect(bill_signature.serialized.download).to eq('{}') } + it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } end - context 'one operation' do + context 'when there is one operation' do let(:dossier_operation_logs) do [build(:dossier_operation_log, id: '1234', digest: 'abcd')] end - it { expect(subject.operations_bill).to eq({ '1234' => 'abcd' }) } - it { expect(subject.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd"}')) } - it { expect(subject.serialized.download).to eq('{"1234":"abcd"}') } - it { expect(subject.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } + it { expect(bill_signature.operations_bill).to eq({ '1234' => 'abcd' }) } + it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd"}')) } + it { expect(bill_signature.serialized.download).to eq('{"1234":"abcd"}') } + it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } end - context 'several operations' do + context 'when there are several operations' do let(:dossier_operation_logs) do [ build(:dossier_operation_log, id: '1234', digest: 'abcd'), @@ -149,10 +151,10 @@ RSpec.describe BillSignature, type: :model do ] end - it { expect(subject.operations_bill).to eq({ '1234' => 'abcd', '5678' => 'dcba' }) } - it { expect(subject.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd","5678":"dcba"}')) } - it { expect(subject.serialized.download).to eq('{"1234":"abcd","5678":"dcba"}') } - it { expect(subject.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } + it { expect(bill_signature.operations_bill).to eq({ '1234' => 'abcd', '5678' => 'dcba' }) } + it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd","5678":"dcba"}')) } + it { expect(bill_signature.serialized.download).to eq('{"1234":"abcd","5678":"dcba"}') } + it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') } end end end