Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions result_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# Import Flask and route blueprints
from flask import Flask, render_template, current_app
from flask_session import Session
from routes.receive import receive_bp
from routes.results import results_bp
from routes.upload_tgz import upload_bp
Expand Down Expand Up @@ -43,11 +44,38 @@ def create_app(prefix="", base_dir=None):

# --- セッションCookieのセキュリティ設定 ---
app.config.update(
SESSION_TYPE='filesystem',
SESSION_FILE_DIR=os.path.join(base_dir, "flask_session"),
SESSION_PERMANENT=True,
SESSION_USE_SIGNER=True,
SESSION_COOKIE_SECURE=True, # HTTPS必須
SESSION_COOKIE_HTTPONLY=True, # JSからのアクセス禁止
SESSION_COOKIE_SAMESITE="Strict", # もしくは "Lax"
PERMANENT_SESSION_LIFETIME=timedelta(minutes=30), # セッション寿命を短めに
)
Session(app)


# Redis 接続
import redis
redis_url = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
r_conn = redis.from_url(redis_url, decode_responses=True)

# 環境ごとの prefix
if prefix == "/dev":
app.config["SESSION_COOKIE_NAME"] = "session_dev"
key_prefix = "dev:"
else:
app.config["SESSION_COOKIE_NAME"] = "session_main"
key_prefix = "main:"

# OTP Manager 初期化
import utils.otp_redis_manager as otp_redis_manager
otp_redis_manager.init_redis(r_conn, key_prefix)

# 他でもredisを使う場合、
#app.redis = r_conn
#app.redis_prefix = key_prefix



Expand Down
20 changes: 10 additions & 10 deletions result_server/routes/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
redirect, url_for, flash, abort, send_from_directory
)
from utils.results_loader import load_results_table, load_estimated_results_table
from utils.otp_manager import send_otp, verify_otp, get_affiliations
#from utils.otp_manager import get_affiliations
from utils.otp_redis_manager import send_otp, verify_otp, invalidate_otp, is_allowed, get_affiliations
from utils.result_file import load_result_file, get_file_confidential_tags

results_bp = Blueprint("results", __name__)
Expand Down Expand Up @@ -51,30 +52,29 @@ def handle_otp_post(session_key_authenticated, session_key_email, route_name):

if email and not otp:
success, msg = send_otp(email)
if success:
flash("OTPをメールに送信しました")
flash(msg)
if success and is_allowed(email):
session.clear()
session[session_key_email] = email
session["otp_stage"] = "otp"
else:
flash(msg)
session.pop(session_key_email, None)
session.clear()
session["otp_stage"] = "email"
return redirect(url_for(route_name))

elif otp:
otp_email = session.get(session_key_email)
if otp_email and verify_otp(otp_email, otp):
session.clear()
session[session_key_authenticated] = True
flash("認証成功")
session.pop("otp_stage", None)
else:
flash("OTP認証失敗")
session.pop(session_key_email, None)
session.pop(session_key_authenticated, None)
session["otp_stage"] = "email"
session.clear()
flash("認証失敗")
return redirect(url_for(route_name))



def render_confidential_table(template_name, public_only, session_key_authenticated, session_key_email, loader_func=None):
authenticated = session.get(session_key_authenticated, False)
otp_email = session.get(session_key_email)
Expand Down
124 changes: 124 additions & 0 deletions result_server/utils/otp_redis_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import redis
import random
import string
import time
from typing import List, Tuple
from datetime import datetime, timedelta
from email.mime.text import MIMEText
import smtplib
import json
import os

# -------------------------------
# Redis 接続 (初期化は後で)
# -------------------------------
r = None
prefix = "" # app から渡す

def init_redis(redis_conn, key_prefix: str = ""):
global r, prefix
r = redis_conn
prefix = key_prefix

# -------------------------------
# メール許可・所属
# -------------------------------
with open("config/allowed_emails.json", encoding="utf-8") as f:
_ALLOWED = json.load(f)

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 []

# -------------------------------
# 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")

# -------------------------------
# OTP 設定
# -------------------------------
OTP_TTL_SECONDS = 5 * 60
MAX_OTP_ATTEMPTS = 5

def generate_otp_code(length=6) -> str:
return ''.join(random.choices(string.digits, k=length))

def send_otp(email: str) -> Tuple[bool, str]:
"""OTP を生成して Redis に保存してメール送信"""
public_message = "メールを確認してください。"
if not is_allowed(email):
return True, public_message

code = generate_otp_code()
now = int(time.time())
key = f"{prefix}:otp:{email}"

r.hmset(key, {
"code": code,
"expires_at": now + OTP_TTL_SECONDS,
"attempts_left": MAX_OTP_ATTEMPTS,
})
r.expire(key, OTP_TTL_SECONDS)

# メール送信
msg = MIMEText(
f"あなたの認証コード (OTP) は {code} です。\n"
f"有効期限は {datetime.fromtimestamp(now + OTP_TTL_SECONDS).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, public_message

def verify_otp(email: str, code: str) -> bool:
key = f"{prefix}:otp:{email}"
data = r.hgetall(key)
now = int(time.time())
if not data:
return False

if now > int(data.get("expires_at", 0)):
r.delete(key)
return False

attempts_left = int(data.get("attempts_left", MAX_OTP_ATTEMPTS))
if attempts_left <= 0:
r.delete(key)
return False

if code == data.get("code"):
r.delete(key)
return True
else:
attempts_left -= 1
if attempts_left <= 0:
r.delete(key)
else:
r.hset(key, "attempts_left", attempts_left)
return False

def invalidate_otp(email: str):
r.delete(f"{prefix}:otp:{email}")

19 changes: 18 additions & 1 deletion scripts/matrix_generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ OUTPUT_FILE=".gitlab-ci.generated.yml"

source ./scripts/job_functions.sh

CODE_FILTER=""
SYSTEM_FILTER=""

while [[ $# -gt 0 ]]; do
case $1 in
code=*) CODE_FILTER="${1#code=}" ;;
system=*) SYSTEM_FILTER="${1#system=}" ;;
*) echo "Unknown argument: $1"; exit 1 ;;
esac
shift
done


echo "# Auto-generated GitLab CI configuration" > "$OUTPUT_FILE"
echo "
stages:
Expand All @@ -20,11 +33,15 @@ stages:
for listfile in programs/*/list.csv; do
program_dir=$(dirname "$listfile")
program=$(basename "$program_dir")


[[ -n "$CODE_FILTER" && "$program" != "$CODE_FILTER" ]] && continue

while IFS=, read -r system mode queue_group nodes numproc_node nthreads elapse; do
[[ "$system" == "system" ]] && continue # skip header
[[ "$system" == *"#"* ]] && continue # skip #

[[ -n "$SYSTEM_FILTER" && "$system" != "$SYSTEM_FILTER" ]] && continue

job_prefix="${program}_${system}_N${nodes}_P${numproc_node}_T${nthreads}"
program_path="$program_dir"

Expand Down