Skip to content

Commit 0a21c10

Browse files
committed
Merge branch 'master' into TP2000-1434--packaging-queue-race-condition
2 parents ce5ca59 + 3cf4f34 commit 0a21c10

14 files changed

+165
-34
lines changed

.copilot/config.yml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
repository: tariff-application/tamato
2+
builder:
3+
name: paketobuildpacks/builder-jammy-full
4+
version: 0.3.339

.copilot/image_build_run.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
3+
set -e
4+
5+
# Add commands below to run inside the container after all the other buildpacks have been applied

.copilot/phases/build.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
set -e

.copilot/phases/install.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
set -e

.copilot/phases/post_build.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
set -e

.copilot/phases/pre_build.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
set -e

common/tests/test_validators.py

+63
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from django.core.exceptions import ValidationError
23

34
from common.tests.util import check_validator
45
from common.validators import AlphanumericValidator
@@ -8,6 +9,8 @@
89
from common.validators import NumericValidator
910
from common.validators import PasswordPolicyValidator
1011
from common.validators import SymbolValidator
12+
from common.validators import validate_filename
13+
from common.validators import validate_filepath
1114

1215

1316
@pytest.mark.parametrize(
@@ -116,3 +119,63 @@ def test_numeric_validator(value, expected_valid):
116119
def test_password_policy_validator(value, expected_valid):
117120
validator = PasswordPolicyValidator()
118121
check_validator(validator.validate, value, expected_valid)
122+
123+
124+
@pytest.mark.parametrize(
125+
"filename",
126+
[
127+
"TGB123",
128+
"TGB123-v2.xml",
129+
"TGB123_v3.xml",
130+
],
131+
)
132+
def test_validate_filename_valid_name(filename):
133+
try:
134+
validate_filename(filename)
135+
except ValidationError:
136+
pytest.fail(
137+
f'Unexpected ValidationError exception raised for filename "{filename}"',
138+
)
139+
140+
141+
@pytest.mark.parametrize(
142+
"filename",
143+
[
144+
"../dotdot/separator",
145+
"$p£(!@lc|-|ars).txt",
146+
"test.html%00.xml",
147+
],
148+
)
149+
def test_validate_filename_raises_exception(filename):
150+
with pytest.raises(ValidationError):
151+
validate_filename(filename)
152+
153+
154+
@pytest.mark.parametrize(
155+
("source", "base_path"),
156+
[
157+
("common", ""),
158+
("common/tests/", "common/"),
159+
("common/tests/test_files", "common/"),
160+
("common/tests/test_files/dtd.xml", "common/tests/test_files"),
161+
],
162+
)
163+
def test_validate_filepath_valid_path(source, base_path):
164+
try:
165+
validate_filepath(source, base_path)
166+
except ValidationError:
167+
pytest.fail(
168+
f'Unexpected ValidationError exception raised for source "{source}" and base_path "{base_path}"',
169+
)
170+
171+
172+
@pytest.mark.parametrize(
173+
("source", "base_path"),
174+
[
175+
("../../etc/passwd", ""),
176+
("common/tests/../../../outside", "common/"),
177+
],
178+
)
179+
def test_validate_filepath_raises_exception(source, base_path):
180+
with pytest.raises(ValidationError):
181+
validate_filepath(source, base_path)

common/util.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from datetime import timedelta
99
from functools import lru_cache
1010
from functools import partial
11+
from pathlib import Path
1112
from platform import python_version_tuple
13+
from typing import IO
1214
from typing import Any
1315
from typing import Dict
1416
from typing import Optional
@@ -553,7 +555,7 @@ def check_docinfo(elementtree, forbid_dtd=False):
553555
raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id)
554556

555557

556-
def parse_xml(source, forbid_dtd=True):
558+
def parse_xml(source: Union[str, Path, IO], forbid_dtd=True):
557559
parser = etree.XMLParser(resolve_entities=False)
558560
elementtree = etree.parse(source, parser)
559561
check_docinfo(elementtree, forbid_dtd=forbid_dtd)

common/validators.py

+45
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
"""Common validators."""
22

3+
import os
4+
from pathlib import Path
5+
from typing import IO
6+
from typing import Union
7+
8+
from django.conf import settings
39
from django.core.exceptions import ValidationError
410
from django.core.validators import RegexValidator
511
from django.db import models
612
from django.utils.deconstruct import deconstructible
13+
from werkzeug.utils import secure_filename
714

