diff --git a/app/jobs/anti_virus_job.rb b/app/jobs/anti_virus_job.rb new file mode 100644 index 000000000..c53a246bc --- /dev/null +++ b/app/jobs/anti_virus_job.rb @@ -0,0 +1,20 @@ +class AntiVirusJob < ApplicationJob + include ActiveStorage::Downloading + + attr_reader :blob + + def perform(virus_scan) + @blob = ActiveStorage::Blob.find_by(key: virus_scan.blob_key) + + if @blob.present? + download_blob_to_tempfile do |file| + if ClamavService.safe_file?(file.path) + status = "safe" + else + status = "infected" + end + virus_scan.update(scanned_at: Time.now, status: status) + end + end + end +end diff --git a/app/models/champ.rb b/app/models/champ.rb index d9c32362a..91b8055c0 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -3,6 +3,7 @@ class Champ < ApplicationRecord belongs_to :type_de_champ, inverse_of: :champ has_many :commentaires has_one_attached :piece_justificative_file + has_one :virus_scan delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, to: :type_de_champ diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb index 83718ab29..4c4270609 100644 --- a/app/models/champs/piece_justificative_champ.rb +++ b/app/models/champs/piece_justificative_champ.rb @@ -1,2 +1,12 @@ class Champs::PieceJustificativeChamp < Champ + after_commit :create_virus_scan + + def create_virus_scan + if self.piece_justificative_file&.attachment&.blob.present? + VirusScan.where(champ: self).where.not(blob_key: self.piece_justificative_file.blob.key).delete_all + VirusScan.find_or_create_by!(champ: self, blob_key: self.piece_justificative_file.blob.key) do |virus_scan| + virus_scan.status = "pending" + end + end + end end diff --git a/app/models/virus_scan.rb b/app/models/virus_scan.rb new file mode 100644 index 000000000..f0a45ca31 --- /dev/null +++ b/app/models/virus_scan.rb @@ -0,0 +1,17 @@ +class VirusScan < ApplicationRecord + belongs_to :champ + + enum status: { + pending: 'pending', + safe: 'safe', + infected: 'infected', + } + + validates :champ_id, uniqueness: { scope: :blob_key } + + after_create :perform_scan + + def perform_scan + AntiVirusJob.perform_later(self) + end +end diff --git a/app/views/dossiers/_infos_dossier.html.haml b/app/views/dossiers/_infos_dossier.html.haml index a14e46c4b..5da92d8d8 100644 --- a/app/views/dossiers/_infos_dossier.html.haml +++ b/app/views/dossiers/_infos_dossier.html.haml @@ -47,9 +47,7 @@ - else Pas de dossier associé - elsif champ.type_champ == 'piece_justificative' - - pj = champ.piece_justificative_file - %a{ href: url_for(pj), target: '_blank' } - = pj.filename.to_s + = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: true } - elsif champ.type_champ == 'textarea' = simple_format(champ.decorate.value) - else diff --git a/app/views/new_gestionnaire/dossiers/_champs.html.haml b/app/views/new_gestionnaire/dossiers/_champs.html.haml index a0e203778..2cdda4cb3 100644 --- a/app/views/new_gestionnaire/dossiers/_champs.html.haml +++ b/app/views/new_gestionnaire/dossiers/_champs.html.haml @@ -36,8 +36,7 @@ %td.rich-text - pj = c.piece_justificative_file - if pj.attached? - %a{ href: url_for(pj), target: '_blank' } - = pj.filename.to_s + = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: c, user_can_upload: false } - else Pièce justificative non fournie - when "textarea" diff --git a/app/views/shared/champs/piece_justificative/_pj_link.html.haml b/app/views/shared/champs/piece_justificative/_pj_link.html.haml new file mode 100644 index 000000000..d808058a4 --- /dev/null +++ b/app/views/shared/champs/piece_justificative/_pj_link.html.haml @@ -0,0 +1,18 @@ +- pj = champ.piece_justificative_file +- if champ.virus_scan.present? + - if champ.virus_scan.safe? + = link_to pj.filename.to_s, url_for(pj), target: '_blank' + - else + = pj.filename.to_s + - if champ.virus_scan.pending? + (analyse antivirus en cours + = link_to "rafraichir", request.path + ) + - elsif champ.virus_scan.infected? + - if user_can_upload + (virus détecté, merci d'envoyer un autre fichier) + - else + (virus détecté, le téléchargement de ce fichier est bloqué) +- else + = link_to pj.filename.to_s, url_for(pj), target: '_blank' + (ce fichier n'a pas été analysé par notre antivirus, téléchargez-le avec précaution) diff --git a/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml b/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml index c541ffdda..ce4b05fa3 100644 --- a/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml +++ b/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml @@ -10,8 +10,7 @@ id: "champs_#{champ.id}", direct_upload: true - else - %a{ href: url_for(pj), target: '_blank' } - = pj.filename.to_s + = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: true } %br Modifier : = form.file_field :piece_justificative_file, diff --git a/config/environments/test.rb b/config/environments/test.rb index 380b51502..5043e3677 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -47,6 +47,7 @@ Rails.application.configure do } config.active_job.queue_adapter = :test + config.active_storage.service = :test # Raises error for missing translations # config.action_view.raise_on_missing_translations = true diff --git a/config/storage.yml b/config/storage.yml index 2c6762e0d..1f93f7323 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -1,3 +1,7 @@ local: service: Disk root: <%= Rails.root.join("storage") %> + +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> diff --git a/db/migrate/20180511124302_create_virus_scans.rb b/db/migrate/20180511124302_create_virus_scans.rb new file mode 100644 index 000000000..80e8e4380 --- /dev/null +++ b/db/migrate/20180511124302_create_virus_scans.rb @@ -0,0 +1,12 @@ +class CreateVirusScans < ActiveRecord::Migration[5.2] + def change + create_table :virus_scans do |t| + t.datetime :scanned_at + t.string :status + t.references :champ, index: true + t.string :blob_key + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e2471ac7a..1a351cd1d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -594,6 +594,16 @@ ActiveRecord::Schema.define(version: 2018_06_01_084546) do t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + create_table "virus_scans", force: :cascade do |t| + t.datetime "scanned_at" + t.string "status" + t.bigint "champ_id" + t.string "blob_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["champ_id"], name: "index_virus_scans_on_champ_id" + end + create_table "without_continuation_mails", id: :serial, force: :cascade do |t| t.text "body" t.string "subject" diff --git a/lib/tasks/2018_06_04_scan_pjs.rake b/lib/tasks/2018_06_04_scan_pjs.rake new file mode 100644 index 000000000..3ad0e15dc --- /dev/null +++ b/lib/tasks/2018_06_04_scan_pjs.rake @@ -0,0 +1,7 @@ +namespace :'2018_06_04_scan_pjs' do + task scan_all: :environment do + Champs::PieceJustificativeChamp.all.each do |pj_champ| + pj_champ.create_virus_scan + end + end +end diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb index 45b2637e6..41553b5b6 100644 --- a/spec/factories/champ.rb +++ b/spec/factories/champ.rb @@ -17,5 +17,15 @@ FactoryBot.define do trait :dossier_link do type_de_champ { create(:type_de_champ_dossier_link) } end + + trait :piece_justificative do + type_de_champ { create(:type_de_champ_piece_justificative) } + end + + trait :with_piece_justificative_file do + after(:create) do |champ, evaluator| + champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") + end + end end end diff --git a/spec/factories/virus_scan.rb b/spec/factories/virus_scan.rb new file mode 100644 index 000000000..198f63ee8 --- /dev/null +++ b/spec/factories/virus_scan.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :virus_scan do + end +end diff --git a/spec/jobs/anti_virus_job_spec.rb b/spec/jobs/anti_virus_job_spec.rb new file mode 100644 index 000000000..4c2f1756f --- /dev/null +++ b/spec/jobs/anti_virus_job_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe AntiVirusJob, type: :job do + let(:champ) do + champ = create(:champ, :piece_justificative) + champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") + champ + end + let(:virus_scan) { create(:virus_scan, status: "pending", champ: champ, blob_key: champ.piece_justificative_file.blob.key) } + + subject { AntiVirusJob.new.perform(virus_scan) } + + context "when no virus is found" do + let(:virus_found?) { true } + + before do + allow(ClamavService).to receive(:safe_file?).and_return(virus_found?) + subject + end + + it { expect(virus_scan.reload.status).to eq("safe") } + end + + context "when a virus is found" do + let(:virus_found?) { false } + + before do + allow(ClamavService).to receive(:safe_file?).and_return(virus_found?) + subject + end + + it { expect(virus_scan.reload.status).to eq("infected") } + end +end diff --git a/spec/models/champ_spec.rb b/spec/models/champ_spec.rb index 2d7767e9c..dcc51b45a 100644 --- a/spec/models/champ_spec.rb +++ b/spec/models/champ_spec.rb @@ -127,4 +127,28 @@ describe Champ do it { expect(champ.for_export).to eq('Crétinier, Mousserie') } end end + + describe '#enqueue_virus_check' do + let(:champ) { type_de_champ.champ.build(value: nil) } + + context 'when type_champ is type_de_champ_piece_justificative' do + let(:type_de_champ) { create(:type_de_champ_piece_justificative) } + + context 'and there is a blob' do + before { champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") } + + it { expect{ champ.save }.to change(VirusScan, :count).by(1) } + end + + context 'and there is no blob' do + it { expect{ champ.save }.to_not change(VirusScan, :count) } + end + end + + context 'when type_champ is not type_de_champ_piece_justificative' do + let(:type_de_champ) { create(:type_de_champ_textarea) } + + it { expect{ champ.save }.to_not change(VirusScan, :count) } + end + end end