Merge pull request #2046 from betagouv/antivirus

Antivirus async
This commit is contained in:
Mathieu Magnin 2018-06-12 15:30:14 +02:00 committed by GitHub
commit 8bb4343b90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 173 additions and 7 deletions

View file

@ -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

View file

@ -3,6 +3,7 @@ class Champ < ApplicationRecord
belongs_to :type_de_champ, inverse_of: :champ belongs_to :type_de_champ, inverse_of: :champ
has_many :commentaires has_many :commentaires
has_one_attached :piece_justificative_file 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 delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, to: :type_de_champ

View file

@ -1,2 +1,12 @@
class Champs::PieceJustificativeChamp < Champ 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 end

17
app/models/virus_scan.rb Normal file
View file

@ -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

View file

@ -47,9 +47,7 @@
- else - else
Pas de dossier associé Pas de dossier associé
- elsif champ.type_champ == 'piece_justificative' - elsif champ.type_champ == 'piece_justificative'
- pj = champ.piece_justificative_file = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: true }
%a{ href: url_for(pj), target: '_blank' }
= pj.filename.to_s
- elsif champ.type_champ == 'textarea' - elsif champ.type_champ == 'textarea'
= simple_format(champ.decorate.value) = simple_format(champ.decorate.value)
- else - else

View file

@ -36,8 +36,7 @@
%td.rich-text %td.rich-text
- pj = c.piece_justificative_file - pj = c.piece_justificative_file
- if pj.attached? - if pj.attached?
%a{ href: url_for(pj), target: '_blank' } = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: c, user_can_upload: false }
= pj.filename.to_s
- else - else
Pièce justificative non fournie Pièce justificative non fournie
- when "textarea" - when "textarea"

View file

@ -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)

View file

@ -10,8 +10,7 @@
id: "champs_#{champ.id}", id: "champs_#{champ.id}",
direct_upload: true direct_upload: true
- else - else
%a{ href: url_for(pj), target: '_blank' } = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: true }
= pj.filename.to_s
%br %br
Modifier : Modifier :
= form.file_field :piece_justificative_file, = form.file_field :piece_justificative_file,

View file

@ -47,6 +47,7 @@ Rails.application.configure do
} }
config.active_job.queue_adapter = :test config.active_job.queue_adapter = :test
config.active_storage.service = :test
# Raises error for missing translations # Raises error for missing translations
# config.action_view.raise_on_missing_translations = true # config.action_view.raise_on_missing_translations = true

View file

@ -1,3 +1,7 @@
local: local:
service: Disk service: Disk
root: <%= Rails.root.join("storage") %> root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>

View file

@ -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

View file

@ -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 t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end 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| create_table "without_continuation_mails", id: :serial, force: :cascade do |t|
t.text "body" t.text "body"
t.string "subject" t.string "subject"

View file

@ -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

View file

@ -17,5 +17,15 @@ FactoryBot.define do
trait :dossier_link do trait :dossier_link do
type_de_champ { create(:type_de_champ_dossier_link) } type_de_champ { create(:type_de_champ_dossier_link) }
end 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
end end

View file

@ -0,0 +1,4 @@
FactoryBot.define do
factory :virus_scan do
end
end

View file

@ -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

View file

@ -127,4 +127,28 @@ describe Champ do
it { expect(champ.for_export).to eq('Crétinier, Mousserie') } it { expect(champ.for_export).to eq('Crétinier, Mousserie') }
end end
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 end