Skip to content

Commit

Permalink
Use unique API responses for different filescan results
Browse files Browse the repository at this point in the history
  • Loading branch information
juho-kettunen-nc committed Feb 4, 2025
1 parent 59e7b27 commit 0ad522d
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 103 deletions.
41 changes: 24 additions & 17 deletions filescan/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,32 @@ def scan_result(self) -> FileScanResult:
return FileScanResult.PENDING

@staticmethod
def is_file_scanned_and_safe(fieldfile_instance: PrivateFieldFile) -> bool:
if settings.FLAG_FILE_SCAN is True:
content_type = ContentType.objects.get_for_model(fieldfile_instance)
scan_status = (
FileScanStatus.objects.filter(
content_type=content_type,
object_id=fieldfile_instance.pk,
)
.order_by("id")
.last()
)
if scan_status is None:
return False
def filefield_latest_scan_result(
file_object: models.Model,
) -> FileScanResult:
if settings.FLAG_FILE_SCAN is False:
# Feature is not enabled, all files are considered safe.
return FileScanResult.SAFE

scan_result = scan_status.scan_result()
is_file_scanned_and_safe = scan_result == FileScanResult.SAFE
return is_file_scanned_and_safe
# Find the latest filescan status for this file
content_type = ContentType.objects.get_for_model(file_object)
scan_status = (
FileScanStatus.objects.filter(
content_type=content_type,
object_id=file_object.pk,
)
.order_by("id")
.last()
)
if scan_status is None:
# The file has not yet been queued for a virus scan.
# Consider if this branch should raise an error.
logger.warning(
f"FileScanStatus not found for object {file_object.pk} of contenttype {content_type}"
)
return FileScanResult.PENDING

return True
return scan_status.scan_result()

class Meta:
verbose_name = _("File Scan Status")
Expand Down
100 changes: 33 additions & 67 deletions filescan/tests/test_filescan.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
_scan_file_task,
schedule_file_for_virus_scanning,
)
from utils.models.fields import PrivateFieldFile, UnsafeFileError
from utils.models.fields import (
FileScanError,
FileScanPendingError,
FileUnsafeError,
PrivateFieldFile,
)


