Skip to content

Commit 90af72e

Browse files
committed
Drop phase handling (#1210)
1 parent cd3a286 commit 90af72e

File tree

5 files changed

+18
-151
lines changed

5 files changed

+18
-151
lines changed

src/server/_common.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from ._config import SECRET, REVERSE_PROXY_DEPTH
1212
from ._db import engine
1313
from ._exceptions import DatabaseErrorException, EpiDataException
14-
from ._security import current_user, _is_public_route, resolve_auth_token, show_no_api_key_warning, update_key_last_time_used, ERROR_MSG_INVALID_KEY
14+
from ._security import current_user, _is_public_route, resolve_auth_token, update_key_last_time_used, ERROR_MSG_INVALID_KEY
1515

1616

1717
app = Flask("EpiData", static_url_path="")
@@ -128,11 +128,10 @@ def before_request_execute():
128128
user_id=(user and user.id)
129129
)
130130

131-
if not show_no_api_key_warning():
132-
if not _is_public_route() and api_key and not user:
133-
# if this is a privleged endpoint, and an api key was given but it does not look up to a user, raise exception:
134-
get_structured_logger("server_api").info("bad api key used", api_key=api_key)
135-
raise Unauthorized(ERROR_MSG_INVALID_KEY)
131+
if not _is_public_route() and api_key and not user:
132+
# if this is a privleged endpoint, and an api key was given but it does not look up to a user, raise exception:
133+
get_structured_logger("server_api").info("bad api key used", api_key=api_key)
134+
raise Unauthorized(ERROR_MSG_INVALID_KEY)
136135

137136
if request.path.startswith("/lib"):
138137
# files served from 'lib' directory don't need the database, so we can exit this early...

src/server/_config.py

-2
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@
8585
}
8686
NATION_REGION = "nat"
8787

88-
API_KEY_REQUIRED_STARTING_AT = date.fromisoformat(os.environ.get("API_KEY_REQUIRED_STARTING_AT", "2023-06-21"))
89-
TEMPORARY_API_KEY = os.environ.get("TEMPORARY_API_KEY", "TEMP-API-KEY-EXPIRES-2023-06-28")
9088
# password needed for the admin application if not set the admin routes won't be available
9189
ADMIN_PASSWORD = os.environ.get("API_KEY_ADMIN_PASSWORD", "abc")
9290
# secret for the google form to give to the admin/register endpoint

src/server/_limiter.py

+2-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from delphi.epidata.server.endpoints.covidcast_utils.dashboard_signals import DashboardSignals
2-
from flask import Response, request, make_response, jsonify
2+
from flask import Response, request
33
from flask_limiter import Limiter, HEADERS
44
from redis import Redis
55
from werkzeug.exceptions import Unauthorized, TooManyRequests
@@ -8,7 +8,7 @@
88
from ._config import RATE_LIMIT, RATELIMIT_STORAGE_URL, REDIS_HOST, REDIS_PASSWORD
99
from ._exceptions import ValidationFailedException
1010
from ._params import extract_dates, extract_integers, extract_strings
11-
from ._security import _is_public_route, current_user, require_api_key, show_no_api_key_warning, resolve_auth_token, ERROR_MSG_RATE_LIMIT, ERROR_MSG_MULTIPLES
11+
from ._security import _is_public_route, current_user, resolve_auth_token, ERROR_MSG_RATE_LIMIT, ERROR_MSG_MULTIPLES
1212

1313

1414
def deduct_on_success(response: Response) -> bool:
@@ -108,39 +108,15 @@ def ratelimit_handler(e):
108108
return TooManyRequests(ERROR_MSG_RATE_LIMIT)
109109

110110