815

916
@deconstructible
@@ -127,3 +134,41 @@ def get_help_text(self):
127134
"tbody",
128135
"td",
129136
]
137+
138+
139+
def validate_filename(filename: str) -> None:
140+
"""
141+
Validates that `filename` only includes alphanumeric characters and special
142+
characters such as spaces, hyphens and underscores.
143+
144+
Raises a `ValidationError` if `filename` includes non-permitted characters.
145+
"""
146+
147+
# filename might include spaces which secure_filename normalises to underscores
148+
if filename.replace(" ", "_") != secure_filename(filename):
149+
raise ValidationError(
150+
f"File name must only include alphanumeric characters and special characters such as spaces, hyphens and underscores.",
151+
)
152+
153+
154+
def validate_filepath(source: Union[str, Path, IO], base_path: str = "") -> None:
155+
"""
156+
Validates that `source` normalises to an absolute path located within
157+
`base_path` directory (`settings.BASE_DIR` by default).
158+
159+
Raises a `ValidationError` if the resulting path would be located outside the expected base path.
160+
"""
161+
162+
if isinstance(source, str):
163+
path = source
164+
elif hasattr(source, "name"):
165+
path = source.name
166+
else:
167+
raise ValueError(f"Expected str, Path or File-like object, not {type(source)}")
168+
169+
if not base_path:
170+
base_path = settings.BASE_DIR
171+
172+
full_path = os.path.normpath(os.path.join(base_path, path))
173+
if not full_path.startswith(base_path):
174+
raise ValidationError("Invalid file path: {path}")

common/views.py

+15-7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import kombu.exceptions
1515
from botocore.exceptions import ClientError
1616
from botocore.exceptions import EndpointConnectionError
17+
from dbt_copilot_python.utility import is_copilot
1718
from django.conf import settings
1819
from django.contrib.auth.mixins import LoginRequiredMixin
1920
from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -302,13 +303,20 @@ def check_celery_broker(self) -> Tuple[str, int]:
302303

303304
def check_s3(self) -> Tuple[str, int]:
304305
try:
305-
client = boto3.client(
306-
"s3",
307-
aws_access_key_id=settings.HMRC_PACKAGING_S3_ACCESS_KEY_ID,
308-
aws_secret_access_key=settings.HMRC_PACKAGING_S3_SECRET_ACCESS_KEY,
309-
endpoint_url=settings.S3_ENDPOINT_URL,
310-
region_name=settings.HMRC_PACKAGING_S3_REGION_NAME,
311-
)
306+
if is_copilot():
307+
client = boto3.client(
308+
"s3",
309+
endpoint_url=settings.S3_ENDPOINT_URL,
310+
region_name=settings.HMRC_PACKAGING_S3_REGION_NAME,
311+
)
312+
else:
313+
client = boto3.client(
314+
"s3",
315+
aws_access_key_id=settings.HMRC_PACKAGING_S3_ACCESS_KEY_ID,
316+
aws_secret_access_key=settings.HMRC_PACKAGING_S3_SECRET_ACCESS_KEY,
317+
endpoint_url=settings.S3_ENDPOINT_URL,
318+
region_name=settings.HMRC_PACKAGING_S3_REGION_NAME,
319+
)
312320
client.head_bucket(Bucket=settings.HMRC_PACKAGING_STORAGE_BUCKET_NAME)
313321
return "OK", 200
314322
except (ClientError, EndpointConnectionError):

importer/forms.py

+12-19
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
from common.util import get_mime_type
2121
from common.util import parse_xml
22+
from common.validators import validate_filename
23+
from common.validators import validate_filepath
2224
from importer.chunker import chunk_taric
2325
from importer.management.commands.run_import_batch import run_batch
2426
from importer.models import ImportBatch
@@ -74,13 +76,8 @@ def clean_taric_file(self):
7476
if mime_type not in ["text/xml", "application/xml"]:
7577
raise ValidationError("The selected file must be XML")
7678

