diff --git a/.gitignore b/.gitignore index 43588ea..11e7685 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,9 @@ result_server/received/ # Python bytecode cache result_server/routes/__pycache__/ +result_server/utils/__pycache__/ -# editor backup files +# editor backup files and caches *~ *.bak +*.pyc \ No newline at end of file diff --git a/programs/qws/run.sh b/programs/qws/run.sh index 456f49e..57a46bc 100644 --- a/programs/qws/run.sh +++ b/programs/qws/run.sh @@ -66,7 +66,14 @@ case "$system" in #ls ../results/ ;; RC_GH200) - echo FOM:11.22 FOM_version:dummy Exp:DummyFrom_qc-gh200 node_count:$nodes >> ../results/result + echo FOM:11.22 FOM_version:dummy_qc-gh200 Exp:confidential_null node_count:$nodes >> ../results/result + # with confidential key + echo FOM:11.22 FOM_version:dummy_qc-gh200 Exp:confidential_TeamA node_count:$nodes confidential:TeamA>> ../results/result + echo FOM:11.22 FOM_version:dummy_qc-gh200 Exp:confidential_TeamB node_count:$nodes confidential:TeamB>> ../results/result + echo FOM:11.22 FOM_version:dummy_qc-gh200 Exp:confidential_TeamC node_count:$nodes confidential:TeamC>> ../results/result + echo FOM:11.22 FOM_version:dummy_qc-gh200 Exp:confidential_TeamD node_count:$nodes confidential:TeamD>> ../results/result + echo FOM:11.22 FOM_version:dummy_qc-gh200 Exp:confidential_TeamE node_count:$nodes confidential:TeamE>> ../results/result + echo FOM:11.22 FOM_version:dummy_qc-gh200 Exp:confidential_TeamF node_count:$nodes confidential:TeamF>> ../results/result ;; *) echo "Unknown Running system: $system" diff --git a/result_server/app.py b/result_server/app.py index 29dfff5..01660de 100644 --- a/result_server/app.py +++ b/result_server/app.py @@ -18,6 +18,11 @@ # Create the Flask app and specify the templates folder app = Flask(__name__, template_folder="templates") + +# Set a secret key for session management (required for flash and OTP sessions) +# In production, use a secure random key, e.g., os.urandom(24) +app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev_secret_key") + # Register route blueprints app.register_blueprint(receive_bp) app.register_blueprint(results_bp) diff --git a/result_server/routes/results.py b/result_server/routes/results.py index 1557f47..c4b201d 100644 --- a/result_server/routes/results.py +++ b/result_server/routes/results.py @@ -1,111 +1,101 @@ import os -import json -from flask import Blueprint, render_template, send_from_directory, abort, jsonify -import re -from datetime import datetime +from flask import ( + Blueprint, render_template, request, session, + redirect, url_for, flash, abort +) +from utils.results_loader import load_results_table +from utils.otp_manager import send_otp, verify_otp, get_affiliations +from utils.result_file import load_result_file, get_file_confidential_tags results_bp = Blueprint("results", __name__) SAVE_DIR = "received" + +# ========================================== +# 公開用の結果一覧ページ +# ========================================== @results_bp.route("/results") -def list_results(): - files = os.listdir(SAVE_DIR) - json_files = sorted([f for f in files if f.endswith(".json")], reverse=True) - tgz_files = [f for f in files if f.endswith(".tgz")] +def results(): + rows, columns = load_results_table(public_only=True) + return render_template("results.html", rows=rows, columns=columns) - rows = [] - for json_file in json_files: - json_path = os.path.join(SAVE_DIR, json_file) - try: - with open(json_path, "r", encoding="utf-8") as f: - data = json.load(f) - code = data.get("code", "N/A") - sys = data.get("system", "N/A") - fom = data.get("FOM", "N/A") - fom_version = data.get("FOM_version", "N/A") - exp = data.get("Exp", "N/A") - cpu = data.get("cpu_name", "N/A") - gpu = data.get("gpu_name", "N/A") - nodes = data.get("node_count", "N/A") - cpus = data.get("cpus_per_node", "N/A") - gpus = data.get("gpus_per_node", "N/A") - cpu_cores = data.get("cpu_cores", "N/A") - except Exception: - code = sys = fom = cpu = gpu = nodes = cpus = gpus = cpu_cores = "Invalid" - match = re.search(r"\d{8}_\d{6}", json_file) - if match: - try: - ts = datetime.strptime(match.group(), "%Y%m%d_%H%M%S") - timestamp = ts.strftime("%Y-%m-%d %H:%M:%S") - except ValueError: - timestamp = "Invalid" - else: - timestamp = "Unknown" +# ========================================== +# 機密データ付きの結果ページ(OTP認証付き) +# ========================================== +@results_bp.route("/results_confidential", methods=["GET", "POST"]) +def results_confidential(): + # フォーム送信処理 + if request.method == "POST": + email = request.form.get("email") + otp = request.form.get("otp") - uuid_match = re.search(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", json_file, re.IGNORECASE) - if uuid_match: - uid = uuid_match.group(0) - else: - uid = None + if email and not otp: + # STEP1: メール送信 + success, msg = send_otp(email) + if success: + flash("OTPをメールに送信しました") + session["otp_email"] = email + session["otp_stage"] = "otp" + else: + flash(msg) + session.pop("otp_email", None) + session["otp_stage"] = "email" + return redirect(url_for("results.results_confidential")) - if uid: - tgz_file = next((f for f in tgz_files if uid in f), None) - else: - tgz_file = None + elif otp: + # STEP2: OTP検証 + otp_email = session.get("otp_email") + if otp_email and verify_otp(otp_email, otp): + session["authenticated_confidential"] = True + flash("認証成功") + session.pop("otp_stage", None) # 認証済みなので削除 + else: + flash("OTP認証失敗") + session.pop("otp_email", None) + session.pop("authenticated_confidential", None) + session["otp_stage"] = "email" + return redirect(url_for("results.results_confidential")) - row = { - "timestamp": timestamp, - "code": code, - "exp": exp, - "fom": fom, - "fom_version": fom_version, - "system": sys, - "cpu": cpu, - "gpu": gpu, - "nodes": nodes, - "cpus": cpus, - "gpus": gpus, - "cpu_cores": cpu_cores, - "json_link": json_file, - "data_link": tgz_file, - } + # OTPステージ判定 + authenticated = session.get("authenticated_confidential", False) + otp_email = session.get("otp_email") - rows.append(row) + if authenticated: + otp_stage = None # 認証済みなのでフォームは出さない + elif otp_email: + otp_stage = "otp" # OTP入力待ち + else: + otp_stage = "email" # メール入力待ち - columns = [ - ("Timestamp", "timestamp"), - ("CODE", "code"), - ("Exp", "exp"), - ("FOM", "fom"), - ("FOM version", "fom_version"), - ("SYSTEM", "system"), - ("CPU Name", "cpu"), - ("GPU Name", "gpu"), - ("Nodes", "nodes"), - ("CPU/node", "cpus"), - ("GPU/node", "gpus"), - ("CPU Core Count", "cpu_cores"), - ("JSON", "json_link"), - ("PA Data", "data_link"), - ] - return render_template("results.html", columns=columns, rows=rows) + # 結果テーブル読み込み(confidential制御は utils 内で処理) + rows, columns = load_results_table( + public_only=False, + session_email=otp_email, + authenticated=authenticated + ) + return render_template( + "results_confidential.html", + rows=rows, + columns=columns, + authenticated=authenticated, + otp_stage=otp_stage + ) +# ========================================== +# 個別結果ファイルの表示/ダウンロード +# ========================================== @results_bp.route("/results/") def show_result(filename): - filepath = os.path.join(SAVE_DIR, filename) - if not os.path.exists(filepath): - abort(404) + tags = get_file_confidential_tags(filename) - if filename.endswith(".json"): - try: - with open(filepath, "r", encoding="utf-8") as f: - data = json.load(f) - return jsonify(data) - except json.JSONDecodeError: - abort(400, "Invalid JSON") - else: - return send_from_directory(SAVE_DIR, filename) - + if tags: + authenticated = session.get("authenticated_confidential", False) + email = session.get("otp_email") + affs = get_affiliations(email) if email else [] + if not authenticated or not (set(tags) & set(affs)): + abort(403, "このファイルにアクセスする権限がありません") + + return load_result_file(filename) diff --git a/result_server/templates/_results_table.html b/result_server/templates/_results_table.html new file mode 100644 index 0000000..0fb83eb --- /dev/null +++ b/result_server/templates/_results_table.html @@ -0,0 +1,39 @@ + + + + + {% for col_name, _ in columns %} + {% if col_name in ["Timestamp", "SYSTEM", "CODE", "FOM", "FOM version", "Exp", "Nodes", "JSON", "PA Data"] %} + + {% endif %} + {% endfor %} + + + + {% for row in rows %} + + {% for _, key in columns %} + {% if key in ["json_link", "data_link"] %} + + {% elif key == "system" %} + + {% elif key in ["timestamp", "code", "fom", "fom_version", "exp", "nodes"] %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
{{ col_name }}
+ {% if row[key] %} + + {{ "json" if key == "json_link" else "data" }} + + {% else %} + - + {% endif %} + + + {{ row[key] }} + + {{ row[key] }}
diff --git a/result_server/templates/results.html b/result_server/templates/results.html index 89a1c01..aad13c7 100644 --- a/result_server/templates/results.html +++ b/result_server/templates/results.html @@ -4,28 +4,11 @@ Uploaded Results + + + +

