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/.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/CONTRIBUTING.md b/CONTRIBUTING.md index 6a51ae6d8..295a988ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,8 +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. -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, 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. 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/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/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 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/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/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/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/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/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/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..3449517ec 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 :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, dependent: :destroy + has_many :avis, inverse_of: :dossier, dependent: :destroy has_many :dossier_operation_logs, dependent: :destroy 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/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/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/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 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/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/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/locales/models/bill_signature/fr.yml b/config/locales/models/bill_signature/fr.yml new file mode 100644 index 000000000..b614025fe --- /dev/null +++ b/config/locales/models/bill_signature/fr.yml @@ -0,0 +1,34 @@ +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 + 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' 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/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/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/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/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) 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/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/fixtures/files/bill_signature/signature.der b/spec/fixtures/files/bill_signature/signature.der new file mode 100644 index 000000000..9adcc9a7e Binary files /dev/null and b/spec/fixtures/files/bill_signature/signature.der differ diff --git a/spec/lib/asn1/timestamp_spec.rb b/spec/lib/asn1/timestamp_spec.rb new file mode 100644 index 000000000..3ae0d6272 --- /dev/null +++ b/spec/lib/asn1/timestamp_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe ASN1::Timestamp do + let(:asn1timestamp) { File.read('spec/fixtures/files/bill_signature/signature.der') } + + describe '.timestamp_time' do + subject { described_class.signature_time(asn1timestamp) } + + it { is_expected.to eq Time.zone.parse('2019-04-30 15:30:20 UTC') } + end + + describe '.timestamp_signed_data' do + subject { described_class.signed_digest(asn1timestamp) } + + let(:data) { Digest::SHA256.hexdigest('CECI EST UN BLOB') } + + it { is_expected.to eq data } + end +end 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 diff --git a/spec/models/bill_signature_spec.rb b/spec/models/bill_signature_spec.rb new file mode 100644 index 000000000..30157cbfe --- /dev/null +++ b/spec/models/bill_signature_spec.rb @@ -0,0 +1,160 @@ +require 'rails_helper' + +RSpec.describe BillSignature, type: :model do + describe 'validations' do + subject(:bill_signature) { BillSignature.new } + + describe 'check_bill_digest' do + before do + bill_signature.dossier_operation_logs = dossier_operation_logs + bill_signature.digest = digest + bill_signature.valid? + end + + context 'when there are no operations' do + let(:dossier_operation_logs) { [] } + + context 'when the digest is correct' do + let(:digest) { Digest::SHA256.hexdigest('{}') } + + it { expect(bill_signature.errors.details[:digest]).to be_empty } + end + + context 'when the digest is incorrect' do + let(:digest) { 'baadf00d' } + + it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] } + end + end + + context 'when the signature has operations' do + let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] } + + context 'when the digest is correct' do + let(:digest) { Digest::SHA256.hexdigest('{"1234":"abcd"}') } + + it { expect(bill_signature.errors.details[:digest]).to be_empty } + end + + context 'when the digest is incorrect' do + let(:digest) { 'baadf00d' } + + it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] } + end + end + end + + describe 'check_serialized_bill_contents' do + before do + 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 'when there are no operations' do + let(:dossier_operation_logs) { [] } + let(:serialized) { '{}' } + + it { expect(bill_signature.errors.details[:serialized]).to be_empty } + end + + 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(bill_signature.errors.details[:serialized]).to be_empty } + end + + context 'when serialized isn’t set' do + let(:dossier_operation_logs) { [] } + let(:serialized) { nil } + + it { expect(bill_signature.errors.details[:serialized]).to eq [error: :blank] } + end + end + + describe 'check_signature_contents' do + before do + 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) + bill_signature.digest = digest + bill_signature.valid? + end + + 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(bill_signature.errors.details[:signature]).to be_empty } + end + + 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(bill_signature.errors.details[:signature]).to eq [error: :blank] } + end + + 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(bill_signature.errors.details[:signature]).to eq [error: :invalid_date] } + end + + 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(bill_signature.errors.details[:signature]).to eq [error: :invalid] } + end + end + end + + describe '.build_with_operations' do + subject(:bill_signature) { described_class.build_with_operations(dossier_operation_logs, Date.new(1871, 03, 18)) } + + context 'when there are no operations' do + let(:dossier_operation_logs) { [] } + + 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 'when there is one operation' do + let(:dossier_operation_logs) do + [build(:dossier_operation_log, id: '1234', digest: 'abcd')] + end + + 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 'when there are 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(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 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 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"