77-
secure_file_name = secure_filename(uploaded_taric_file.name)
78-
if uploaded_taric_file.name.replace(" ", "_") == secure_file_name:
79-
uploaded_taric_file.name == secure_file_name
80-
else:
81-
raise ValidationError(
82-
"File name must only include alphanumeric characters and special characters such as spaces, hyphens and underscores.",
83-
)
79+
validate_filename(uploaded_taric_file.name)
80+
validate_filepath(uploaded_taric_file)
8481

8582
try:
8683
xml_file = parse_xml(uploaded_taric_file)
@@ -154,13 +151,8 @@ def clean_taric_file(self):
154151
if mime_type not in ["text/xml", "application/xml"]:
155152
raise ValidationError("The selected file must be XML")
156153

157-
secure_file_name = secure_filename(uploaded_taric_file.name)
158-
if uploaded_taric_file.name.replace(" ", "_") == secure_file_name:
159-
uploaded_taric_file.name == secure_file_name
160-
else:
161-
raise ValidationError(
162-
"File name must only include alphanumeric characters and special characters such as spaces, hyphens and underscores.",
163-
)
154+
validate_filename(uploaded_taric_file.name)
155+
validate_filepath(uploaded_taric_file)
164156

165157
try:
166158
xml_file = parse_xml(uploaded_taric_file)
@@ -352,19 +344,20 @@ def save(self):
352344
make sense to use commit=False. process_file() should be moved into the
353345
view if this (common) behaviour becomes required.
354346
"""
355-
file_name = os.path.splitext(self.cleaned_data["name"])[0]
356-
description = f"TARIC {file_name} commodity code changes"
347+
taric_filename = secure_filename(self.cleaned_data["name"])
348+
file_id = os.path.splitext(taric_filename)[0]
349+
description = f"TARIC {file_id} commodity code changes"
357350

358351
import_batch = ImportBatch(
359352
author=self.request.user,
360-
name=self.cleaned_data["name"],
353+
name=taric_filename,
361354
goods_import=True,
362355
)
363356

364357
self.files["taric_file"].seek(0, os.SEEK_SET)
365358
import_batch.taric_file.save(
366-
self.files["taric_file"].name,
367-
ContentFile(self.files["taric_file"].read()),
359+
name=taric_filename,
360+
content=ContentFile(self.files["taric_file"].read()),
368361
)
369362

370363
import_batch.save()

measures/tests/test_views.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,13 @@ def test_multiple_measure_delete_template(client, valid_user, user_workbasket):
216216
)
217217

218218
# Get the measure ids that are being shown in the table in the template.
219-
measure_ids_in_table = [e.text for e in soup.select("table tr td:first-child")]
219+
measure_ids_in_table = {e.text for e in soup.select("table tr td:first-child")}
220220

221221
# Get the sids for the measures we selected, as these are what are shown in the template.
222-
selected_measures_ids = [str(measure.sid) for measure in selected_measures]
222+
selected_measures_ids = {str(measure.sid) for measure in selected_measures}
223223

224224
assert measure_ids_in_table == selected_measures_ids
225-
assert set(measure_ids_in_table).difference([measure_4.sid, measure_5.sid])
225+
assert measure_ids_in_table.difference([measure_4.sid, measure_5.sid])
226226

227227
# 4th column is start date
228228
start_dates_in_table = {e.text for e in soup.select("table tr td:nth-child(4)")}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"clean": "rm -f ./run/static/webpack_bundles/*",
4545
"heroku-prebuild": "",
4646
"heroku-postbuild": "npm run build",
47+
"start": "bash scripts/web-worker-entrypoint.sh",
4748
"test": "jest",
4849
"lint:js": "npx eslint . --ext .jsx,.js",
4950
"lint:js:fix": "npx eslint . --ext .jsx,.js --fix",

settings/common.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,8 @@
324324

325325
# DBT PaaS
326326
elif is_copilot():
327-
DB_URL = dj_database_url.config(
328-
default=database_url_from_env("DATABASE_CREDENTIALS"),
329-
)
330-
DATABASES = {"default": DB_URL}
327+
DB_URL = database_url_from_env("DATABASE_CREDENTIALS")
328+
DATABASES = {"default": dj_database_url.parse(DB_URL)}
331329
# Govuk PaaS
332330
elif VCAP_SERVICES.get("postgres"):
333331
DB_URL = VCAP_SERVICES["postgres"][0]["credentials"]["uri"]

0 commit comments

Comments
 (0)