111-
def requests_left():
112-
r = Redis(host=REDIS_HOST, password=REDIS_PASSWORD)
113-
allowed_count, period = RATE_LIMIT.split("/")
114-
try:
115-
remaining_count = int(allowed_count) - int(
116-
r.get(f"LIMITER/{_resolve_tracking_key()}/EpidataLimiter/{allowed_count}/1/{period}")
117-
)
118-
except TypeError:
119-
return 1
120-
return remaining_count
121-
122-
123111
@limiter.request_filter
124112
def _no_rate_limit() -> bool:
125-
if show_no_api_key_warning():
126-
# no rate limit in phase 0
127-
return True
128113
if _is_public_route():
129114
# no rate limit for public routes
130115
return True
131116
if current_user:
132117
# no rate limit if user is registered
133118
return True
134119

135-
if not require_api_key():
136-
# we are in phase 1 or 2
137-
if requests_left() > 0:
138-
# ...and user is below rate limit, we still want to record this query for the rate computation...
139-
return False
140-
# ...otherwise, they have exceeded the limit, but we still want to allow them through
141-
return True
142-
143-
# phase 3 (full api-keys behavior)
144120
multiples = get_multiples_count(request)
145121
if multiples < 0:
146122
# too many multiples

src/server/_printer.py

+11-79
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
from io import StringIO
33
from typing import Any, Dict, Iterable, List, Optional, Union
44

5-
from flask import Response, jsonify, stream_with_context, request
5+
from flask import Response, jsonify, stream_with_context
66
from flask.json import dumps
77
import orjson
88

99
from ._config import MAX_RESULTS, MAX_COMPATIBILITY_RESULTS
10-
# TODO: remove warnings after once we are past the API_KEY_REQUIRED_STARTING_AT date
11-
from ._security import show_hard_api_key_warning, show_soft_api_key_warning, ROLLOUT_WARNING_RATE_LIMIT, ROLLOUT_WARNING_MULTIPLES, _ROLLOUT_WARNING_AD_FRAGMENT, PHASE_1_2_STOPGAP
1210
from ._common import is_compatibility_mode, log_info_with_request
13-
from ._limiter import requests_left, get_multiples_count
1411
from delphi.epidata.common.logger import get_structured_logger
1512

1613

