Merge pull request #3886 from betagouv/timestamp-dossier-operations
Timestamp operations
This commit is contained in:
commit
de79a9a8bd
29 changed files with 635 additions and 2 deletions
|
@ -10,5 +10,5 @@ indent_size = 2
|
||||||
indent_style = space
|
indent_style = space
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.{ico,keep,pdf,svg}]
|
[*.{ico,keep,pdf,svg,der}]
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
|
|
|
@ -70,6 +70,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
|
||||||
WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
|
WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
|
||||||
GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
|
GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
|
||||||
PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later
|
PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later
|
||||||
|
OperationsSignatureJob.set(cron: "0 6 * * *").perform_later
|
||||||
|
|
||||||
### Voir les emails envoyés en local
|
### Voir les emails envoyés en local
|
||||||
|
|
||||||
|
|
4
app/controllers/manager/bill_signatures_controller.rb
Normal file
4
app/controllers/manager/bill_signatures_controller.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Manager
|
||||||
|
class BillSignaturesController < Manager::ApplicationController
|
||||||
|
end
|
||||||
|
end
|
22
app/dashboards/bill_signature_dashboard.rb
Normal file
22
app/dashboards/bill_signature_dashboard.rb
Normal file
|
@ -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
|
11
app/fields/attachment_field.rb
Normal file
11
app/fields/attachment_field.rb
Normal file
|
@ -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
|
15
app/jobs/operations_signature_job.rb
Normal file
15
app/jobs/operations_signature_job.rb
Normal file
|
@ -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
|
25
app/lib/asn1/timestamp.rb
Normal file
25
app/lib/asn1/timestamp.rb
Normal file
|
@ -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
|
40
app/lib/universign/api.rb
Normal file
40
app/lib/universign/api.rb
Normal file
|
@ -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
|
84
app/models/bill_signature.rb
Normal file
84
app/models/bill_signature.rb
Normal file
|
@ -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
|
|
@ -12,6 +12,7 @@ class DossierOperationLog < ApplicationRecord
|
||||||
|
|
||||||
belongs_to :dossier
|
belongs_to :dossier
|
||||||
has_one_attached :serialized
|
has_one_attached :serialized
|
||||||
|
belongs_to :bill_signature, optional: true
|
||||||
|
|
||||||
def self.create_and_serialize(params)
|
def self.create_and_serialize(params)
|
||||||
dossier = params.fetch(:dossier)
|
dossier = params.fetch(:dossier)
|
||||||
|
|
16
app/services/bill_signature_service.rb
Normal file
16
app/services/bill_signature_service.rb
Normal file
|
@ -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
|
1
app/views/fields/attachment_field/_index.html.haml
Normal file
1
app/views/fields/attachment_field/_index.html.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
= link_to(field.to_s, field.blob_path)
|
68
app/views/manager/bill_signatures/_collection.html.erb
Normal file
68
app/views/manager/bill_signatures/_collection.html.erb
Normal file
|
@ -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
|
||||||
|
%>
|
||||||
|
|
||||||
|
<table aria-labelledby="<%= table_title %>">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
|
||||||
|
<th class="cell-label
|
||||||
|
cell-label--<%= attr_type.html_class %>
|
||||||
|
cell-label--<%= collection_presenter.ordered_html_class(attr_name) %>"
|
||||||
|
scope="col"
|
||||||
|
role="columnheader"
|
||||||
|
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>">
|
||||||
|
<%= 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) %>
|
||||||
|
<span class="cell-label__sort-indicator cell-label__sort-indicator--<%= collection_presenter.ordered_html_class(attr_name) %>">
|
||||||
|
<svg aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-up-caret" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</th>
|
||||||
|
<% end %>
|
||||||
|
<% [valid_action?(:edit, collection_presenter.resource_name),
|
||||||
|
valid_action?(:destroy, collection_presenter.resource_name)].count(true).times do %>
|
||||||
|
<th scope="col"></th>
|
||||||
|
<% end %>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% resources.each do |resource| %>
|
||||||
|
<tr class="js-table-row"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<% collection_presenter.attributes_for(resource).each do |attribute| %>
|
||||||
|
<td class="cell-data cell-data--<%= attribute.html_class %>">
|
||||||
|
<%= render_field attribute %>
|
||||||
|
</td>
|
||||||
|
<% end %>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -66,3 +66,6 @@ TRUSTED_NETWORKS=""
|
||||||
SKYLIGHT_AUTHENTICATION_KEY=""
|
SKYLIGHT_AUTHENTICATION_KEY=""
|
||||||
|
|
||||||
LOGRAGE_ENABLED="disabled"
|
LOGRAGE_ENABLED="disabled"
|
||||||
|
|
||||||
|
UNIVERSIGN_API_URL=""
|
||||||
|
UNIVERSIGN_USERPWD=""
|
||||||
|
|
|
@ -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")
|
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")
|
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")
|
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
|
# Internal URLs
|
||||||
FOG_BASE_URL = "https://static.demarches-simplifiees.fr"
|
FOG_BASE_URL = "https://static.demarches-simplifiees.fr"
|
||||||
|
|
34
config/locales/models/bill_signature/fr.yml
Normal file
34
config/locales/models/bill_signature/fr.yml
Normal file
|
@ -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
|
|
@ -37,6 +37,8 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :demandes, only: [:index]
|
resources :demandes, only: [:index]
|
||||||
|
|
||||||
|
resources :bill_signatures, only: [:index]
|
||||||
|
|
||||||
resources :services, only: [:index, :show]
|
resources :services, only: [:index, :show]
|
||||||
|
|
||||||
post 'demandes/create_administrateur'
|
post 'demandes/create_administrateur'
|
||||||
|
|
|
@ -64,6 +64,8 @@ defaults: &defaults
|
||||||
crisp:
|
crisp:
|
||||||
enabled: <%= ENV['CRISP_ENABLED'] == 'enabled' %>
|
enabled: <%= ENV['CRISP_ENABLED'] == 'enabled' %>
|
||||||
client_key: <%= ENV['CRISP_CLIENT_KEY'] %>
|
client_key: <%= ENV['CRISP_CLIENT_KEY'] %>
|
||||||
|
universign:
|
||||||
|
userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,6 +92,8 @@ test:
|
||||||
token_endpoint: https://bidon.com/endpoint
|
token_endpoint: https://bidon.com/endpoint
|
||||||
userinfo_endpoint: https://bidon.com/endpoint
|
userinfo_endpoint: https://bidon.com/endpoint
|
||||||
logout_endpoint: https://bidon.com/endpoint
|
logout_endpoint: https://bidon.com/endpoint
|
||||||
|
universign:
|
||||||
|
userpwd: 'fake:fake'
|
||||||
|
|
||||||
# Do not keep production secrets in the repository,
|
# Do not keep production secrets in the repository,
|
||||||
# instead read values from the environment.
|
# instead read values from the environment.
|
||||||
|
|
10
db/migrate/20190616141702_create_bill_signature.rb
Normal file
10
db/migrate/20190616141702_create_bill_signature.rb
Normal file
|
@ -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
|
11
db/schema.rb
11
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
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"
|
t.index ["gestionnaire_id"], name: "index_avis_on_gestionnaire_id"
|
||||||
end
|
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|
|
create_table "champs", id: :serial, force: :cascade do |t|
|
||||||
t.string "value"
|
t.string "value"
|
||||||
t.integer "type_de_champ_id"
|
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 "keep_until"
|
||||||
t.datetime "executed_at"
|
t.datetime "executed_at"
|
||||||
t.text "digest"
|
t.text "digest"
|
||||||
|
t.bigint "bill_signature_id"
|
||||||
t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_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 ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id"
|
||||||
t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_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"
|
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 "closed_mails", "procedures"
|
||||||
add_foreign_key "commentaires", "dossiers"
|
add_foreign_key "commentaires", "dossiers"
|
||||||
add_foreign_key "dossier_operation_logs", "administrations"
|
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", "dossiers"
|
||||||
add_foreign_key "dossier_operation_logs", "gestionnaires"
|
add_foreign_key "dossier_operation_logs", "gestionnaires"
|
||||||
add_foreign_key "dossiers", "users"
|
add_foreign_key "dossiers", "users"
|
||||||
|
|
6
spec/factories/bill_signature.rb
Normal file
6
spec/factories/bill_signature.rb
Normal file
|
@ -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
|
5
spec/factories/dossier_operation_log.rb
Normal file
5
spec/factories/dossier_operation_log.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :dossier_operation_log do
|
||||||
|
operation { :passer_en_instruction }
|
||||||
|
end
|
||||||
|
end
|
43
spec/fixtures/cassettes/universign.yml
vendored
Normal file
43
spec/fixtures/cassettes/universign.yml
vendored
Normal file
|
@ -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
|
1
spec/fixtures/files/bill_signature/serialized.json
vendored
Normal file
1
spec/fixtures/files/bill_signature/serialized.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"dossier1": "hash1", "dossier2": "hash2"}
|
BIN
spec/fixtures/files/bill_signature/signature.der
vendored
Normal file
BIN
spec/fixtures/files/bill_signature/signature.der
vendored
Normal file
Binary file not shown.
19
spec/lib/asn1/timestamp_spec.rb
Normal file
19
spec/lib/asn1/timestamp_spec.rb
Normal file
|
@ -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
|
11
spec/lib/universign/api_spec.rb
Normal file
11
spec/lib/universign/api_spec.rb
Normal file
|
@ -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
|
160
spec/models/bill_signature_spec.rb
Normal file
160
spec/models/bill_signature_spec.rb
Normal file
|
@ -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
|
37
spec/services/bill_signature_service_spec.rb
Normal file
37
spec/services/bill_signature_service_spec.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue