Skip to content

Commit cdc24e9

Browse files
authored
CWS: Mark ajax request endpoints as @api_login_required (#1522)
This prevents cases where they would redirect to the login screen unexpectedly. Also improved handling of errors in the submission details popup.
1 parent f942b02 commit cdc24e9

File tree

7 files changed

+45
-35
lines changed

7 files changed

+45
-35
lines changed

cms/server/contest/handlers/api.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,15 @@
2020
2121
"""
2222

23-
from collections.abc import Callable
24-
import functools
2523
import ipaddress
2624
import logging
27-
import typing
2825

2926
from cms.db.submission import Submission
3027
from cms.server import multi_contest
3128
from cms.server.contest.authentication import validate_login
3229
from cms.server.contest.submission import \
3330
UnacceptableSubmission, accept_submission
34-
from .contest import ContestHandler
31+
from .contest import ContestHandler, api_login_required
3532
from ..phase_management import actual_phase_required
3633

3734
logger = logging.getLogger(__name__)
@@ -47,27 +44,6 @@ def __init__(self, *args, **kwargs):
4744
self.api_request = True
4845

4946

50-
_P = typing.ParamSpec("_P")
51-
_R = typing.TypeVar("_R")
52-
_Self = typing.TypeVar("_Self", bound="ApiContestHandler")
53-
54-
def api_login_required(
55-
func: Callable[typing.Concatenate[_Self, _P], _R],
56-
) -> Callable[typing.Concatenate[_Self, _P], _R | None]:
57-
"""A decorator filtering out unauthenticated requests.
58-
59-
"""
60-
61-
@functools.wraps(func)
62-
def wrapped(self: _Self, *args: _P.args, **kwargs: _P.kwargs):
63-
if not self.current_user:
64-
self.json({"error": "An authenticated user is required"}, 403)
65-
else:
66-
return func(self, *args, **kwargs)
67-
68-
return wrapped
69-
70-
7147
class ApiLoginHandler(ApiContestHandler):
7248
"""Login handler.
7349

cms/server/contest/handlers/contest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
3030
"""
3131

32+
from collections.abc import Callable
33+
import functools
3234
import ipaddress
3335
import json
3436
import logging
37+
import typing
3538

3639
import collections
3740

@@ -334,3 +337,26 @@ def check_xsrf_cookie(self):
334337

335338
class FileHandler(ContestHandler, FileHandlerMixin):
336339
pass
340+
341+
_P = typing.ParamSpec("_P")
342+
_R = typing.TypeVar("_R")
343+
_Self = typing.TypeVar("_Self", bound="ContestHandler")
344+
345+
def api_login_required(
346+
func: Callable[typing.Concatenate[_Self, _P], _R],
347+
) -> Callable[typing.Concatenate[_Self, _P], _R | None]:
348+
"""A decorator filtering out unauthenticated requests.
349+
350+
Unlike @tornado.web.authenticated, this returns a JSON error instead of
351+
redirecting.
352+
353+
"""
354+
355+
@functools.wraps(func)
356+
def wrapped(self: _Self, *args: _P.args, **kwargs: _P.kwargs):
357+
if not self.current_user:
358+
self.json({"error": "An authenticated user is required"}, 403)
359+
else:
360+
return func(self, *args, **kwargs)
361+
362+
return wrapped

cms/server/contest/handlers/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
UnacceptablePrintJob
5959
from cmscommon.crypto import hash_password, validate_password
6060
from cmscommon.datetime import make_datetime, make_timestamp
61-
from .contest import ContestHandler
61+
from .contest import ContestHandler, api_login_required
6262
from ..phase_management import actual_phase_required
6363

6464

@@ -294,7 +294,7 @@ class NotificationsHandler(ContestHandler):
294294

295295
refresh_cookie = False
296296

297-
@tornado.web.authenticated
297+
@api_login_required
298298
@multi_contest
299299
def get(self):
300300
participation: Participation = self.current_user

cms/server/contest/handlers/tasksubmission.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
UnacceptableToken, TokenAlreadyPlayed, accept_token, tokens_available
5858
from cmscommon.crypto import encrypt_number
5959
from cmscommon.mimetypes import get_type_for_file_name
60-
from .contest import ContestHandler, FileHandler
60+
from .contest import ContestHandler, FileHandler, api_login_required
6161
from ..phase_management import actual_phase_required
6262

6363

@@ -236,7 +236,7 @@ def add_task_score(self, participation: Participation, task: Task, data: dict):
236236
data["task_tokened_score"], score_type.max_score, None,
237237
task.score_precision, translation=self.translation)
238238

239-
@tornado.web.authenticated
239+
@api_login_required
240240
@actual_phase_required(0, 1, 2, 3, 4)
241241
@multi_contest
242242
def get(self, task_name, opaque_id):
@@ -296,7 +296,7 @@ class SubmissionDetailsHandler(ContestHandler):
296296

297297
refresh_cookie = False
298298

299-
@tornado.web.authenticated
299+
@api_login_required
300300
@actual_phase_required(0, 1, 2, 3, 4)
301301
@multi_contest
302302
def get(self, task_name, opaque_id):

cms/server/contest/handlers/taskusertest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
TestingNotAllowed, UnacceptableUserTest, accept_user_test
4949
from cmscommon.crypto import encrypt_number
5050
from cmscommon.mimetypes import get_type_for_file_name
51-
from .contest import ContestHandler, FileHandler
51+
from .contest import ContestHandler, FileHandler, api_login_required
5252
from ..phase_management import actual_phase_required
5353

5454

@@ -166,7 +166,7 @@ class UserTestStatusHandler(ContestHandler):
166166

167167
refresh_cookie = False
168168

169-
@tornado.web.authenticated
169+
@api_login_required
170170
@actual_phase_required(0)
171171
@multi_contest
172172
def get(self, task_name, user_test_num):
@@ -221,7 +221,7 @@ class UserTestDetailsHandler(ContestHandler):
221221

222222
refresh_cookie = False
223223

224-
@tornado.web.authenticated
224+
@api_login_required
225225
@actual_phase_required(0)
226226
@multi_contest
227227
def get(self, task_name, user_test_num):

cms/server/contest/templates/task_submissions.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@
4040
var modal = $("#submission_detail");
4141
var modal_body = modal.children(".modal-body");
4242
modal_body.html('<div class="loading"><img src="{{ url("static", "loading.gif") }}"/>{% trans %}loading...{% endtrans %}</div>');
43-
modal_body.load(utils.contest_url("tasks", "{{ task.name }}", "submissions", submission_id, "details"), function() {
43+
modal_body.load(utils.contest_url("tasks", "{{ task.name }}", "submissions", submission_id, "details"), function(response, status, xhr) {
44+
if(status != "success") {
45+
$(this).html("{% trans %}Error loading details, please refresh the page.{% endtrans %}");
46+
return;
47+
}
4448
$(".score_details .subtask .subtask-head").each(function () {
4549
$(this).prepend("<i class=\"icon-chevron-right\"></i>");
4650
});

cms/server/contest/templates/test_interface.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
var modal = $("#user_test_detail");
2626
var modal_body = modal.children(".modal-body");
2727
modal_body.html('<div class="loading"><img src="{{ url("static", "loading.gif") }}"/>{% trans %}loading...{% endtrans %}</div>');
28-
modal_body.load(utils.contest_url("tasks", task_id, "tests", user_test_id, "details"));
28+
modal_body.load(utils.contest_url("tasks", task_id, "tests", user_test_id, "details"), function(response, status, xhr) {
29+
if(status != "success") {
30+
$(this).html("{% trans %}Error loading details, please refresh the page.{% endtrans %}");
31+
}
32+
});
2933
modal.modal("show");
3034
});
3135

0 commit comments

Comments
 (0)