Skip to content

Commit 96f724f

Browse files
Merge pull request #2256 from IFRCGo/feature/secure-files
Secure file fields by adding random uuid in the file name.
2 parents 67e0e60 + cac156a commit 96f724f

12 files changed

+211
-11
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 4.2.16 on 2024-10-29 08:51
2+
3+
from django.db import migrations
4+
5+
import api.models
6+
import main.fields
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("api", "0214_alter_profile_limit_access_to_guest"),
13+
]
14+
15+
operations = [
16+
migrations.AlterField(
17+
model_name="generaldocument",
18+
name="document",
19+
field=main.fields.SecureFileField(
20+
blank=True, null=True, upload_to=api.models.general_document_path, verbose_name="document"
21+
),
22+
),
23+
migrations.AlterField(
24+
model_name="situationreport",
25+
name="document",
26+
field=main.fields.SecureFileField(
27+
blank=True, null=True, upload_to=api.models.sitrep_document_path, verbose_name="document"
28+
),
29+
),
30+
]

api/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from django.utils.translation import gettext_lazy as _
2323
from tinymce.models import HTMLField
2424

25+
from main.fields import SecureFileField
26+
2527
from .utils import validate_slug_number # is_user_ifrc,
2628

2729

@@ -984,7 +986,7 @@ def sitrep_document_path(instance, filename):
984986
class SituationReport(models.Model):
985987
created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True)
986988
name = models.CharField(verbose_name=_("name"), max_length=100)
987-
document = models.FileField(verbose_name=_("document"), null=True, blank=True, upload_to=sitrep_document_path)
989+
document = SecureFileField(verbose_name=_("document"), null=True, blank=True, upload_to=sitrep_document_path)
988990
document_url = models.URLField(verbose_name=_("document url"), blank=True)
989991

990992
event = models.ForeignKey(Event, verbose_name=_("event"), on_delete=models.CASCADE)
@@ -1287,7 +1289,7 @@ class GeneralDocument(models.Model):
12871289
name = models.CharField(verbose_name=_("name"), max_length=100)
12881290
# Don't set `auto_now_add` so we can modify it on save
12891291
created_at = models.DateTimeField(verbose_name=_("created at"), blank=True)
1290-
document = models.FileField(verbose_name=_("document"), null=True, blank=True, upload_to=general_document_path)
1292+
document = SecureFileField(verbose_name=_("document"), null=True, blank=True, upload_to=general_document_path)
12911293
document_url = models.URLField(verbose_name=_("document url"), blank=True)
12921294

12931295
class Meta:

api/test_views.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
2+
import uuid
23

34
from django.contrib.auth.models import User
5+
from django.core.files.uploadedfile import SimpleUploadedFile
46

57
import api.models as models
68
from api.factories.event import (
@@ -13,9 +15,37 @@
1315
from api.factories.field_report import FieldReportFactory
1416
from api.models import Profile, VisibilityChoices
1517
from deployments.factories.user import UserFactory
18+
from dref.models import DrefFile
1619
from main.test_case import APITestCase, SnapshotTestCase
1720

1821

22+
class SecureFileFieldTest(APITestCase):
23+
def is_valid_uuid(self, uuid_to_test):
24+
try:
25+
# Validate if the directory name is a valid UUID (hex)
26+
uuid_obj = uuid.UUID(uuid_to_test, version=4)
27+
except ValueError:
28+
return False
29+
return uuid_obj.hex == uuid_to_test
30+
31+
def test_filefield_uuid_directory_and_filename(self):
32+
# Mocking a file upload with SimpleUploadedFile
33+
original_filename = "test_file.txt"
34+
mock_file = SimpleUploadedFile(original_filename, b"file_content", content_type="text/plain")
35+
# Create an instance of MyModel and save it with the mocked file
36+
instance = DrefFile.objects.create(file=mock_file)
37+
# Check the uploaded file name
38+
uploaded_file_name = instance.file.name
39+
# Example output: uploads/5f9d54c8b5a34d3e8fdc4e4f43e2f82a/test_file.txt
40+
41+
# Extract UUID directory part and filename part
42+
directory_name, file_name = uploaded_file_name.split("/")[-2:]
43+
# Check that the directory name is a valid UUID (hexadecimal)
44+
self.assertTrue(self.is_valid_uuid(directory_name))
45+
# Check that the file name retains the original name
46+
self.assertEqual(file_name, original_filename)
47+
48+
1949
class GuestUserPermissionTest(APITestCase):
2050
def setUp(self):
2151
# Create guest user
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 4.2.16 on 2024-10-29 08:51
2+
3+
import django.core.validators
4+
from django.db import migrations
5+
6+
import country_plan.models
7+
import main.fields
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
("country_plan", "0007_alter_membershipcoordination_sector_and_more"),
14+
]
15+
16+
operations = [
17+
migrations.AlterField(
18+
model_name="countryplan",
19+
name="internal_plan_file",
20+
field=main.fields.SecureFileField(
21+
blank=True,
22+
null=True,
23+
upload_to=country_plan.models.pdf_upload_to,
24+
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
25+
verbose_name="Internal Plan",
26+
),
27+
),
28+
]

country_plan/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.utils.translation import gettext_lazy as _
66

77
from api.models import Country
8+
from main.fields import SecureFileField
89

910

1011
def file_upload_to(instance, filename):
@@ -63,7 +64,7 @@ def save(self, *args, **kwargs):
6364

