feat: 新增本地認證模式支援開發測試環境
- 新增 LOCAL_AUTH_ENABLED/USERNAME/PASSWORD 環境變數設定 - 當本地認證啟用時,使用環境變數中的帳密驗證 - 本地認證用戶自動取得管理員權限 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
# ============================================================
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user