feat(security): automate governance review and access controls
This commit is contained in:
62
auth.py
62
auth.py
@@ -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'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user