@@ -25,15 +22,7 @@ def print_non_standard(format: str, data):
2522
message = "no results"
2623
result = -2
2724
else:
28-
warning = ""
29-
if show_hard_api_key_warning():
30-
if requests_left() == 0:
31-
warning = f"{ROLLOUT_WARNING_RATE_LIMIT}"
32-
if get_multiples_count(request) < 0:
33-
warning = f"{warning} {ROLLOUT_WARNING_MULTIPLES}"
34-
if requests_left() == 0 or get_multiples_count(request) < 0:
35-
warning = f"{warning} {_ROLLOUT_WARNING_AD_FRAGMENT} {PHASE_1_2_STOPGAP}"
36-
message = warning.strip() or "success"
25+
message = "success"
3726
result = 1
3827
if result == -1 and is_compatibility_mode():
3928
return jsonify(dict(result=result, message=message))
@@ -127,40 +116,21 @@ class ClassicPrinter(APrinter):
127116
"""
128117

129118
def _begin(self):
130-
if is_compatibility_mode() and not show_hard_api_key_warning():
119+
if is_compatibility_mode():
131120
return "{ "
132-
r = '{ "epidata": ['
133-
if show_hard_api_key_warning():
134-
warning = ""
135-
if requests_left() == 0:
136-
warning = f"{warning} {ROLLOUT_WARNING_RATE_LIMIT}"
137-
if get_multiples_count(request) < 0:
138-
warning = f"{warning} {ROLLOUT_WARNING_MULTIPLES}"
139-
if requests_left() == 0 or get_multiples_count(request) < 0:
140-
warning = f"{warning} {_ROLLOUT_WARNING_AD_FRAGMENT} {PHASE_1_2_STOPGAP}"
141-
if warning != "":
142-
return f'{r} "{warning.strip()}",'
143-
return r
121+
return '{ "epidata": ['
144122

145123
def _format_row(self, first: bool, row: Dict):
146-
if first and is_compatibility_mode() and not show_hard_api_key_warning():
124+
if first and is_compatibility_mode():
147125
sep = b'"epidata": ['
148126
else:
149127
sep = b"," if not first else b""
150128
return sep + orjson.dumps(row)
151129

152130
def _end(self):
153-
warning = ""
154-
if show_soft_api_key_warning():
155-
if requests_left() == 0:
156-
warning = f"{warning} {ROLLOUT_WARNING_RATE_LIMIT}"
157-
if get_multiples_count(request) < 0:
158-
warning = f"{warning} {ROLLOUT_WARNING_MULTIPLES}"
159-
if requests_left() == 0 or get_multiples_count(request) < 0:
160-
warning = f"{warning} {_ROLLOUT_WARNING_AD_FRAGMENT} {PHASE_1_2_STOPGAP}"
161-
message = warning.strip() or "success"
131+
message = "success"
162132
prefix = "], "
163-
if self.count == 0 and is_compatibility_mode() and not show_hard_api_key_warning():
133+
if self.count == 0 and is_compatibility_mode():
164134
# no array to end
165135
prefix = ""
166136

@@ -194,7 +164,7 @@ def _format_row(self, first: bool, row: Dict):
194164
self._tree[group].append(row)
195165
else:
196166
self._tree[group] = [row]
197-
if first and is_compatibility_mode() and not show_hard_api_key_warning():
167+
if first and is_compatibility_mode():
198168
return b'"epidata": ['
199169
return None
200170

@@ -205,10 +175,7 @@ def _end(self):
205175
tree = orjson.dumps(self._tree)
206176
self._tree = dict()
207177
r = super(ClassicTreePrinter, self)._end()
208-
r = tree + r
209-
if show_hard_api_key_warning() and (requests_left() == 0 or get_multiples_count(request) < 0):
210-
r = b", " + r
211-
return r
178+
return tree + r
212179

213180

214181
class CSVPrinter(APrinter):
@@ -243,17 +210,6 @@ def _format_row(self, first: bool, row: Dict):
243210
columns = list(row.keys())
244211
self._writer = DictWriter(self._stream, columns, lineterminator="\n")
245212
self._writer.writeheader()
246-
if show_hard_api_key_warning():
247-
warning = ""
248-
if requests_left() == 0:
249-
warning = f"{warning} {ROLLOUT_WARNING_RATE_LIMIT}"
250-
if get_multiples_count(request) < 0:
251-
warning = f"{warning} {ROLLOUT_WARNING_MULTIPLES}"
252-
if requests_left() == 0 or get_multiples_count(request) < 0:
253-
warning = f"{warning} {_ROLLOUT_WARNING_AD_FRAGMENT} {PHASE_1_2_STOPGAP}"
254-
if warning.strip() != "":
255-
self._writer.writerow({columns[0]: warning})
256-
257213
self._writer.writerow(row)
258214

259215
# remove the stream content to print just one line at a time
@@ -274,18 +230,7 @@ class JSONPrinter(APrinter):
274230
"""
275231

276232
def _begin(self):
277-
r = b"["
278-
if show_hard_api_key_warning():
279-
warning = ""
280-
if requests_left() == 0:
281-
warning = f"{warning} {ROLLOUT_WARNING_RATE_LIMIT}"
282-
if get_multiples_count(request) < 0:
283-
warning = f"{warning} {ROLLOUT_WARNING_MULTIPLES}"
284-
if requests_left() == 0 or get_multiples_count(request) < 0:
285-
warning = f"{warning} {_ROLLOUT_WARNING_AD_FRAGMENT} {PHASE_1_2_STOPGAP}"
286-
if warning.strip() != "":
287-
r = b'["' + bytes(warning, "utf-8") + b'",'
288-
return r
233+
return b"["
289234

290235
def _format_row(self, first: bool, row: Dict):
291236
sep = b"," if not first else b""
@@ -303,19 +248,6 @@ class JSONLPrinter(APrinter):
303248
def make_response(self, gen, headers=None):
304249
return Response(gen, mimetype=" text/plain; charset=utf8", headers=headers)
305250

