Skip to content

Commit a1e4113

Browse files
authored
Merge branch 'qat' into mod/dev-15451-submission-zip-prefix
2 parents 61460fd + 4e0abbb commit a1e4113

22 files changed

Lines changed: 196 additions & 47 deletions

dataactbroker/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def before_request():
106106
content_type = request.headers.get("Content-Type")
107107

108108
# If the request is a POST we want to log the request body
109-
if request.method == "POST" and content_type and "login" not in request.url.lower():
109+
if request.method == "POST" and content_type and "login" not in request.base_url.lower():
110110
request_body = {}
111111

112112
# If request is json, turn it into a dict
@@ -137,7 +137,7 @@ def handle_exception(exception):
137137
return JsonResponse.error(wrapped, wrapped.status)
138138

139139
# Add routes for modules here
140-
add_login_routes(flask_app, bcrypt)
140+
add_login_routes(flask_app, local, bcrypt)
141141

142142
add_file_routes(flask_app, local, broker_file_path)
143143
add_generation_routes(flask_app, local, broker_file_path)

dataactbroker/handlers/account_handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hmac
12
import json
23
import logging
34
from operator import attrgetter
@@ -119,7 +120,7 @@ def proxy_login(self, session):
119120
roles = safe_dictionary.get_value("roles")
120121
token = safe_dictionary.get_value("token")
121122

122-
if token != CONFIG_BROKER["api_proxy_token"]:
123+
if not hmac.compare_digest(token, CONFIG_BROKER["api_proxy_token"]):
123124
raise ValueError("Invalid token")
124125

125126
user = sess.query(User).filter(func.lower(User.email) == func.lower(email)).one_or_none()

dataactbroker/handlers/fileHandler.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import boto3
22
import calendar
3+
import csv
34
import logging
45
import os
6+
import re
57
import requests
6-
import threading
7-
import csv
8-
import tempfile
98
import shutil
9+
import tempfile
10+
import threading
1011

1112
from collections import namedtuple
1213
from datetime import datetime, timedelta
@@ -27,7 +28,7 @@
2728
)
2829
from dataactbroker.helpers.fabs_derivations_helper import fabs_derivations, log_derivation
2930
from dataactbroker.helpers.filters_helper import permissions_filter, agency_filter
30-
from dataactbroker.helpers.generic_helper import zip_dir
31+
from dataactbroker.helpers.generic_helper import sanitize_for_csv, zip_dir
3132
from dataactbroker.permissions import active_user_can_on_submission
3233

3334
from dataactcore.aws.s3Handler import S3Handler
@@ -1604,7 +1605,9 @@ def update_submission_comments(submission, comment_request, is_local):
16041605
json = comment_request or {}
16051606
# clean input
16061607
comments_json = {
1607-
key.upper(): value.strip() for key, value in json.items() if isinstance(value, str) and value.strip()
1608+
key.upper(): sanitize_for_csv(value.strip())
1609+
for key, value in json.items()
1610+
if isinstance(value, str) and value.strip()
16081611
}
16091612

16101613
sess = GlobalDB.db().session
@@ -2088,7 +2091,7 @@ def add_list_submission_filters(query, filters, submission_updated_view):
20882091
# Make a list of all the names we're filtering on
20892092
file_array = []
20902093
for file_name in file_list:
2091-
file_regex = r".+\/.*" + str(file_name).upper() + r"[^\/]*$"
2094+
file_regex = r".+\/.*" + re.escape(str(file_name).upper()) + r"[^\/]*$"
20922095
file_array.append(func.upper(Job.filename).op("~")(file_regex))
20932096

20942097
# Create a subquery to get all submission IDs related to upload jobs (every type except cross-file has an

dataactbroker/handlers/generation_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,6 @@ def check_detached_generation(job_id):
266266
Returns:
267267
Response object with keys job_id, status, file_type, url, message, start, and end.
268268
"""
269-
response_dict = generation_helper.check_file_generation(job_id)
269+
response_dict = generation_helper.check_file_generation(job_id, detached=True)
270270

271271
return JsonResponse.create(StatusCode.OK, response_dict)

dataactbroker/helpers/generation_helper.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,12 @@ def start_dabs_generation(job, start_date, end_date, agency_code):
194194
logger.debug(log_data)
195195

196196

197-
def check_file_generation(job_id):
197+
def check_file_generation(job_id, detached=False):
198198
"""Check the status of a file generation
199199
200200
Args:
201201
job_id: upload Job ID
202+
detached: whether the job_id provided should be detached or not
202203
Return:
203204
Dict with keys: job_id, status, file_type, message, url, start, end
204205
"""
@@ -223,6 +224,12 @@ def check_file_generation(job_id):
223224
response_dict["status"] = "invalid"
224225
response_dict["message"] = "No generation job found with the specified ID"
225226
return response_dict
227+
if detached and upload_job.submission_id is not None:
228+
response_dict["start"] = ""
229+
response_dict["end"] = ""
230+
response_dict["status"] = "invalid"
231+
response_dict["message"] = "Job ID not associated with a detached file generation."
232+
return response_dict
226233

227234
response_dict["file_type"] = lookups.FILE_TYPE_DICT_LETTER[upload_job.file_type_id]
228235
response_dict["size"] = upload_job.file_size

dataactbroker/helpers/generic_helper.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,28 @@ def generate_raw_quoted_query(queryset):
185185
)
186186

187187

188+
def sanitize_for_csv(value):
189+
"""Checks for and neutralizes potential CSV formula injection, exception of numbers
190+
191+
Args:
192+
value: string to sanitize
193+
194+
Returns:
195+
sanitized string (single quote in front if applicable)
196+
"""
197+
if not value or not isinstance(value, str):
198+
return value
199+
200+
# Characters that spreadsheet software parses as the start of a formula
201+
formula_chars = r"^\s*[=+\-@|](?![\d.,]+$)"
202+
203+
# Prepend an apostrophe if a formula-triggering character is detected
204+
if re.match(formula_chars, value):
205+
return f"'{value}"
206+
207+
return value
208+
209+
188210
def fy(raw_date):
189211
"""Get fiscal year from date, datetime, or date string
190212

