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