Skip to content

Commit ff97c87

Browse files
fix: hide cached auth-only result pages [skip-ci]
1 parent 88805ac commit ff97c87

7 files changed

Lines changed: 110 additions & 8 deletions

File tree

result_server/routes/admin.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Blueprint,
1111
abort,
1212
flash,
13+
make_response,
1314
redirect,
1415
render_template,
1516
request,
@@ -22,6 +23,12 @@
2223
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
2324

2425

26+
def _add_no_store_headers(response):
27+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
28+
response.headers["Pragma"] = "no-cache"
29+
return response
30+
31+
2532
def _get_users_with_totp_status():
2633
"""全ユーザーを取得し、各ユーザーに has_totp フラグを付与して返す。"""
2734
store = get_user_store()
@@ -41,11 +48,11 @@ def admin_required(f):
4148
@wraps(f)
4249
def decorated(*args, **kwargs):
4350
if not session.get("authenticated"):
44-
return redirect(url_for("auth.login"))
51+
return _add_no_store_headers(make_response(redirect(url_for("auth.login"))))
4552
affiliations = session.get("user_affiliations", [])
4653
if "admin" not in affiliations:
4754
abort(403)
48-
return f(*args, **kwargs)
55+
return _add_no_store_headers(make_response(f(*args, **kwargs)))
4956

5057
return decorated
5158

result_server/routes/estimated.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from flask import (
22
Blueprint, render_template, request, session,
3-
redirect, url_for, flash, abort, current_app
3+
redirect, url_for, flash, abort, current_app, make_response
44
)
55
from utils.results_loader import load_estimated_results_table, get_filter_options, ESTIMATED_FIELD_MAP
66
from routes.results import extract_query_params
@@ -11,10 +11,29 @@
1111
estimated_bp = Blueprint("estimated", __name__)
1212

1313

14+
def _render_estimated_auth_required():
15+
systems_info = get_all_systems_info()
16+
response = make_response(render_template(
17+
"estimated_results.html",
18+
rows=[], columns=[],
19+
authenticated=False, systems_info=systems_info,
20+
pagination={"page": 1, "per_page": 100, "total": 0, "total_pages": 1},
21+
filter_options={"systems": [], "codes": [], "exps": []},
22+
current_system=None, current_code=None,
23+
current_exp=None, current_per_page=100,
24+
))
25+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
26+
response.headers["Pragma"] = "no-cache"
27+
return response
28+
29+
1430
# GET /estimated/
1531
@estimated_bp.route("/", methods=["GET"], strict_slashes=False)
1632
def estimated_results():
1733
authenticated = session.get("authenticated", False)
34+
if not authenticated:
35+
return _render_estimated_auth_required()
36+
1837
email = session.get("user_email")
1938

2039
store = get_user_store()
@@ -58,19 +77,24 @@ def estimated_results():
5877
field_map=ESTIMATED_FIELD_MAP,
5978
)
6079
systems_info = get_all_systems_info()
61-
return render_template(
80+
response = make_response(render_template(
6281
"estimated_results.html",
6382
rows=rows, columns=columns,
6483
authenticated=authenticated, systems_info=systems_info,
6584
pagination=pagination_info, filter_options=filter_options,
6685
current_system=filter_system, current_code=filter_code,
6786
current_exp=filter_exp, current_per_page=per_page,
68-
)
87+
))
88+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
89+
response.headers["Pragma"] = "no-cache"
90+
return response
6991

7092

7193
# GET /estimated/<filename>
7294
@estimated_bp.route("/<filename>")
7395
def show_estimated_result(filename):
96+
if not session.get("authenticated", False):
97+
abort(403, "Authentication required to view estimated data")
7498
estimated_dir = current_app.config["ESTIMATED_DIR"]
7599
check_file_permission(filename, estimated_dir)
76100
return load_result_file(filename, estimated_dir)

result_server/routes/results.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from flask import (
44
Blueprint, render_template, request, session,
5-
redirect, url_for, flash, abort, current_app
5+
redirect, url_for, flash, abort, current_app, make_response
66
)
77
from utils.results_loader import load_results_table, load_single_result, load_multiple_results, get_filter_options, ALLOWED_PER_PAGE, DEFAULT_PER_PAGE
88
from utils.user_store import get_user_store
@@ -14,6 +14,22 @@
1414
results_bp = Blueprint("results", __name__)
1515

1616

