Skip to content

Commit

Permalink
Merge pull request #469 from etalab/feature/dat-571-se-brancher-avec-…
Browse files Browse the repository at this point in the history
…lapi-jcop

Feature/dat 571 se brancher avec lapi jcop
  • Loading branch information
Samuelfaure authored Oct 28, 2024
2 parents 002fe5e + 11b0a0d commit a7ff009
Show file tree
Hide file tree
Showing 34 changed files with 588 additions and 31 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ gem 'kaminari'
gem 'factory_bot_rails'
gem 'faker'
gem 'faraday'
gem 'faraday-multipart'
gem 'faraday-net_http'
gem 'faraday-retry'
gem 'faraday-gzip'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ GEM
faraday-gzip (2.0.1)
faraday (>= 1.0)
zlib (~> 3.0)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.3.0)
net-http
faraday-retry (2.2.1)
Expand Down Expand Up @@ -327,6 +329,7 @@ GEM
msgpack (1.7.2)
multi_test (1.1.0)
multi_xml (0.6.0)
multipart-post (2.4.1)
nenv (0.3.0)
net-http (0.4.1)
uri
Expand Down Expand Up @@ -608,6 +611,7 @@ DEPENDENCIES
faker
faraday
faraday-gzip
faraday-multipart
faraday-net_http
faraday-retry
foreman
Expand Down
3 changes: 3 additions & 0 deletions app/assets/stylesheets/components/malware_badge.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.malware-badge {
display: inline-block;
}
18 changes: 0 additions & 18 deletions app/form_builders/authorization_request_form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,6 @@ def all_terms_not_accepted_error?(attribute)
end
end

def dsfr_file_field(attribute, opts = {})
opts[:class] ||= 'fr-upload-group'

existing_file_link = link_to_file(attribute)
required = opts[:required] && !existing_file_link

dsfr_input_group(attribute, opts) do
@template.safe_join(
[
label_with_hint(attribute, opts),
readonly? ? nil : file_field(attribute, class: 'fr-upload', autocomplete: 'off', required:, **enhance_input_options(opts).except(:class, :required)),
error_message(attribute),
existing_file_link,
].compact
)
end
end

def dsfr_scope(scope, opts = {})
disabled = opts.delete(:disabled)

Expand Down
14 changes: 13 additions & 1 deletion app/form_builders/dsfr_form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def dsfr_file_field(attribute, opts = {})
file_field(attribute, class: 'fr-upload', autocomplete: 'off', required:, **enhance_input_options(opts).except(:class, :required)),
error_message(attribute),
existing_file_link,
]
link_to_file(attribute)
].compact
)
end
end
Expand Down Expand Up @@ -102,6 +103,17 @@ def dsfr_select(attribute, choices, opts = {})
end
end

def dsfr_malware_badge(attribute, opts = {})
safety_state = attribute.malware_scan&.safety_state || 'unknown'

badge_class = I18n.t("malware_scan.badge_class.#{safety_state}")
label = I18n.t("malware_scan.label.#{safety_state}")

@template.content_tag(:p, class: "fr-badge fr-badge--#{badge_class} malware-badge #{opts[:class]}") do
label_value(label)
end
end

private

def dsfr_select_tag(attribute, choices, opts)
Expand Down
15 changes: 15 additions & 0 deletions app/interactors/run_malware_scan_on_attachments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class RunMalwareScanOnAttachments < ApplicationInteractor
def call
attachment_names.each do |attachment_name|
attachment = context.authorization_request.public_send(attachment_name)

MalwareScanJob.perform_later(attachment.id)
end
end

private

def attachment_names
context.authorization_request.form.authorization_request_class.documents
end
end
30 changes: 30 additions & 0 deletions app/jobs/malware_scan_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class MalwareScanJob < ApplicationJob
RETRY_WAIT_TIME = 5.minutes

retry_on Faraday::ServerError, wait: RETRY_WAIT_TIME, attempts: :infinite
retry_on Faraday::ConnectionFailed, wait: RETRY_WAIT_TIME, attempts: :infinite

def perform(attachment_id)
@attachment = ActiveStorage::Attachment.find_by(id: attachment_id)

return if @attachment.blank? || @attachment.blob.blank?

create_and_start_scan
end

private

def create_and_start_scan
MalwareScan.create!(uuid:, attachment: @attachment)

MalwareScanRetrieveStateJob.perform_later(uuid)
end