@override_settings(FLAG_FILE_SCAN=False)
Expand Down Expand Up @@ -186,103 +191,64 @@ def test_file_deletion(

@override_settings(FLAG_FILE_SCAN=False)
@pytest.mark.django_db
def test_is_file_scanned_and_safe(
django_db_setup,
generic_test_data,
):
"""
Test different scan_results with _is_file_scanned_and_safe().
"""
scan: FileScanStatus = generic_test_data["scan"]
private_fieldfile: PrivateFieldFile = generic_test_data["private_fieldfile"]
with override_settings(FLAG_FILE_SCAN=True):
assert private_fieldfile._is_file_scanned_and_safe() is False

scan.scanned_at = timezone.now()
scan.save()
assert private_fieldfile._is_file_scanned_and_safe() is True

scan.error_message = "error"
scan.save()
assert private_fieldfile._is_file_scanned_and_safe() is False

scan.error_message = None
scan.file_deleted_at = timezone.now()
scan.save()
assert private_fieldfile._is_file_scanned_and_safe() is False


@override_settings(FLAG_FILE_SCAN=False)
@pytest.mark.django_db
def test_is_file_scanned_and_safe_multiple_scan_statuses(
def test_private_field_file_open(
django_db_setup,
generic_test_data,
file_scan_status_factory,
):
"""
Test different scan_results with _is_file_scanned_and_safe()
With multiple FileScanStatus for the same file.
File in a PrivateFileField is only allowed to be opened if the file has been
successfully scanned and found to be safe.
"""
attachment = generic_test_data["attachment"]
private_fieldfile: PrivateFieldFile = generic_test_data["private_fieldfile"]

# File must be allowed to be opened for reading if feature flag is off
with override_settings(FLAG_FILE_SCAN=False):
try:
assert private_fieldfile.open()
except (FileScanPendingError, FileUnsafeError, FileScanError):
pytest.fail(
"An error related to file scanning was raised when feature flag is off"
)

# File has not yet been scanned --> open() method must raise an error
_scan_status_pending: FileScanStatus = generic_test_data["scan"] # noqa: F841
with override_settings(FLAG_FILE_SCAN=True):
assert private_fieldfile._is_file_scanned_and_safe() is False
with pytest.raises(FileScanPendingError):
private_fieldfile.open()

# File has been found safe --> it can be read
_scan_status_safe: FileScanStatus = file_scan_status_factory( # noqa: F841
content_object=attachment,
filepath=attachment.file_attachment.name,
filefield_field_name="file_attachment",
scanned_at=timezone.now(),
)

with override_settings(FLAG_FILE_SCAN=True):
assert private_fieldfile._is_file_scanned_and_safe() is True
assert private_fieldfile.open()

# File has been found unsafe --> open() method must raise an error
_scan_status_unsafe: FileScanStatus = file_scan_status_factory( # noqa: F841
content_object=attachment,
filepath=attachment.file_attachment.name,
filefield_field_name="file_attachment",
file_deleted_at=timezone.now(),
)
with override_settings(FLAG_FILE_SCAN=True):
assert private_fieldfile._is_file_scanned_and_safe() is False


@pytest.mark.django_db
def test_filescan_unsafe_fieldfile_open(
django_db_setup,
generic_test_data,
):
"""
Test PrivateFieldFile.open() with feature flag on and off, and
FileScanStatus with safe and unsafe.
"""
private_fieldfile: PrivateFieldFile = generic_test_data["private_fieldfile"]

with override_settings(FLAG_FILE_SCAN=True):
with pytest.raises(UnsafeFileError):
with pytest.raises(FileUnsafeError):
private_fieldfile.open()

with override_settings(FLAG_FILE_SCAN=False):
try:
private_fieldfile.open()
except UnsafeFileError:
pytest.fail("UnsafeFileDeletedError was raised when feature flag is off")

scan: FileScanStatus = generic_test_data["scan"]
scan.file_deleted_at = timezone.now()
scan.save()

scan.file_deleted_at = None
scan.scanned_at = timezone.now()
scan.save()
# Filescan was not successful --> open() method must raise an error
_scan_status_error: FileScanStatus = file_scan_status_factory( # noqa: F841
content_object=attachment,
filepath=attachment.file_attachment.name,
filefield_field_name="file_attachment",
error_message="error message",
)
with override_settings(FLAG_FILE_SCAN=True):
try:
with pytest.raises(FileScanError):
private_fieldfile.open()
except UnsafeFileError:
pytest.fail("UnsafeFileDeletedError was raised when file was safe")


@override_settings(FLAG_FILE_SCAN=False)
Expand Down
4 changes: 2 additions & 2 deletions filescan/tests/test_filescan_attachment_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def test_attachment_classes(
f"{api_route_root}-download", kwargs={"pk": attachment_id}
)
response = admin_client.get(path=url_download)
assert response.status_code == http_status.HTTP_410_GONE
assert response.status_code == http_status.HTTP_403_FORBIDDEN
assert "error" in response.data.keys()

# If a file has been scanned successfully with no detections, allow download
Expand All @@ -130,7 +130,7 @@ def test_attachment_classes(
# If scanning process encountered an error, deny download
_mock_scan_file_task_error(filescan.pk)
response = admin_client.get(path=url_download)
assert response.status_code == http_status.HTTP_410_GONE
assert response.status_code == http_status.HTTP_403_FORBIDDEN
assert "error" in response.data.keys()

# If file was deleted due to detected virus, deny download
Expand Down
41 changes: 40 additions & 1 deletion locale/fi/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: MVJ 0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-16 13:25+0200\n"
"POT-Creation-Date: 2025-02-04 13:24+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: \n"
"Language: fi\n"
Expand Down Expand Up @@ -373,6 +373,24 @@ msgid ""
"Cannot find business id or national identification number in customer data."
msgstr "Asiakastiedoista ei löydy y-tunnusta tai henkilötunnusta."

msgid "filepath"
msgstr "tiedostopolku"

msgid "scan time"
msgstr "skannaushetki"

msgid "deletion time"
msgstr "poistohetki"

msgid "error message"
msgstr "virheviesti"

msgid "File Scan Status"
msgstr "Tiedoston skannauksen tila"

msgid "File Scan Statuses"
msgstr "Tiedoston skannauksen tilat"

msgctxt "Form state"
msgid "Work in progress"
msgstr "Kesken"
Expand Down Expand Up @@ -4017,10 +4035,31 @@ msgstr "Token olemassa"
msgid "Users permissions"
msgstr "Käyttäjän oikeudet"

msgid ""
"File has not yet been scanned for viruses, and is unsafe to open at this "
"time."
msgstr "Tiedostoa ei ole vielä skannattu virusten varalta, joten sitä ei ole "
"turvallista avata tällä hetkellä."

msgid "File was found to contain virus or malware, and it has been deleted."
msgstr "Tiedostosta löydettiin virus tai muu haittaohjelma, ja tiedosto on "
"poistettu."

msgid ""
"File scan failed. File is unsafe to open before before it has been "
"successfully scanned for viruses and malware."
msgstr "Tiedoston skannaus epäonnistui. Tiedostoa ei ole turvallista avata "
"ennen kuin se on onnistuneesti skannattu virusten ja haittaohjelmien varalta."

#, python-brace-format
msgid "File '{file.name}' does not have an extension."
msgstr "Puuttuva tiedostopääte '{file.name}'"

#, python-brace-format
msgid "File extension '.{ext}' is not allowed."
msgstr "Tiedostomuoto '.{ext}' ei ole sallittu."

#, python-brace-format
msgid "File '{file.name}' exceeds maximum file size of {MAX_FILE_SIZE_MB} MB."
msgstr "Tiedosto '{file.name}' ylittää suurimman sallitun tiedostokoon "
"{MAX_FILE_SIZE_MB} MB"
37 changes: 36 additions & 1 deletion locale/sv/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-16 13:25+0200\n"
"POT-Creation-Date: 2025-02-04 14:05+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -384,6 +384,24 @@ msgid ""
msgstr ""
"Kunduppgifterna innehåller inget FO-nummer eller ingen personbeteckning."

msgid "filepath"
msgstr ""

msgid "scan time"
msgstr ""

msgid "deletion time"
msgstr ""

msgid "error message"
msgstr ""

msgid "File Scan Status"
msgstr ""

msgid "File Scan Statuses"
msgstr ""

msgctxt "Form state"
msgid "Work in progress"
msgstr "Oavslutad"
Expand Down Expand Up @@ -4025,10 +4043,27 @@ msgstr "Token existerar"
msgid "Users permissions"
msgstr "Användarens rättigheter"

msgid ""
"File has not yet been scanned for viruses, and is unsafe to open at this "
"time."
msgstr ""

msgid "File was found to contain virus or malware, and it has been deleted."
msgstr ""

msgid ""
"File scan failed. File is unsafe to open before before it has been "
"successfully scanned for viruses and malware."
msgstr ""

#, python-brace-format
msgid "File '{file.name}' does not have an extension."
msgstr ""

#, python-brace-format
msgid "File extension '.{ext}' is not allowed."
msgstr ""

#, python-brace-format
msgid "File '{file.name}' exceeds maximum file size of {MAX_FILE_SIZE_MB} MB."
msgstr ""
Loading

0 comments on commit 0ad522d

Please sign in to comment.