17+
def _render_confidential_auth_required():
18+
systems_info = get_all_systems_info()
19+
response = make_response(render_template(
20+
"results_confidential.html",
21+
rows=[], columns=[], systems_info=systems_info,
22+
pagination={"page": 1, "per_page": DEFAULT_PER_PAGE, "total": 0, "total_pages": 1},
23+
filter_options={"systems": [], "codes": [], "exps": []},
24+
current_system=None, current_code=None,
25+
current_exp=None, current_per_page=DEFAULT_PER_PAGE,
26+
authenticated=False,
27+
))
28+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
29+
response.headers["Pragma"] = "no-cache"
30+
return response
31+
32+
1733
def extract_query_params():
1834
"""request.args から page, per_page, system, code, exp を一括抽出する。
1935
@@ -77,6 +93,8 @@ def _render_results_list(public_only, template_name, redirect_endpoint):
7793

7894
if not public_only:
7995
authenticated = session.get("authenticated", False)
96+
if not authenticated:
97+
return _render_confidential_auth_required()
8098
email = session.get("user_email")
8199
store = get_user_store()
82100
affs = store.get_affiliations(email) if email else []
@@ -99,14 +117,18 @@ def _render_results_list(public_only, template_name, redirect_endpoint):
99117

100118
filter_options = get_filter_options(received_dir, filter_code=filter_code, **filter_kwargs)
101119
systems_info = get_all_systems_info()
102-
return render_template(
120+
response = make_response(render_template(
103121
template_name,
104122
rows=rows, columns=columns, systems_info=systems_info,
105123
pagination=pagination_info, filter_options=filter_options,
106124
current_system=filter_system, current_code=filter_code,
107125
current_exp=filter_exp, current_per_page=per_page,
108126
**template_extra,
109-
)
127+
))
128+
if not public_only:
129+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
130+
response.headers["Pragma"] = "no-cache"
131+
return response
110132

111133

112134
# ==========================================

result_server/templates/estimated_results.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
{% endblock %}
1313

1414
{% block content %}
15+
{% if authenticated %}
1516
{% include "_filter_dropdowns.html" %}
1617
</div>
1718

@@ -134,4 +135,5 @@
134135
window.location.href = url;
135136
}
136137
</script>
138+
{% endif %}
137139
{% endblock %}

result_server/templates/results_confidential.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@
1212
{% endblock %}
1313

1414
{% block content %}
15+
{% if authenticated %}
1516
{% include "_results_table.html" %}
17+
{% endif %}
1618
{% endblock %}

result_server/tests/test_pagination.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,9 @@ def test_estimated_route_redirect(self, flask_app, tmp_dir):
428428
"""推定結果ルートのページ範囲外リダイレクト"""
429429
self._make_estimated_files(tmp_dir, 5)
430430
with flask_app.test_client() as client:
431+
with client.session_transaction() as sess:
432+
sess["authenticated"] = True
433+
sess["user_email"] = "user@example.com"
431434
resp = client.get("/estimated/?page=999")
432435
assert resp.status_code == 302
433436

@@ -436,6 +439,32 @@ def test_estimated_route_redirect(self, flask_app, tmp_dir):
436439
# 既存機能の互換性テスト
437440
# ============================================================
438441

442+
class TestEstimatedAuth:
443+
def test_estimated_route_hides_table_when_unauthenticated(self, flask_app, tmp_dir):
444+
uid = str(uuid.uuid4())
445+
_write_json(tmp_dir, f"estimate_20250101_000000_{uid}.json", {
446+
"code": "qws",
447+
"current_system": {"system": "SysA", "fom": 1.0, "target_nodes": "1", "scaling_method": "m", "benchmark": {"system": "SysA", "fom": 1.0, "nodes": "1"}},
448+
"future_system": {"system": "SysB", "fom": 2.0, "target_nodes": "2", "scaling_method": "m", "benchmark": {"system": "SysB", "fom": 2.0, "nodes": "2"}},
449+
"performance_ratio": 2.0,
450+
})
451+
with flask_app.test_client() as client:
452+
resp = client.get("/estimated/")
453+
assert resp.status_code == 200
454+
html = resp.get_data(as_text=True)
455+
assert "Authentication required to view estimated data." in html
456+
assert '<table id="resultsTable"' not in html
457+
assert "no-store" in resp.headers.get("Cache-Control", "")
458+
459+
def test_estimated_json_requires_authentication(self, flask_app, tmp_dir):
460+
uid = str(uuid.uuid4())
461+
fname = f"estimate_20250101_000000_{uid}.json"
462+
_write_json(tmp_dir, fname, {"code": "qws", "system": "SysA", "exp": "CASE0"})
463+
with flask_app.test_client() as client:
464+
resp = client.get(f"/estimated/{fname}")
465+
assert resp.status_code == 403
466+
467+
439468
class TestExistingFeatureCompatibility:
440469
def test_load_results_table_returns_3_tuple(self, flask_app, tmp_dir):
441470
"""load_results_table が (rows, columns, pagination_info) を返す"""

result_server/tests/test_usage_route.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,25 @@ def _write_result(directory, filename, data):
105105

106106

107107
class TestUsageRoute:
108+
def test_confidential_results_hides_table_when_unauthenticated(self, client, tmp_dirs):
109+
received, _ = tmp_dirs
110+
_write_result(
111+
received,
112+
"result_20260401_123456_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.json",
113+
{"code": "qws", "system": "Fugaku", "Exp": "CASE0", "FOM": 1.0},
114+
)
115+
resp = client.get("/results/confidential")
116+
text = resp.get_data(as_text=True)
117+
assert resp.status_code == 200
118+
assert "Authentication required to view confidential data." in text
119+
assert '<table id="resultsTable"' not in text
120+
assert "no-store" in resp.headers.get("Cache-Control", "")
121+
108122
def test_unauthenticated_user_is_redirected_to_login(self, client):
109123
resp = client.get("/results/usage")
110124
assert resp.status_code == 302
111125
assert "/auth/login" in resp.headers["Location"]
126+
assert "no-store" in resp.headers.get("Cache-Control", "")
112127

113128
def test_non_admin_user_gets_403(self, client):
114129
_login_session(client, "user@example.com", ["dev"])
@@ -120,6 +135,7 @@ def test_admin_user_can_access_usage_page(self, client):
120135
resp = client.get("/results/usage")
121136
assert resp.status_code == 200
122137
assert "Usage Report" in resp.get_data(as_text=True)
138+
assert "no-store" in resp.headers.get("Cache-Control", "")
123139

124140
def test_usage_route_uses_default_parameters(self, app, client, monkeypatch):
125141
_login_session(client, "admin@example.com", ["admin"])

0 commit comments

Comments
 (0)