diff --git a/app/__init__.py b/app/__init__.py index 38165e16b..3345c1cb4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -25,6 +25,7 @@ from functools import partial from notifications_python_client.errors import HTTPError from notifications_utils import logging, request_helper, formatters +from notifications_utils.clients.antivirus.antivirus_client import AntivirusClient from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient from notifications_utils.clients.statsd.statsd_client import StatsdClient from notifications_utils.recipients import ( @@ -86,6 +87,7 @@ email_branding_client = EmailBrandingClient() organisations_client = OrganisationsClient() org_invite_api_client = OrgInviteApiClient() asset_fingerprinter = AssetFingerprinter() +antivirus_client = AntivirusClient() statsd_client = StatsdClient() zendesk_client = ZendeskClient() letter_jobs_client = LetterJobsClient() @@ -121,6 +123,7 @@ def create_app(application): application.config.from_object(configs[notify_environment]) init_app(application) + antivirus_client.init_app(application) statsd_client.init_app(application) zendesk_client.init_app(application) logging.init_app(application, statsd_client) diff --git a/app/config.py b/app/config.py index 8cf8f7a09..f446bd7ed 100644 --- a/app/config.py +++ b/app/config.py @@ -28,6 +28,10 @@ class Config(object): NOTIFY_LOG_PATH = os.getenv('NOTIFY_LOG_PATH') ADMIN_CLIENT_USER_NAME = 'notify-admin' + + ANTIVIRUS_API_HOST = os.environ.get('ANTIVIRUS_API_HOST') + ANTIVIRUS_API_KEY = os.environ.get('ANTIVIRUS_API_KEY') + ASSETS_DEBUG = False AWS_REGION = 'eu-west-1' DEFAULT_SERVICE_LIMIT = 50 @@ -85,6 +89,8 @@ class Development(Config): API_HOST_NAME = 'http://localhost:6011' DANGEROUS_SALT = 'dev-notify-salt' SECRET_KEY = 'dev-notify-secret-key' + ANTIVIRUS_API_HOST = 'http://localhost:6016' + ANTIVIRUS_API_KEY = 'test-key' class Test(Development): @@ -98,6 +104,8 @@ class Test(Development): NOTIFY_ENVIRONMENT = 'test' API_HOST_NAME = 'http://you-forgot-to-mock-an-api-call-to' TEMPLATE_PREVIEW_API_HOST = 'http://localhost:9999' + ANTIVIRUS_API_HOST = 'https://test-antivirus' + ANTIVIRUS_API_KEY = 'test-antivirus-secret' class Preview(Config): diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index c0cc57997..76737d505 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -5,8 +5,12 @@ from datetime import datetime from flask import abort, flash, redirect, render_template, request, url_for from flask_login import login_required from notifications_python_client.errors import HTTPError +from notifications_utils.clients.antivirus.antivirus_client import ( + AntivirusError, +) from app import ( + antivirus_client, complaint_api_client, letter_jobs_client, platform_stats_api_client, @@ -247,14 +251,24 @@ def platform_admin_returned_letters(): def platform_admin_letter_validation_preview(): message, pages, result = None, [], None form = PDFUploadForm() + if form.validate_on_submit(): pdf_file = form.file.data + + try: + virus_free = antivirus_client.scan(pdf_file) + except AntivirusError: + flash("Antivirus API error") + abort(503) + + if not virus_free: + flash("Document didn't pass the virus scan") + abort(400) + try: response = validate_letter(pdf_file) if response.status_code == 200: - pages = response.json()["pages"] - message = response.json()["message"] - result = response.json()["result"] + pages, message, result = response.json()["pages"], response.json()["message"], response.json()["result"] except HTTPError as error: if error.status_code == 400: flash("Something was wrong with the file you tried to upload. Please upload a valid PDF file.") @@ -263,10 +277,7 @@ def platform_admin_letter_validation_preview(): return render_template( 'views/platform-admin/letter-validation-preview.html', - form=form, - message=message, - pages=pages, - result=result + form=form, message=message, pages=pages, result=result ) diff --git a/manifest-base.yml b/manifest-base.yml index 3bf227a5b..c28457046 100644 --- a/manifest-base.yml +++ b/manifest-base.yml @@ -29,6 +29,9 @@ env: AWS_ACCESS_KEY_ID: null AWS_SECRET_ACCESS_KEY: null + ANTIVIRUS_API_HOST: null + ANTIVIRUS_API_KEY: null + STATSD_PREFIX: null ZENDESK_API_KEY: null diff --git a/tests/app/main/views/test_platform_admin.py b/tests/app/main/views/test_platform_admin.py index 9b2373295..719f83ef3 100644 --- a/tests/app/main/views/test_platform_admin.py +++ b/tests/app/main/views/test_platform_admin.py @@ -7,7 +7,7 @@ from unittest.mock import ANY import pytest import requests_mock from bs4 import BeautifulSoup -from flask import url_for, current_app +from flask import current_app, url_for from freezegun import freeze_time from app.main.views.platform_admin import ( @@ -779,6 +779,8 @@ def test_letter_validation_preview_calls_template_preview_when_data_correct_and_ mock_get_user(mocker, user=platform_admin_user) client.login(platform_admin_user) endpoint = '{}/precompiled/validate?include_preview=true'.format(current_app.config['TEMPLATE_PREVIEW_API_HOST']) + mocker.patch('app.main.views.platform_admin.antivirus_client.scan', return_value=True) + with requests_mock.mock() as rmock: rmock.request( "POST", @@ -803,6 +805,7 @@ def test_letter_validation_preview_calls_template_preview_when_data_correct_and_ def test_letter_validation_preview_doesnt_call_template_preview_when_no_file(mocker, client, platform_admin_user): mock_get_user(mocker, user=platform_admin_user) client.login(platform_admin_user) + antivirus_scan = mocker.patch('app.main.views.platform_admin.antivirus_client.scan') validate_letter = mocker.patch('app.main.views.platform_admin.validate_letter') response = client.post( url_for('main.platform_admin_letter_validation_preview'), @@ -810,6 +813,7 @@ def test_letter_validation_preview_doesnt_call_template_preview_when_no_file(moc content_type='multipart/form-data' ) assert response.status_code == 200 + antivirus_scan.assert_not_called() validate_letter.assert_not_called() page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') @@ -819,6 +823,7 @@ def test_letter_validation_preview_doesnt_call_template_preview_when_no_file(moc def test_letter_validation_preview_doesnt_call_template_preview_when_file_not_pdf(mocker, client, platform_admin_user): mock_get_user(mocker, user=platform_admin_user) client.login(platform_admin_user) + antivirus_scan = mocker.patch('app.main.views.platform_admin.antivirus_client.scan') validate_letter = mocker.patch('app.main.views.platform_admin.validate_letter') with open('tests/non_spreadsheet_files/actually_a_png.csv', 'rb') as file: response = client.post( @@ -827,6 +832,29 @@ def test_letter_validation_preview_doesnt_call_template_preview_when_file_not_pd content_type='multipart/form-data' ) assert response.status_code == 200 + antivirus_scan.assert_not_called() validate_letter.assert_not_called() page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') assert page.find('span', class_='error-message').text.strip() == "PDF documents only!" + + +def test_letter_validation_preview_doesnt_call_template_preview_when_file_doesnt_pass_virus_scan( + mocker, client, platform_admin_user +): + mock_get_user(mocker, user=platform_admin_user) + client.login(platform_admin_user) + antivirus_scan = mocker.patch('app.main.views.platform_admin.antivirus_client.scan', return_value=False) + validate_letter = mocker.patch('app.main.views.platform_admin.validate_letter') + + with open('tests/test_pdf_files/multi_page_pdf.pdf', 'rb') as file: + response = client.post( + url_for('main.platform_admin_letter_validation_preview'), + data={"file": file}, + content_type='multipart/form-data' + ) + assert response.status_code == 400 + assert antivirus_scan.called is True + validate_letter.assert_not_called() + + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + assert page.find('div', class_='banner-dangerous').text.strip() == "Document didn't pass the virus scan"