diff --git a/integrations/acquisition/covid_hosp/facility/test_scenarios.py b/integrations/acquisition/covid_hosp/facility/test_scenarios.py index 53597c90d..0c3ef1952 100644 --- a/integrations/acquisition/covid_hosp/facility/test_scenarios.py +++ b/integrations/acquisition/covid_hosp/facility/test_scenarios.py @@ -2,7 +2,7 @@ # standard library import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch # first party from delphi.epidata.acquisition.covid_hosp.common.database import Database @@ -19,6 +19,21 @@ NEWLINE="\n" +def _request(params): + """Request and parse epidata. + + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class AcquisitionTests(unittest.TestCase): def setUp(self): diff --git a/integrations/acquisition/covid_hosp/state_daily/test_scenarios.py b/integrations/acquisition/covid_hosp/state_daily/test_scenarios.py index e55bc8ca6..5cb6f786a 100644 --- a/integrations/acquisition/covid_hosp/state_daily/test_scenarios.py +++ b/integrations/acquisition/covid_hosp/state_daily/test_scenarios.py @@ -22,7 +22,21 @@ __test_target__ = \ 'delphi.epidata.acquisition.covid_hosp.state_daily.update' +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class AcquisitionTests(unittest.TestCase): def setUp(self): diff --git a/integrations/acquisition/covid_hosp/state_timeseries/test_scenarios.py b/integrations/acquisition/covid_hosp/state_timeseries/test_scenarios.py index 5d13ccbb0..abe2d94cf 100644 --- a/integrations/acquisition/covid_hosp/state_timeseries/test_scenarios.py +++ b/integrations/acquisition/covid_hosp/state_timeseries/test_scenarios.py @@ -2,7 +2,7 @@ # standard library import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch # first party from delphi.epidata.acquisition.covid_hosp.common.database import Database @@ -18,7 +18,21 @@ __test_target__ = \ 'delphi.epidata.acquisition.covid_hosp.state_timeseries.update' +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class AcquisitionTests(unittest.TestCase): def setUp(self): diff --git a/integrations/acquisition/covidcast/test_covidcast_meta_caching.py b/integrations/acquisition/covidcast/test_covidcast_meta_caching.py index f1f9c4ef9..27bd0e004 100644 --- a/integrations/acquisition/covidcast/test_covidcast_meta_caching.py +++ b/integrations/acquisition/covidcast/test_covidcast_meta_caching.py @@ -3,6 +3,7 @@ # standard library import json import unittest +from unittest.mock import patch # third party import mysql.connector @@ -24,7 +25,21 @@ # use the local instance of the Epidata API BASE_URL = 'http://delphi_web_epidata/epidata/api.php' +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class CovidcastMetaCacheTests(unittest.TestCase): """Tests covidcast metadata caching.""" @@ -144,7 +159,7 @@ def test_caching(self): self.cnx.commit() # fetch the cached version (manually) - params = {'endpoint': 'covidcast_meta', 'cached': 'true'} + params = {'endpoint': 'covidcast_meta', 'cached': 'true', 'meta_key': 'meta_secret'} response = requests.get(BASE_URL, params=params) response.raise_for_status() epidata4 = response.json() @@ -167,7 +182,7 @@ def test_caching(self): self.cnx.commit() # fetch the cached version (manually) - params = {'endpoint': 'covidcast_meta', 'cached': 'true'} + params = {'endpoint': 'covidcast_meta', 'cached': 'true', 'meta_key': 'meta_secret'} response = requests.get(BASE_URL, params=params) response.raise_for_status() epidata5 = response.json() diff --git a/integrations/acquisition/covidcast/test_csv_uploading.py b/integrations/acquisition/covidcast/test_csv_uploading.py index bb65e10d8..4483b05e9 100644 --- a/integrations/acquisition/covidcast/test_csv_uploading.py +++ b/integrations/acquisition/covidcast/test_csv_uploading.py @@ -4,7 +4,7 @@ from datetime import date import os import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch # third party import mysql.connector @@ -20,7 +20,21 @@ # py3tester coverage target (equivalent to `import *`) __test_target__ = 'delphi.epidata.acquisition.covidcast.csv_to_database' +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class CsvUploadingTests(unittest.TestCase): """Tests covidcast CSV uploading.""" diff --git a/integrations/acquisition/covidcast/test_delete_batch.py b/integrations/acquisition/covidcast/test_delete_batch.py index 30f3239cb..c98c40446 100644 --- a/integrations/acquisition/covidcast/test_delete_batch.py +++ b/integrations/acquisition/covidcast/test_delete_batch.py @@ -3,6 +3,7 @@ # standard library from collections import namedtuple import unittest +from unittest.mock import patch from os import path # third party @@ -18,6 +19,21 @@ Example = namedtuple("example", "given expected") +def _request(params): + """Request and parse epidata. + + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class DeleteBatch(unittest.TestCase): """Tests batch deletions""" diff --git a/integrations/acquisition/covidcast/test_fill_is_latest_issue.py b/integrations/acquisition/covidcast/test_fill_is_latest_issue.py index c4d6c741b..b94bd4e4b 100644 --- a/integrations/acquisition/covidcast/test_fill_is_latest_issue.py +++ b/integrations/acquisition/covidcast/test_fill_is_latest_issue.py @@ -15,7 +15,21 @@ # py3tester coverage target (equivalent to `import *`) __test_target__ = 'delphi.epidata.acquisition.covidcast.fill_is_latest_issue' +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@unittest.mock.patch.object(Epidata, '_request', _request) class FillIsLatestIssueTests(unittest.TestCase): """Tests filling is_latest_issue column""" diff --git a/integrations/acquisition/covidcast_nowcast/test_csv_uploading.py b/integrations/acquisition/covidcast_nowcast/test_csv_uploading.py index 9dc163a2b..7a6c45e6e 100644 --- a/integrations/acquisition/covidcast_nowcast/test_csv_uploading.py +++ b/integrations/acquisition/covidcast_nowcast/test_csv_uploading.py @@ -26,7 +26,21 @@ issue=(date(2020, 4, 21), epi.Week.fromdate(date(2020, 4, 21))) ) +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class CsvUploadingTests(unittest.TestCase): """Tests covidcast nowcast CSV uploading.""" diff --git a/integrations/client/test_delphi_epidata.py b/integrations/client/test_delphi_epidata.py index 8d3560a68..eeed4bc66 100644 --- a/integrations/client/test_delphi_epidata.py +++ b/integrations/client/test_delphi_epidata.py @@ -27,7 +27,21 @@ def wrapper(*args): Epidata.BASE_URL = 'http://delphi_web_epidata/epidata/api.php' return wrapper +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class DelphiEpidataPythonClientTests(unittest.TestCase): """Tests the Python client.""" @@ -315,7 +329,7 @@ def test_retry_request(self, get): mock_response = MagicMock() mock_response.status_code = 200 get.side_effect = [JSONDecodeError('Expecting value', "", 0), mock_response] - response = Epidata._request(None) + response = Epidata._request({}) self.assertEqual(get.call_count, 2) self.assertEqual(response, mock_response.json()) @@ -326,7 +340,7 @@ def test_retry_request(self, get): get.side_effect = [JSONDecodeError('Expecting value', "", 0), JSONDecodeError('Expecting value', "", 0), mock_response] - response = Epidata._request(None) + response = Epidata._request({}) self.assertEqual(get.call_count, 2) # 2 from previous test + 2 from this one self.assertEqual(response, {'result': 0, 'message': 'error: Expecting value: line 1 column 1 (char 0)'} @@ -615,7 +629,8 @@ def test_async_epidata(self): 'time_type': 'day', 'geo_type': 'county', 'geo_value': '11111', - 'time_values': '20200414' + 'time_values': '20200414', + 'meta_key': 'meta_secret' }, { 'source': 'covidcast', @@ -624,7 +639,8 @@ def test_async_epidata(self): 'time_type': 'day', 'geo_type': 'county', 'geo_value': '00000', - 'time_values': '20200414' + 'time_values': '20200414', + 'meta_key': 'meta_secret' } ]*12, batch_size=10) responses = [i[0] for i in test_output] @@ -634,7 +650,7 @@ def test_async_epidata(self): Epidata.covidcast('src', 'sig', 'day', 'county', 20200414, '00000')]*12 ) - @fake_epidata_endpoint + @patch.object(Epidata, "BASE_URL", 'http://delphi_web_epidata/epidata/fake_api.php') def test_async_epidata_fail(self): with pytest.raises(ClientResponseError, match="404, message='NOT FOUND'"): Epidata.async_epidata([ @@ -645,6 +661,7 @@ def test_async_epidata_fail(self): 'time_type': 'day', 'geo_type': 'county', 'geo_value': '11111', - 'time_values': '20200414' + 'time_values': '20200414', + 'meta_key': 'meta_secret' } ]) \ No newline at end of file diff --git a/integrations/server/test_covid_hosp.py b/integrations/server/test_covid_hosp.py index 16538b82d..1b15aac69 100644 --- a/integrations/server/test_covid_hosp.py +++ b/integrations/server/test_covid_hosp.py @@ -2,13 +2,28 @@ # standard library import unittest +from unittest.mock import patch # first party from delphi.epidata.acquisition.covid_hosp.state_timeseries.database import Database from delphi.epidata.client.delphi_epidata import Epidata import delphi.operations.secrets as secrets +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class ServerTests(unittest.TestCase): """Tests the `covid_hosp` endpoint.""" diff --git a/integrations/server/test_covidcast.py b/integrations/server/test_covidcast.py index 36e4cd880..3ab11e5bb 100644 --- a/integrations/server/test_covidcast.py +++ b/integrations/server/test_covidcast.py @@ -67,6 +67,7 @@ def test_round_trip(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '01234', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -163,7 +164,8 @@ def test_csv_format(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '01234', - 'format': 'csv' + 'format': 'csv', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.text @@ -203,7 +205,8 @@ def test_raw_json_format(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '01234', - 'format': 'json' + 'format': 'json', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -251,6 +254,7 @@ def test_fields(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '01234', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -284,7 +288,8 @@ def test_fields(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '01234', - 'fields': 'time_value,geo_value' + 'fields': 'time_value,geo_value', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -308,7 +313,8 @@ def test_fields(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '01234', - 'fields': 'time_value,geo_value,dummy' + 'fields': 'time_value,geo_value,dummy', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -336,7 +342,8 @@ def test_fields(self): 'fields': ( '-value,-stderr,-sample_size,-direction,-issue,-lag,-signal,' + '-missing_value,-missing_stderr,-missing_sample_size' - ) + ), + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -393,6 +400,7 @@ def test_location_wildcard(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '*', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -487,6 +495,7 @@ def fetch(geo_value): 'time_type': 'day', 'geo_type': 'county', 'time_values': 20200414, + 'meta_key': 'meta_secret' } if isinstance(geo_value, list): params['geo_values'] = ','.join(geo_value) @@ -609,6 +618,7 @@ def test_location_timeline(self): 'geo_type': 'county', 'time_values': '20200411-20200413', 'geo_value': '01234', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -735,6 +745,7 @@ def test_nullable_columns(self): 'geo_type': 'county', 'time_values': 20200414, 'geo_value': '01234', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -799,6 +810,7 @@ def test_temporal_partitioning(self): 'geo_type': 'state', 'time_values': '0-9999999999', 'geo_value': 'vi', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -865,6 +877,7 @@ def test_date_formats(self): 'geo_type': 'county', 'time_values': '20200411', 'geo_value': '*', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -881,6 +894,7 @@ def test_date_formats(self): 'geo_type': 'county', 'time_values': '2020-04-11', 'geo_value': '*', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -897,6 +911,7 @@ def test_date_formats(self): 'geo_type': 'county', 'time_values': '20200411,20200412', 'geo_value': '*', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -913,6 +928,7 @@ def test_date_formats(self): 'geo_type': 'county', 'time_values': '2020-04-11,2020-04-12', 'geo_value': '*', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -929,6 +945,7 @@ def test_date_formats(self): 'geo_type': 'county', 'time_values': '20200411-20200413', 'geo_value': '*', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -945,6 +962,7 @@ def test_date_formats(self): 'geo_type': 'county', 'time_values': '2020-04-11:2020-04-13', 'geo_value': '*', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() diff --git a/integrations/server/test_covidcast_endpoints.py b/integrations/server/test_covidcast_endpoints.py index 68c542846..976e0ee53 100644 --- a/integrations/server/test_covidcast_endpoints.py +++ b/integrations/server/test_covidcast_endpoints.py @@ -142,6 +142,8 @@ def _insert_rows(self, rows: Iterable[CovidcastRow]): def _fetch(self, endpoint="/", **params): # make the request + if params: + params.update({'meta_key': 'meta_secret'}) response = requests.get( f"{BASE_URL}{endpoint}", params=params, @@ -297,9 +299,10 @@ def test_csv(self): first = rows[0] self._insert_rows(rows) + params = dict(signal=first.signal_pair, start_day="2020-04-01", end_day="2020-12-12", geo_type=first.geo_type, meta_key='meta_secret') response = requests.get( f"{BASE_URL}/csv", - params=dict(signal=first.signal_pair, start_day="2020-04-01", end_day="2020-12-12", geo_type=first.geo_type), + params=params, ) response.raise_for_status() out = response.text diff --git a/integrations/server/test_covidcast_meta.py b/integrations/server/test_covidcast_meta.py index 36a68c3a8..f2e4ba9d3 100644 --- a/integrations/server/test_covidcast_meta.py +++ b/integrations/server/test_covidcast_meta.py @@ -89,7 +89,7 @@ def test_round_trip(self): update_cache(args=None) # make the request - response = requests.get(BASE_URL, params={'endpoint': 'covidcast_meta'}) + response = requests.get(BASE_URL, params={'endpoint': 'covidcast_meta', 'meta_key': 'meta_secret'}) response.raise_for_status() response = response.json() @@ -150,7 +150,7 @@ def test_filter(self): def fetch(**kwargs): # make the request params = kwargs.copy() - params['endpoint'] = 'covidcast_meta' + params.update({'endpoint': 'covidcast_meta', 'meta_key': 'meta_secret'}) response = requests.get(BASE_URL, params=params) response.raise_for_status() return response.json() @@ -277,7 +277,7 @@ def test_suppress_work_in_progress(self): update_cache(args=None) # make the request - response = requests.get(BASE_URL, params={'endpoint': 'covidcast_meta'}) + response = requests.get(BASE_URL, params={'endpoint': 'covidcast_meta', 'meta_key': 'meta_secret'}) response.raise_for_status() response = response.json() diff --git a/integrations/server/test_covidcast_nowcast.py b/integrations/server/test_covidcast_nowcast.py index 7df695038..7626add64 100644 --- a/integrations/server/test_covidcast_nowcast.py +++ b/integrations/server/test_covidcast_nowcast.py @@ -58,7 +58,8 @@ def test_query(self): 'geo_type': 'county', 'time_values': 20200101, 'geo_value': '01001', - 'issues': 20200101 + 'issues': 20200101, + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -85,6 +86,7 @@ def test_query(self): 'geo_type': 'county', 'time_values': 20200101, 'geo_value': '01001', + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() @@ -111,7 +113,8 @@ def test_query(self): 'geo_type': 'county', 'time_values': 20200101, 'geo_value': '01001', - 'as_of': 20200101 + 'as_of': 20200101, + 'meta_key': 'meta_secret' }) response.raise_for_status() response = response.json() diff --git a/integrations/server/test_fluview.py b/integrations/server/test_fluview.py index 8bfc18376..af6ae9736 100644 --- a/integrations/server/test_fluview.py +++ b/integrations/server/test_fluview.py @@ -2,6 +2,7 @@ # standard library import unittest +from unittest.mock import patch # third party import mysql.connector @@ -9,7 +10,21 @@ # first party from delphi.epidata.client.delphi_epidata import Epidata +def _request(params): + """Request and parse epidata. + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class FluviewTests(unittest.TestCase): """Tests the `fluview` endpoint.""" diff --git a/integrations/server/test_fluview_meta.py b/integrations/server/test_fluview_meta.py index 137e9464a..549eab93e 100644 --- a/integrations/server/test_fluview_meta.py +++ b/integrations/server/test_fluview_meta.py @@ -2,6 +2,7 @@ # standard library import unittest +from unittest.mock import patch # third party import mysql.connector @@ -10,6 +11,21 @@ from delphi.epidata.client.delphi_epidata import Epidata +def _request(params): + """Request and parse epidata. + + We default to GET since it has better caching and logging + capabilities, but fall back to POST if the request is too + long and returns a 414. + """ + params.update({'meta_key': 'meta_secret'}) + try: + return Epidata._request_with_retry(params).json() + except Exception as e: + return {'result': 0, 'message': 'error: ' + str(e)} + + +@patch.object(Epidata, '_request', _request) class FluviewMetaTests(unittest.TestCase): """Tests the `fluview_meta` endpoint.""" @@ -67,9 +83,9 @@ def test_round_trip(self): self.assertEqual(response, { 'result': 1, 'epidata': [{ - 'latest_update': '2020-04-28', - 'latest_issue': 202022, - 'table_rows': 2, - }], + 'latest_update': '2020-04-28', + 'latest_issue': 202022, + 'table_rows': 2, + }], 'message': 'success', }) diff --git a/src/server/_config.py b/src/server/_config.py index f14ed3bb3..e93d891d5 100644 --- a/src/server/_config.py +++ b/src/server/_config.py @@ -13,6 +13,7 @@ SQLALCHEMY_DATABASE_URI = os.environ.get("SQLALCHEMY_DATABASE_URI", "sqlite:///test.db") SQLALCHEMY_ENGINE_OPTIONS = json.loads(os.environ.get("SQLALCHEMY_ENGINE_OPTIONS", "{}")) SECRET = os.environ.get("FLASK_SECRET", "secret") +META_SECRET = os.environ.get("META_SECRET", "meta_secret") URL_PREFIX = os.environ.get("FLASK_PREFIX", "/") AUTH = { diff --git a/src/server/_security.py b/src/server/_security.py new file mode 100644 index 000000000..972d38743 --- /dev/null +++ b/src/server/_security.py @@ -0,0 +1,9 @@ +from flask import request +from ._config import META_SECRET +from ._exceptions import UnAuthenticatedException + +def check_meta_key(): + if request.values: + meta_key = request.values.get("meta_key", None) + if not (meta_key and meta_key == META_SECRET): + raise UnAuthenticatedException() diff --git a/src/server/main.py b/src/server/main.py index 5434b744b..2c2652435 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -7,6 +7,7 @@ from ._config import URL_PREFIX, VERSION from ._common import app, set_compatibility_mode from ._exceptions import MissingOrWrongSourceException +from ._security import check_meta_key from .endpoints import endpoints __all__ = ["app"] @@ -21,11 +22,16 @@ if alias: endpoint_map[alias] = endpoint.handle +# Add meta security hook before any request +@app.before_request +def check_key(): + check_meta_key() @app.route(f"{URL_PREFIX}/api.php", methods=["GET", "POST"]) def handle_generic(): # mark as compatibility mode set_compatibility_mode() + check_meta_key() endpoint = request.values.get("endpoint", request.values.get("source")) if not endpoint or endpoint not in endpoint_map: raise MissingOrWrongSourceException(endpoint_map.keys()) diff --git a/tests/server/endpoints/test_covidcast.py b/tests/server/endpoints/test_covidcast.py index 43a13fccf..e6c24faff 100644 --- a/tests/server/endpoints/test_covidcast.py +++ b/tests/server/endpoints/test_covidcast.py @@ -34,7 +34,7 @@ def test_url(self): self.assertRegex(msg["message"], r"missing parameter.*") def test_time(self): - rv: Response = self.client.get("/covidcast/", query_string=dict(signal="src1:*", time="day:20200101", geo="state:*")) + rv: Response = self.client.get("/covidcast/", query_string=dict(signal="src1:*", time="day:20200101", geo="state:*", meta_key="meta_secret")) msg = rv.get_json() self.assertEqual(rv.status_code, 200) self.assertEqual(msg["result"], -2) # no result diff --git a/tests/server/endpoints/test_nidss_flu.py b/tests/server/endpoints/test_nidss_flu.py index bc0723bf3..33ee45ab3 100644 --- a/tests/server/endpoints/test_nidss_flu.py +++ b/tests/server/endpoints/test_nidss_flu.py @@ -26,20 +26,20 @@ def setUp(self): def test_urls(self): with self.subTest('direct url'): - rv: Response = self.client.get('/nidss_flu', follow_redirects=True) + rv: Response = self.client.get('/nidss_flu', query_string = dict(meta_key="meta_secret"), follow_redirects=True) msg = rv.get_json() self.assertEqual(rv.status_code, 200) self.assertEqual(msg['result'], -1) self.assertRegex(msg['message'], r"missing parameter.*") with self.subTest('with wrapper'): - rv: Response = self.client.get('/api.php?endpoint=nidss_flu', follow_redirects=True) + rv: Response = self.client.get('/api.php?endpoint=nidss_flu&meta_key=meta_secret', follow_redirects=True) msg = rv.get_json() self.assertEqual(rv.status_code, 200) self.assertEqual(msg['result'], -1) self.assertRegex(msg['message'], r"missing parameter.*") def test_(self): - rv: Response = self.client.get('/nidss_flu/', query_string=dict(regions="A", epiweeks="12")) + rv: Response = self.client.get('/nidss_flu/', query_string=dict(regions="A", epiweeks="12", meta_key="meta_secret")) msg = rv.get_json() self.assertEqual(rv.status_code, 200) self.assertEqual(msg['result'], -2) # no result