Confidential Results

+ +{% if not authenticated %} +
+ +
+{% endif %} + + +

+ +{% include "_results_table.html" %} + + + diff --git a/result_server/utils/otp_manager.py b/result_server/utils/otp_manager.py new file mode 100644 index 0000000..5967ac6 --- /dev/null +++ b/result_server/utils/otp_manager.py @@ -0,0 +1,74 @@ +import os +import secrets +from datetime import datetime, timedelta +import smtplib +from email.mime.text import MIMEText +import json +from typing import List, Tuple + +# OTP の有効期限(分) +OTP_EXP_MINUTES = 5 +otp_storage = {} + +# allowed_emails.json は email -> [affiliations] の辞書を想定 +with open("config/allowed_emails.json", encoding="utf-8") as f: + _ALLOWED = json.load(f) + +# Gmail の SMTP 設定(環境変数から取得) +SMTP_SERVER = os.environ.get("SMTP_SERVER", "smtp.gmail.com") +SMTP_PORT = int(os.environ.get("SMTP_PORT", 587)) +SMTP_USER = os.environ.get("SMTP_USER") +SMTP_PASS = os.environ.get("SMTP_PASS") + + +def is_allowed(email: str) -> bool: + """許可されたメールアドレスか判定""" + return email in _ALLOWED + + +def get_affiliations(email: str) -> List[str]: + """メールアドレスに紐づく所属を返す""" + if not is_allowed(email): + return [] + aff = _ALLOWED[email] + if isinstance(aff, list): + return [str(a).strip() for a in aff if str(a).strip()] + if isinstance(aff, str): + return [a.strip() for a in aff.split(",") if a.strip()] + return [] + + +def send_otp(email: str) -> Tuple[bool, str]: + """OTP を生成してメール送信""" + if not is_allowed(email): + return False, "許可されていないメールアドレスです" + + otp = str(secrets.randbelow(1000000)).zfill(6) + expire = datetime.now() + timedelta(minutes=OTP_EXP_MINUTES) + otp_storage[email] = {"value": otp, "expire": expire} + + msg = MIMEText( + f"あなたの認証コード (OTP) は {otp} です。\n" + f"有効期限は {expire.strftime('%H:%M:%S')} までです。" + ) + msg["Subject"] = "OTP 認証コード" + msg["From"] = SMTP_USER + msg["To"] = email + + try: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + server.starttls() + server.login(SMTP_USER, SMTP_PASS) + server.sendmail(SMTP_USER, [email], msg.as_string()) + except Exception as e: + return False, f"メール送信失敗: {e}" + + return True, "" + + +def verify_otp(email: str, otp: str) -> bool: + """OTP を検証""" + stored = otp_storage.get(email) + if stored and stored["value"] == otp and datetime.now() < stored["expire"]: + return True + return False diff --git a/result_server/utils/result_file.py b/result_server/utils/result_file.py new file mode 100644 index 0000000..0246db3 --- /dev/null +++ b/result_server/utils/result_file.py @@ -0,0 +1,57 @@ +import os +import json +import re +from flask import abort, jsonify, send_from_directory + +SAVE_DIR = "received" + +def load_result_file(filename: str): + filepath = os.path.join(SAVE_DIR, filename) + if not os.path.exists(filepath): + abort(404) + + if filename.endswith(".json"): + try: + with open(filepath, "r", encoding="utf-8") as f: + data = json.load(f) + return jsonify(data) + except json.JSONDecodeError: + abort(400, "Invalid JSON") + else: + return send_from_directory(SAVE_DIR, filename) + + +def get_file_confidential_tags(filename: str): + """ + JSON/TGZのconfidentialタグ取得 + """ + if filename.endswith(".json"): + return _read_confidential_from_json(filename) + + # tgzの場合、対応するUUIDを含むJSONを探す + uuid_match = re.search(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", filename, re.IGNORECASE) + if not uuid_match: + return [] + + uuid = uuid_match.group(0) + for f in os.listdir(SAVE_DIR): + if f.endswith(".json") and uuid in f: + return _read_confidential_from_json(f) + return [] + + +def _read_confidential_from_json(json_file: str): + filepath = os.path.join(SAVE_DIR, json_file) + if not os.path.exists(filepath): + return [] + try: + with open(filepath, "r", encoding="utf-8") as f: + data = json.load(f) + c = data.get("confidential") + if isinstance(c, list): + return [str(x) for x in c if x] + elif isinstance(c, str) and c.strip(): + return [c.strip()] + return [] + except Exception: + return [] diff --git a/result_server/utils/results_loader.py b/result_server/utils/results_loader.py new file mode 100644 index 0000000..fab6c19 --- /dev/null +++ b/result_server/utils/results_loader.py @@ -0,0 +1,96 @@ +import os +import json +import re +from datetime import datetime +from utils.result_file import get_file_confidential_tags +from utils.otp_manager import get_affiliations + +SAVE_DIR = "received" + +def load_results_table(public_only=True, session_email=None, authenticated=False): + files = os.listdir(SAVE_DIR) + json_files = sorted([f for f in files if f.endswith(".json")], reverse=True) + tgz_files = [f for f in files if f.endswith(".tgz")] + + rows = [] + for json_file in json_files: + tags = get_file_confidential_tags(json_file) + + if public_only and tags: + continue + + if tags and not authenticated: + # 認証なしならスキップ + continue + if tags and session_email: + affs = get_affiliations(session_email) + if not (set(tags) & set(affs)): + continue + + # JSON読み込み + try: + with open(os.path.join(SAVE_DIR, json_file), "r", encoding="utf-8") as f: + data = json.load(f) + except Exception: + data = {} + + code = data.get("code", "N/A") + sys = data.get("system", "N/A") + fom = data.get("FOM", "N/A") + fom_version = data.get("FOM_version", "N/A") + exp = data.get("Exp", "N/A") + cpu = data.get("cpu_name", "N/A") + gpu = data.get("gpu_name", "N/A") + nodes = data.get("node_count", "N/A") + cpus = data.get("cpus_per_node", "N/A") + gpus = data.get("gpus_per_node", "N/A") + cpu_cores = data.get("cpu_cores", "N/A") + + match = re.search(r"\d{8}_\d{6}", json_file) + timestamp = "Unknown" + if match: + try: + ts = datetime.strptime(match.group(), "%Y%m%d_%H%M%S") + timestamp = ts.strftime("%Y-%m-%d %H:%M:%S") + except: + pass + + uuid_match = re.search(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", json_file, re.IGNORECASE) + uid = uuid_match.group(0) if uuid_match else None + tgz_file = next((f for f in tgz_files if uid in f), None) if uid else None + + row = { + "timestamp": timestamp, + "code": code, + "exp": exp, + "fom": fom, + "fom_version": fom_version, + "system": sys, + "cpu": cpu, + "gpu": gpu, + "nodes": nodes, + "cpus": cpus, + "gpus": gpus, + "cpu_cores": cpu_cores, + "json_link": json_file, + "data_link": tgz_file, + } + rows.append(row) + + columns = [ + ("Timestamp", "timestamp"), + ("CODE", "code"), + ("Exp", "exp"), + ("FOM", "fom"), + ("FOM version", "fom_version"), + ("SYSTEM", "system"), + ("CPU Name", "cpu"), + ("GPU Name", "gpu"), + ("Nodes", "nodes"), + ("CPU/node", "cpus"), + ("GPU/node", "gpus"), + ("CPU Core Count", "cpu_cores"), + ("JSON", "json_link"), + ("PA Data", "data_link"), + ] + return rows, columns diff --git a/scripts/result.sh b/scripts/result.sh index 9b1c952..a84a006 100644 --- a/scripts/result.sh +++ b/scripts/result.sh @@ -99,12 +99,21 @@ while IFS= read -r line; do fi # Description - if echo "$line" | grep -q 'Description:'; then - discription=$(echo "$line" | grep -Eo 'Description:[ ]*[a-zA-Z0-9_.-]*' | head -n1 | awk -F':' '{print $2}' | sed 's/^ *//') + if echo "$line" | grep -q 'description:'; then + discription=$(echo "$line" | grep -Eo 'description:[ ]*[a-zA-Z0-9_.-]*' | head -n1 | awk -F':' '{print $2}' | sed 's/^ *//') else discription=null fi + + # Confidential + if echo "$line" | grep -q 'confidential:'; then + confidential=$(echo "$line" | grep -Eo 'confidential:[ ]*[a-zA-Z0-9_.-]*' | head -n1 | awk -F':' '{print $2}' | sed 's/^ *//') + else + confidential=null + fi + + # --- JSON --- cat < results/result${i}.json { @@ -121,7 +130,8 @@ while IFS= read -r line; do "node_count": "$node_count", "uname": "$uname_info", "cpu_cores": "$cpu_cores", - "discription": "$discription" + "discription": "$discription", + "confidential": "$confidential" } EOF