feat: 新增本地認證模式支援開發測試環境

- 新增 LOCAL_AUTH_ENABLED/USERNAME/PASSWORD 環境變數設定
- 當本地認證啟用時,使用環境變數中的帳密驗證
- 本地認證用戶自動取得管理員權限

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-02-02 18:10:46 +08:00
parent 65f43fbe0c
commit ef83060109
2 changed files with 64 additions and 3 deletions

View File

@@ -46,6 +46,13 @@ LDAP_API_URL=https://adapi.panjit.com.tw
# Admin email addresses (comma-separated for multiple)
ADMIN_EMAILS=ymirliu@panjit.com.tw
# Local Authentication (for development/testing)
# When enabled, uses local credentials instead of LDAP
# Set LOCAL_AUTH_ENABLED=true to bypass LDAP authentication
LOCAL_AUTH_ENABLED=false
LOCAL_AUTH_USERNAME=
LOCAL_AUTH_PASSWORD=
# ============================================================
# Gunicorn Configuration
# ============================================================

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Authentication service using LDAP API."""
"""Authentication service using LDAP API or local credentials."""
from __future__ import annotations
@@ -19,9 +19,47 @@ ADMIN_EMAILS = os.environ.get(
# Timeout for LDAP API requests
LDAP_TIMEOUT = 10
# Local authentication configuration (for development/testing)
LOCAL_AUTH_ENABLED = os.environ.get("LOCAL_AUTH_ENABLED", "false").lower() in ("true", "1", "yes")
LOCAL_AUTH_USERNAME = os.environ.get("LOCAL_AUTH_USERNAME", "")
LOCAL_AUTH_PASSWORD = os.environ.get("LOCAL_AUTH_PASSWORD", "")
def _authenticate_local(username: str, password: str) -> dict | None:
"""Authenticate using local environment credentials.
Args:
username: User provided username
password: User provided password
Returns:
User info dict on success, None on failure
"""
if not LOCAL_AUTH_ENABLED:
return None
if not LOCAL_AUTH_USERNAME or not LOCAL_AUTH_PASSWORD:
logger.warning("Local auth enabled but credentials not configured")
return None
if username == LOCAL_AUTH_USERNAME and password == LOCAL_AUTH_PASSWORD:
logger.info("Local auth success for user: %s", username)
return {
"username": username,
"displayName": f"Local User ({username})",
"mail": f"{username}@local.dev",
"department": "Development",
}
logger.warning("Local auth failed for user: %s", username)
return None
def authenticate(username: str, password: str, domain: str = "PANJIT") -> dict | None:
"""Authenticate user via LDAP API.
"""Authenticate user via local credentials or LDAP API.
If LOCAL_AUTH_ENABLED is set, tries local authentication first.
Falls back to LDAP API if local auth is disabled or fails.
Args:
username: Employee ID or email
@@ -32,6 +70,16 @@ def authenticate(username: str, password: str, domain: str = "PANJIT") -> dict |
User info dict on success: {username, displayName, mail, department}
None on failure
"""
# Try local authentication first if enabled
if LOCAL_AUTH_ENABLED:
local_result = _authenticate_local(username, password)
if local_result:
return local_result
# If local auth is enabled but failed, don't fall back to LDAP
# This ensures local-only mode when LOCAL_AUTH_ENABLED is true
return None
# LDAP authentication
try:
response = requests.post(
f"{LDAP_API_BASE}/api/v1/ldap/auth",
@@ -66,7 +114,13 @@ def is_admin(user: dict) -> bool:
user: User info dict with 'mail' field
Returns:
True if user email is in ADMIN_EMAILS list
True if user email is in ADMIN_EMAILS list, or if local auth is enabled
"""
# Local auth users are automatically admins (for development/testing)
if LOCAL_AUTH_ENABLED:
user_mail = user.get("mail", "")
if user_mail.endswith("@local.dev"):
return True
user_mail = user.get("mail", "").lower().strip()
return user_mail in [e.strip() for e in ADMIN_EMAILS]