1st_fix_login_issue
This commit is contained in:
232
app/utils/ldap_auth.py
Normal file
232
app/utils/ldap_auth.py
Normal file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
LDAP 認證服務
|
||||
|
||||
Author: PANJIT IT Team
|
||||
Created: 2024-01-28
|
||||
Modified: 2024-01-28
|
||||
"""
|
||||
|
||||
import time
|
||||
from ldap3 import Server, Connection, SUBTREE, ALL_ATTRIBUTES
|
||||
from flask import current_app
|
||||
from .logger import get_logger
|
||||
from .exceptions import AuthenticationError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LDAPAuthService:
|
||||
"""LDAP 認證服務"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = current_app.config
|
||||
self.server_url = self.config.get('LDAP_SERVER')
|
||||
self.port = self.config.get('LDAP_PORT', 389)
|
||||
self.use_ssl = self.config.get('LDAP_USE_SSL', False)
|
||||
self.bind_user_dn = self.config.get('LDAP_BIND_USER_DN')
|
||||
self.bind_password = self.config.get('LDAP_BIND_USER_PASSWORD')
|
||||
self.search_base = self.config.get('LDAP_SEARCH_BASE')
|
||||
self.login_attr = self.config.get('LDAP_USER_LOGIN_ATTR', 'userPrincipalName')
|
||||
|
||||
def create_connection(self, retries=3):
|
||||
"""建立 LDAP 連線(帶重試機制)"""
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
server = Server(
|
||||
self.server_url,
|
||||
port=self.port,
|
||||
use_ssl=self.use_ssl,
|
||||
get_info=ALL_ATTRIBUTES
|
||||
)
|
||||
|
||||
conn = Connection(
|
||||
server,
|
||||
user=self.bind_user_dn,
|
||||
password=self.bind_password,
|
||||
auto_bind=True,
|
||||
raise_exceptions=True
|
||||
)
|
||||
|
||||
logger.info("LDAP connection established successfully")
|
||||
return conn
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LDAP connection attempt {attempt + 1} failed: {str(e)}")
|
||||
if attempt == retries - 1:
|
||||
raise AuthenticationError(f"LDAP connection failed: {str(e)}")
|
||||
time.sleep(1)
|
||||
|
||||
return None
|
||||
|
||||
def authenticate_user(self, username, password):
|
||||
"""驗證使用者憑證"""
|
||||
try:
|
||||
conn = self.create_connection()
|
||||
if not conn:
|
||||
raise AuthenticationError("Unable to connect to LDAP server")
|
||||
|
||||
# 搜尋使用者
|
||||
search_filter = f"(&(objectClass=person)(objectCategory=person)({self.login_attr}={username}))"
|
||||
|
||||
conn.search(
|
||||
self.search_base,
|
||||
search_filter,
|
||||
SUBTREE,
|
||||
attributes=['displayName', 'mail', 'sAMAccountName', 'userPrincipalName', 'department']
|
||||
)
|
||||
|
||||
if not conn.entries:
|
||||
logger.warning(f"User not found: {username}")
|
||||
raise AuthenticationError("帳號不存在")
|
||||
|
||||
user_entry = conn.entries[0]
|
||||
user_dn = user_entry.entry_dn
|
||||
|
||||
# 驗證使用者密碼
|
||||
try:
|
||||
user_conn = Connection(
|
||||
conn.server,
|
||||
user=user_dn,
|
||||
password=password,
|
||||
auto_bind=True,
|
||||
raise_exceptions=True
|
||||
)
|
||||
user_conn.unbind()
|
||||
|
||||
# 返回使用者資訊
|
||||
user_info = {
|
||||
'username': str(user_entry.sAMAccountName) if user_entry.sAMAccountName else username,
|
||||
'display_name': str(user_entry.displayName) if user_entry.displayName else username,
|
||||
'email': str(user_entry.mail) if user_entry.mail else f"{username}@panjit.com.tw",
|
||||
'department': str(user_entry.department) if hasattr(user_entry, 'department') and user_entry.department else None,
|
||||
'user_principal_name': str(user_entry.userPrincipalName) if user_entry.userPrincipalName else username
|
||||
}
|
||||
|
||||
logger.info(f"User authenticated successfully: {username}")
|
||||
return user_info
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Authentication failed for user {username}: {str(e)}")
|
||||
raise AuthenticationError("密碼錯誤")
|
||||
|
||||
except AuthenticationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"LDAP authentication error: {str(e)}")
|
||||
raise AuthenticationError(f"認證服務錯誤: {str(e)}")
|
||||
|
||||
finally:
|
||||
if 'conn' in locals() and conn:
|
||||
conn.unbind()
|
||||
|
||||
def search_users(self, search_term, limit=20):
|
||||
"""搜尋使用者"""
|
||||
try:
|
||||
conn = self.create_connection()
|
||||
if not conn:
|
||||
return []
|
||||
|
||||
# 建構搜尋過濾器
|
||||
search_filter = f"""(&
|
||||
(objectClass=person)
|
||||
(objectCategory=person)
|
||||
(!(userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
(|
|
||||
(displayName=*{search_term}*)
|
||||
(mail=*{search_term}*)
|
||||
(sAMAccountName=*{search_term}*)
|
||||
(userPrincipalName=*{search_term}*)
|
||||
)
|
||||
)"""
|
||||
|
||||
# 移除多餘空白
|
||||
search_filter = ' '.join(search_filter.split())
|
||||
|
||||
conn.search(
|
||||
self.search_base,
|
||||
search_filter,
|
||||
SUBTREE,
|
||||
attributes=['sAMAccountName', 'displayName', 'mail', 'department'],
|
||||
size_limit=limit
|
||||
)
|
||||
|
||||
results = []
|
||||
for entry in conn.entries:
|
||||
results.append({
|
||||
'username': str(entry.sAMAccountName) if entry.sAMAccountName else '',
|
||||
'display_name': str(entry.displayName) if entry.displayName else '',
|
||||
'email': str(entry.mail) if entry.mail else '',
|
||||
'department': str(entry.department) if hasattr(entry, 'department') and entry.department else ''
|
||||
})
|
||||
|
||||
logger.info(f"LDAP search found {len(results)} results for term: {search_term}")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LDAP search error: {str(e)}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals() and conn:
|
||||
conn.unbind()
|
||||
|
||||
def get_user_info(self, username):
|
||||
"""取得使用者詳細資訊"""
|
||||
try:
|
||||
conn = self.create_connection()
|
||||
if not conn:
|
||||
return None
|
||||
|
||||
# 支援 sAMAccountName 和 userPrincipalName 格式
|
||||
if '@' in username:
|
||||
search_filter = f"""(&
|
||||
(objectClass=person)
|
||||
(|
|
||||
(userPrincipalName={username})
|
||||
(mail={username})
|
||||
)
|
||||
)"""
|
||||
else:
|
||||
search_filter = f"(&(objectClass=person)(sAMAccountName={username}))"
|
||||
|
||||
# 移除多餘空白
|
||||
search_filter = ' '.join(search_filter.split())
|
||||
|
||||
conn.search(
|
||||
self.search_base,
|
||||
search_filter,
|
||||
SUBTREE,
|
||||
attributes=['displayName', 'mail', 'sAMAccountName', 'userPrincipalName', 'department']
|
||||
)
|
||||
|
||||
if not conn.entries:
|
||||
return None
|
||||
|
||||
entry = conn.entries[0]
|
||||
return {
|
||||
'username': str(entry.sAMAccountName) if entry.sAMAccountName else username,
|
||||
'display_name': str(entry.displayName) if entry.displayName else username,
|
||||
'email': str(entry.mail) if entry.mail else f"{username}@panjit.com.tw",
|
||||
'department': str(entry.department) if hasattr(entry, 'department') and entry.department else None,
|
||||
'user_principal_name': str(entry.userPrincipalName) if entry.userPrincipalName else ''
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user info for {username}: {str(e)}")
|
||||
return None
|
||||
finally:
|
||||
if 'conn' in locals() and conn:
|
||||
conn.unbind()
|
||||
|
||||
def test_connection(self):
|
||||
"""測試 LDAP 連線(健康檢查用)"""
|
||||
try:
|
||||
conn = self.create_connection(retries=1)
|
||||
if conn:
|
||||
conn.unbind()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"LDAP connection test failed: {str(e)}")
|
||||
return False
|
Reference in New Issue
Block a user