6465
class CountryPlan(CountryPlanAbstract):
6566
country = models.OneToOneField(Country, on_delete=models.CASCADE, related_name="country_plan", primary_key=True)
66-
internal_plan_file = models.FileField(
67+
internal_plan_file = SecureFileField(
6768
verbose_name=_("Internal Plan"),
6869
upload_to=pdf_upload_to,
6970
validators=[FileExtensionValidator(["pdf"])],
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 4.2.16 on 2024-10-29 08:51
2+
3+
from django.db import migrations
4+
5+
import main.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("dref", "0074_auto_20240129_0909"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="dref",
17+
name="budget_file_preview",
18+
field=main.fields.SecureFileField(
19+
blank=True, null=True, upload_to="dref/images/", verbose_name="budget file preview"
20+
),
21+
),
22+
migrations.AlterField(
23+
model_name="dreffile",
24+
name="file",
25+
field=main.fields.SecureFileField(upload_to="dref/images/", verbose_name="file"),
26+
),
27+
migrations.AlterField(
28+
model_name="dreffinalreport",
29+
name="financial_report_preview",
30+
field=main.fields.SecureFileField(blank=True, null=True, upload_to="dref/images/", verbose_name="financial preview"),
31+
),
32+
migrations.AlterField(
33+
model_name="drefoperationalupdate",
34+
name="budget_file_preview",
35+
field=main.fields.SecureFileField(
36+
blank=True, null=True, upload_to="dref-op-update/images/", verbose_name="budget file preview"
37+
),
38+
),
39+
]

dref/models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pdf2image import convert_from_bytes
1313

1414
from api.models import Country, DisasterType, District, FieldReport
15+
from main.fields import SecureFileField
1516

1617

1718
@reversion.register()
@@ -536,7 +537,7 @@ class Status(models.IntegerChoices):
536537
verbose_name=_("budget file"),
537538
related_name="budget_file_dref",
538539
)
539-
budget_file_preview = models.FileField(verbose_name=_("budget file preview"), null=True, blank=True, upload_to="dref/images/")
540+
budget_file_preview = SecureFileField(verbose_name=_("budget file preview"), null=True, blank=True, upload_to="dref/images/")
540541
assessment_report = models.ForeignKey(
541542
"DrefFile",
542543
on_delete=models.SET_NULL,
@@ -648,7 +649,7 @@ def get_for(user):
648649

649650

650651
class DrefFile(models.Model):
651-
file = models.FileField(
652+
file = SecureFileField(
652653
verbose_name=_("file"),
653654
upload_to="dref/images/",
654655
)
@@ -750,7 +751,7 @@ class DrefOperationalUpdate(models.Model):
750751
verbose_name=_("budget file"),
751752
related_name="budget_file_dref_operational_update",
752753
)
753-
budget_file_preview = models.FileField(
754+
budget_file_preview = SecureFileField(
754755
verbose_name=_("budget file preview"), null=True, blank=True, upload_to="dref-op-update/images/"
755756
)
756757
assessment_report = models.ForeignKey(
@@ -1316,7 +1317,7 @@ class DrefFinalReport(models.Model):
13161317
verbose_name=_("financial report"),
13171318
related_name="financial_report_dref_final_report",
13181319
)
1319-
financial_report_preview = models.FileField(
1320+
financial_report_preview = SecureFileField(
13201321
verbose_name=_("financial preview"), null=True, blank=True, upload_to="dref/images/"
13211322
)
13221323
num_assisted = models.IntegerField(verbose_name=_("number of assisted"), blank=True, null=True)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.16 on 2024-10-29 08:51
2+
3+
from django.db import migrations
4+
5+
import main.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("flash_update", "0012_auto_20230410_0720"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="flashgraphicmap",
17+
name="file",
18+
field=main.fields.SecureFileField(upload_to="flash_update/images", verbose_name="file"),
19+
),
20+
migrations.AlterField(
21+
model_name="flashupdate",
22+
name="extracted_file",
23+
field=main.fields.SecureFileField(
24+
blank=True, null=True, upload_to="flash_update/pdf/", verbose_name="extracted file"
25+
),
26+
),
27+
]

flash_update/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# import os
2+
# from uuid import uuid4
13
import reversion
24
from django.conf import settings
35
from django.contrib.auth.models import Group
@@ -14,11 +16,12 @@
1416
DisasterType,
1517
District,
1618
)
19+
from main.fields import SecureFileField
1720

1821

1922
@reversion.register()
2023
class FlashGraphicMap(models.Model):
21-
file = models.FileField(verbose_name=_("file"), upload_to="flash_update/images/")
24+
file = SecureFileField(verbose_name=_("file"), upload_to="flash_update/images")
2225
caption = models.CharField(max_length=225, blank=True, null=True)
2326
created_by = models.ForeignKey(
2427
settings.AUTH_USER_MODEL,
@@ -116,7 +119,7 @@ class FlashShareWith(models.TextChoices):
116119
verbose_name=_("share with"),
117120
)
118121
references = models.ManyToManyField(FlashReferences, blank=True, verbose_name=_("references"))
119-
extracted_file = models.FileField(verbose_name=_("extracted file"), upload_to="flash_update/pdf/", blank=True, null=True)
122+
extracted_file = SecureFileField(verbose_name=_("extracted file"), upload_to="flash_update/pdf/", blank=True, null=True)
120123
extracted_at = models.DateTimeField(verbose_name=_("extracted at"), blank=True, null=True)
121124

122125
class Meta:

main/fields.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from uuid import uuid4
2+
3+
from django.db.models.fields.files import FileField
4+
5+
6+
class SecureFileField(FileField):
7+
def generate_filename(self, instance, filename):
8+
"""
9+
Overwrites https://github.com/django/django/blob/main/django/db/models/fields/files.py#L345
10+
"""
11+
# Append uuid4 path to the filename
12+
filename = f"{uuid4().hex}/{filename}"
13+
return super().generate_filename(instance, filename) # return self.storage.generate_filename(filename)

0 commit comments

Comments
 (0)