def uuid
scan[:uuid]
end

def scan
@scan ||= JeCliqueOuPasAPIClient.new.analyze(@attachment)
end
end
47 changes: 47 additions & 0 deletions app/jobs/malware_scan_retrieve_state_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class MalwareScanRetrieveStateJob < ApplicationJob
include KeepTrackOfJobAttempts

RETRY_WAIT_TIME = 1.minute
RETRIES = 60

class MalwareScanRetrieveStateJobError < StandardError; end

retry_on MalwareScanRetrieveStateJobError, wait: RETRY_WAIT_TIME, attempts: RETRIES
retry_on Faraday::ServerError, wait: RETRY_WAIT_TIME, attempts: RETRIES
retry_on Faraday::ConnectionFailed, wait: RETRY_WAIT_TIME, attempts: RETRIES

def perform(uuid)
@uuid = uuid

@result_analyze = api_client.result(@uuid)

raise MalwareScanRetrieveStateJobError if safety_state.blank?

malware_scan.update!(safety_state:, analyzed_at:)
rescue MalwareScanRetrieveStateJobError, Faraday::ServerError, Faraday::ConnectionFailed
malware_scan.update!(safety_state: :unknown, analyzed_at: Time.zone.now.to_i) if attempts == RETRIES
end

private

def malware_scan
@malware_scan ||= MalwareScan.find_by(uuid: @uuid)
end

def safety_state
case @result_analyze[:is_malware]
when true
:unsafe
when false
:safe
end
end

def analyzed_at
Time.zone.at(@result_analyze[:analyzed_at])
end

def api_client
@api_client ||= JeCliqueOuPasAPIClient.new
end
end
7 changes: 7 additions & 0 deletions app/models/concerns/active_storage_attachment_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ActiveStorageAttachmentExtension
extend ActiveSupport::Concern

included do
has_one :malware_scan, dependent: :destroy
end
end
8 changes: 8 additions & 0 deletions app/models/malware_scan.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class MalwareScan < ApplicationRecord
belongs_to :attachment, class_name: 'ActiveStorage::Attachment'

enum :safety_state, { pending: 0, safe: 1, unsafe: 2, unknown: 3 }

validates :uuid, presence: true, uniqueness: true
validates :safety_state, presence: true
end
3 changes: 2 additions & 1 deletion app/organizers/submit_authorization_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class SubmitAuthorizationRequest < ApplicationOrganizer

organize AssignParamsToAuthorizationRequest,
CreateAuthorizationRequestChangelog,
ExecuteAuthorizationRequestTransitionWithCallbacks
ExecuteAuthorizationRequestTransitionWithCallbacks,
RunMalwareScanOnAttachments

after do
context.authorization_request.save(context: context.save_context) ||
Expand Down
110 changes: 110 additions & 0 deletions app/services/je_clique_ou_pas_api_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
class JeCliqueOuPasAPIClient
def analyze(attachment)
@blob = attachment.blob

response = request_analyze_with_file
parsed_response = JSON.parse(response.body)

{
uuid: parsed_response['uuid'],
error: parsed_response['error']
}
end

def result(uuid)
response = request_results(uuid:)
parsed_response = JSON.parse(response.body)

{
is_malware: parsed_response['is_malware'],
analyzed_at: parsed_response['timestamp'],
error: parsed_response['error']
}
end

private

def request_analyze_with_file
with_temp_file do |file|
faraday_client(multipart: true).post(
analyze_url,
file_payload(file:),
headers(file:)
)
end
end

def request_results(uuid:)
faraday_client.get(results_url(uuid), nil, headers)
end

def with_temp_file
temp_file = create_temp_file
yield temp_file
ensure
temp_file.close
temp_file.unlink
end

def create_temp_file
Tempfile.new(temp_file_params).tap do |file|
file.binmode
file.write(@blob.download)
file.rewind
end
end

def temp_file_params
[@blob.filename.base, @blob.filename.extension_with_delimiter]
end

def file_payload(file:)
{ file: Faraday::Multipart::FilePart.new(file, @blob.content_type) }
end

def faraday_client(multipart: false)
Faraday.new(ssl: { cert_store: }) do |faraday|
if multipart
faraday.request :multipart
faraday.request :url_encoded
faraday.adapter Faraday.default_adapter
end
end
end

def headers(file: nil)
base_headers = { 'X-Auth-token': token }

