feat(security): automate governance review and access controls

This commit is contained in:
ogt
2026-07-10 18:49:32 +08:00
parent cd12e48505
commit 6b8bd8bb05
26 changed files with 1977 additions and 99 deletions

62
auth.py
View File

@@ -1,6 +1,7 @@
import hmac
import os
import time
from flask import render_template, redirect, url_for, request, session, flash
from flask import jsonify, render_template, redirect, url_for, request, session, flash
from functools import wraps
from werkzeug.security import check_password_hash
from config import LOGIN_PASSWORD
@@ -9,11 +10,17 @@ from datetime import datetime, timedelta
# ==========================================
# 🔓 登入功能開關
# ==========================================
# 設定環境變數 DISABLE_LOGIN=true 可關閉登入驗證
DISABLE_LOGIN = os.getenv('DISABLE_LOGIN', 'false').lower() == 'true'
# 登入繞過只允許測試環境明確開啟,避免 production 因單一 env 誤設而全站失守。
_DISABLE_LOGIN_REQUESTED = os.getenv('DISABLE_LOGIN', 'false').lower() == 'true'
_ALLOW_INSECURE_LOGIN_BYPASS = (
os.getenv('MOMO_ALLOW_INSECURE_CONFIG_FOR_TESTS', 'false').lower() == 'true'
)
DISABLE_LOGIN = _DISABLE_LOGIN_REQUESTED and _ALLOW_INSECURE_LOGIN_BYPASS
if DISABLE_LOGIN:
print("⚠️ 警告:登入驗證已關閉 (DISABLE_LOGIN=true)")
elif _DISABLE_LOGIN_REQUESTED:
print("[Security] 已拒絕 DISABLE_LOGIN僅測試環境可使用登入繞過")
# ==========================================
# 🔒 登入失敗追蹤與帳號鎖定機制
@@ -164,6 +171,41 @@ def login_required(f):
return decorated_view
def require_authenticated_request():
"""供中央 HTTP policy 使用API 回 401頁面導向登入。"""
if DISABLE_LOGIN or session.get('logged_in'):
return None
is_api_request = request.path.startswith('/api/') or '/api/' in request.path
if is_api_request:
return jsonify({
'success': False,
'error': 'authentication_required',
'message': '此端點需要登入。',
}), 401
return redirect(url_for('login'))
def internal_key_required(f):
"""Require the shared internal transport key for non-session service calls."""
@wraps(f)
def decorated_view(*args, **kwargs):
expected = os.getenv('INTERNAL_API_KEY', '').strip()
provided = request.headers.get('X-Internal-Key', '').strip()
if not expected:
return jsonify({
'success': False,
'error': 'internal_auth_not_configured',
}), 503
if not provided or not hmac.compare_digest(provided, expected):
return jsonify({
'success': False,
'error': 'invalid_internal_key',
}), 401
return f(*args, **kwargs)
return decorated_view
def get_current_user():
"""
取得目前登入的使用者資訊
@@ -176,10 +218,12 @@ def get_current_user():
return {
'logged_in': True,
'user_id': session.get('user_id'),
'login_time': session.get('login_time'),
'client_ip': session.get('client_ip'),
'role': session.get('role', 'admin'), # 預設為 admin向後兼容
'username': session.get('username', 'admin')
'username': session.get('username', 'admin'),
'auth_source': session.get('auth_source', 'legacy_shared_password'),
}
@@ -297,17 +341,21 @@ def init_auth_routes(app):
is_password_valid = check_password_hash(LOGIN_PASSWORD, input_password)
else:
# 向後兼容:明文比對(僅用於過渡期)
is_password_valid = (input_password == LOGIN_PASSWORD)
is_password_valid = hmac.compare_digest(input_password, LOGIN_PASSWORD)
if is_password_valid:
print("⚠️ 警告:系統仍在使用明文密碼,請盡快執行密碼雜湊更新")
# 4. 驗證結果處理
if is_password_valid:
# 登入成功
session.clear()
session.permanent = True
session['logged_in'] = True
session['login_time'] = datetime.now().isoformat()
session['client_ip'] = client_ip
session['role'] = 'admin'
session['username'] = 'legacy-admin'
session['auth_source'] = 'legacy_shared_password'
# 清除失敗記錄
clear_login_attempts(client_ip)
@@ -345,9 +393,7 @@ def init_auth_routes(app):
登出路由:清除 session 並導回登入頁。
"""
client_ip = get_client_ip()
session.pop('logged_in', None)
session.pop('login_time', None)
session.pop('client_ip', None)
session.clear()
print(f"👋 使用者已登出 | IP: {client_ip}")
return redirect(url_for('login'))