306-
def _begin(self):
307-
if show_hard_api_key_warning():
308-
warning = ""
309-
if requests_left() == 0:
310-
warning = f"{warning} {ROLLOUT_WARNING_RATE_LIMIT}"
311-
if get_multiples_count(request) < 0:
312-
warning = f"{warning} {ROLLOUT_WARNING_MULTIPLES}"
313-
if requests_left() == 0 or get_multiples_count(request) < 0:
314-
warning = f"{warning} {_ROLLOUT_WARNING_AD_FRAGMENT} {PHASE_1_2_STOPGAP}"
315-
if warning.strip() != "":
316-
return bytes(warning, "utf-8") + b"\n"
317-
return None
318-
319251
def _format_row(self, first: bool, row: Dict):
320252
# each line is a JSON file with a new line to separate them
321253
return orjson.dumps(row, option=orjson.OPT_APPEND_NEWLINE)
@@ -338,4 +270,4 @@ def create_printer(format: str) -> APrinter:
338270
return CSVPrinter()
339271
if format == "jsonl":
340272
return JSONLPrinter()
341-
return ClassicPrinter()
273+
return ClassicPrinter()

src/server/_security.py

-38
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,13 @@
99
from werkzeug.local import LocalProxy
1010

1111
from ._config import (
12-
API_KEY_REQUIRED_STARTING_AT,
1312
REDIS_HOST,
1413
REDIS_PASSWORD,
1514
API_KEY_REGISTRATION_FORM_LINK_LOCAL,
16-
TEMPORARY_API_KEY,
1715
URL_PREFIX,
1816
)
1917
from .admin.models import User
2018

21-
API_KEY_HARD_WARNING = API_KEY_REQUIRED_STARTING_AT - timedelta(days=14)
22-
API_KEY_SOFT_WARNING = API_KEY_HARD_WARNING - timedelta(days=14)
23-
24-
# rollout warning messages
25-
ROLLOUT_WARNING_RATE_LIMIT = "This request exceeded the rate limit on anonymous requests, which will be enforced starting {}.".format(API_KEY_REQUIRED_STARTING_AT)
26-
ROLLOUT_WARNING_MULTIPLES = "This request exceeded the anonymous limit on selected multiples, which will be enforced starting {}.".format(API_KEY_REQUIRED_STARTING_AT)
27-
_ROLLOUT_WARNING_AD_FRAGMENT = "To be exempt from this limit, authenticate your requests with a free API key, now available at {}.".format(API_KEY_REGISTRATION_FORM_LINK_LOCAL)
28-
29-
PHASE_1_2_STOPGAP = (
30-
"A temporary public key `{}` is available for use between now and {} to give you time to register or adapt your requests without this message continuing to break your systems."
31-
).format(TEMPORARY_API_KEY, (API_KEY_REQUIRED_STARTING_AT + timedelta(days=7)))
32-
3319

3420
# steady-state error messages
3521
ERROR_MSG_RATE_LIMIT = "Rate limit exceeded for anonymous queries. To remove this limit, register a free API key at {}".format(API_KEY_REGISTRATION_FORM_LINK_LOCAL)
@@ -54,30 +40,6 @@ def resolve_auth_token() -> Optional[str]:
5440
return None
5541

5642

57-
def show_no_api_key_warning() -> bool:
58-
# aka "phase 0"
59-
n = date.today()
60-
return not current_user and n < API_KEY_SOFT_WARNING
61-
62-
63-
def show_soft_api_key_warning() -> bool:
64-
# aka "phase 1"
65-
n = date.today()
66-
return not current_user and API_KEY_SOFT_WARNING <= n < API_KEY_HARD_WARNING
67-
68-
69-
def show_hard_api_key_warning() -> bool:
70-
# aka "phase 2"
71-
n = date.today()
72-
return not current_user and API_KEY_HARD_WARNING <= n < API_KEY_REQUIRED_STARTING_AT
73-
74-
75-
def require_api_key() -> bool:
76-
# aka "phase 3"
77-
n = date.today()
78-
return API_KEY_REQUIRED_STARTING_AT <= n
79-
80-
8143
def _get_current_user():
8244
if "user" not in g:
8345
api_key = resolve_auth_token()

0 commit comments

Comments
 (0)