From e16cb731c54c437582e0b18a6da21287926ebeca Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Tue, 14 May 2019 15:19:25 +0200 Subject: [PATCH 1/7] =?UTF-8?q?Add=20poor=20man=E2=80=99s=20ASN1=20parsing?= 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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