dataactbroker/helpers/validation_helper.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,24 @@ def clean_numbers_vectorized(series: pd.Series, convert_to_str=False):
180180
series.update(replacements[cleanable])
181181

182182

183+
def sanitize_csv_vectorized(series: pd.Series):
184+
"""In-place sanitization of strings that start with +/-/=/@, exception of numbers
185+
186+
Args:
187+
series: the series that will be cleaned (updated in-place)
188+
189+
Returns:
190+
None (in-place update of given Series)
191+
"""
192+
formula_pattern = r"^\s*[=+\-@|](?![\d.,]+$)"
193+
194+
# Get subset of values in series that need to be sanitized
195+
needs_escape = series.str.contains(formula_pattern, regex=True, na=False)
196+
197+
# Update in place
198+
series.loc[needs_escape] = "'" + series.loc[needs_escape].astype(str)
199+
200+
183201
def concat_flex(row):
184202
"""Concatenates the headers and contents of all the flex cells in one row of a submission and joins the list
185203
on commas

dataactbroker/permissions.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,13 @@ def wrapped(*args, **kwargs):
224224
return inner
225225

226226

227-
def requires_agency_code_perms(perm):
227+
def requires_agency_code_perms(perm, location=None):
228228
"""Decorator that checks the current user's permissions and validates them against the agency code. It expects an
229229
agency_code parameter on top of the function arguments.
230230
231231
Args:
232232
perm: the type of permission we are checking for
233+
location: the location to check for the agency code ('query' or 'json')
233234
234235
Returns:
235236
The args/kwargs that were initially provided
@@ -243,13 +244,10 @@ def inner(fn):
243244
@requires_login
244245
@wraps(fn)
245246
def wrapped(*args, **kwargs):
246-
for location in ["query", "json"]:
247-
req_args = webargs_parser.parse(
248-
{"agency_code": webargs_fields.String(load_default=None)}, request, location=location
249-
)
250-
agency_code = req_args.get("agency_code", None)
251-
if agency_code is not None:
252-
break
247+
req_args = webargs_parser.parse(
248+
{"agency_code": webargs_fields.String(load_default=None)}, request, location=location
249+
)
250+
agency_code = req_args.get("agency_code", None)
253251
# Ensure there is an agency_code
254252
if agency_code is None:
255253
raise ResponseError("Missing required parameter: agency_code", StatusCode.CLIENT_ERROR)

dataactbroker/routes/dashboard_routes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def historic_dabs_graphs(**kwargs):
5454
@use_kwargs(
5555
{
5656
"page": webargs_fields.Int(load_default=1),
57-
"limit": webargs_fields.Int(load_default=5),
57+
"limit": webargs_fields.Int(validate=webargs_validate.Range(min=1, max=1000), load_default=5),
5858
"sort": webargs_fields.String(load_default="period"),
5959
"order": webargs_fields.String(load_default="desc"),
6060
"filters": webargs_fields.Dict(keys=webargs_fields.String(), required=True),
@@ -166,7 +166,7 @@ def significance_counts(submission, file, **kwargs):
166166
load_default="warning",
167167
),
168168
"page": webargs_fields.Int(load_default=1),
169-
"limit": webargs_fields.Int(load_default=5),
169+
"limit": webargs_fields.Int(validate=webargs_validate.Range(min=1, max=1000), load_default=5),
170170
"sort": webargs_fields.String(load_default="significance"),
171171
"order": webargs_fields.String(load_default="asc"),
172172
},

dataactbroker/routes/file_routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def get_banner_list(login):
104104
@parser.use_kwargs(
105105
{
106106
"page": webargs_fields.Int(load_default=1),
107-
"limit": webargs_fields.Int(load_default=5),
107+
"limit": webargs_fields.Int(validate=webargs_validate.Range(min=1, max=1000), load_default=5),
108108
"published": webargs_fields.String(
109109
required=True, validate=webargs_validate.OneOf(("mixed", "true", "false"))
110110
),

0 commit comments

Comments
 (0)