return base_headers unless file

base_headers.merge(
'Content-Type': 'multipart/form-data',
'Content-Length': File.size(file).to_s,
'Transfer-Encoding': 'chunked'
)
end

def analyze_url
"#{host}/submit"
end

def results_url(uuid)
"#{host}/results/#{uuid}"
end

def host
Rails.application.credentials.je_clique_ou_pas[:host]
end

def token
Rails.application.credentials.je_clique_ou_pas[:token]
end

def cert_store
OpenSSL::X509::Store.new.tap { |store| store.add_cert(certificate) }
end

def certificate
OpenSSL::X509::Certificate.new(Rails.application.credentials.je_clique_ou_pas[:certificate])
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
<% end %>

<% if @authorization_request.respond_to?(:maquette_projet) && @authorization_request.maquette_projet.attached? %>
<p>
<div class="fr-mb-5v">
<strong>
<%= f.label_value(:maquette_projet) %> :
</strong>
<br />
<%= link_to @authorization_request.maquette_projet.filename, url_for(@authorization_request.maquette_projet), target: '_blank', class: %w[fr-link--icon-left fr-icon-file-pdf-fill] %>
</p>
<% if namespace?(:instruction) %>
<%= f.dsfr_malware_badge @authorization_request.maquette_projet, class: 'fr-ml-1w' %>
<% end %>
</div>
<% end %>

<% if @authorization_request.respond_to?(:date_prevue_mise_en_production) %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
<% end %>

<% if @authorization_request.cadre_juridique_document.attached? %>
<p>
<div>
<strong>
Document relatif au traitement :
</strong>
<br />
<%= link_to @authorization_request.cadre_juridique_document.filename, url_for(@authorization_request.cadre_juridique_document), target: '_blank', class: %w[fr-link--icon-left fr-icon-file-pdf-fill] %>
</p>
<% if namespace?(:instruction) %>
<%= f.dsfr_malware_badge @authorization_request.cadre_juridique_document, class: 'fr-ml-1w' %>
<% end %>
</div>
<% end %>
<% end %>
2 changes: 1 addition & 1 deletion config/credentials/development.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
N6qthRTcH/UHF7SjIX0glK/jyjnOfYvUTB+5oi/V/709FFP/Er24XaapswYUvCZDd5xCvtWvfkMm4Exl2Li4TYkcDuY1aIHcmc1XigcvMTffHD7HFriP/rKfSNex7qAZw7TlXSmrtqbKj0QhKePFPqS/PcotYaQIInapGkMwndwbusUc8dc5hcMi/lvWLZIrcNm+nI4gHOP3/zE9PsPDdIfCwgiRrlCNRf1IVExbaQDgEPMHsosrktXtSv4Yk1tDnJ9N14WnVsA47kiXAcGm24ULrWFR9VXj/Q6gGMqfZuM/xD2xz7rur4ZiAsJR/WZzInj0O8vRHZ/Ietv/3c8yE26JzyuNPrJRferTsvh5m898T0X9zFkkGB96fliA3KDNGkYjY7MOqQSN7PJ623ymBd3hdOD9WGpaTDSUUadsoRFR3yVqq8A4W5cBA/M1kF9Sp6dmLBbkAHdLpo9yYvDkhnjD5kujOp5L56Bn8Kh9i/8nU/xn8tKWbu+yFrkRuK1eQqbNVHg+9eNO+9X2veh1Ao1PkITEX2DjHiHqxyTi04NSh6/Sr22Daw5MObRBeIi/HtItOTRhNbrTa6z01w+s3xHbe2W+8eU4ie/smKkNo7Ydk8XKxeH7teB8HeTy3MjK+o+PZ9dDP6r5uuc/aga2wgWe7yoyMT+bQbTqZ2oKlgeqMxpLh0SHIMK+491qyIo4Bob8HDNq6QccyCsKwAsMSIYW1/9VQYgXGOG/587u3vquxD2Ifee6T9bGVktO95VIK98vugweTGfALnGeJDOEknUoYwLwdomEgixaM2FAjHlY7a5nVIU0eM7TC2yvGVJCyA==--4qKCoHap6TM0T1Ek--Mdhas1TZvBaKsUkG7c4nwQ==
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--3OgOYyjw719TRBIk--50OoKpf1k7uWV1NTk8EXTQ==
Loading

0 comments on commit a7ff009

Please sign in to comment.