Files
claude-relay-service/src/services/ldapService.js
2025-09-03 13:30:13 +08:00

892 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const ldap = require('ldapjs')
const logger = require('../utils/logger')
const config = require('../../config/config')
const userService = require('./userService')
class LdapService {
constructor() {
this.config = config.ldap || {}
this.client = null
// 设置服务器类型,默认为 OpenLDAP
this.serverType = this.config.serverType || 'openldap'
this.isActiveDirectory = this.serverType === 'activedirectory'
// 验证配置 - 只有在 LDAP 配置存在且启用时才验证
if (this.config && this.config.enabled) {
this.validateConfiguration()
}
}
// 🔍 验证LDAP配置
validateConfiguration() {
const errors = []
if (!this.config.server) {
errors.push('LDAP server configuration is missing')
} else {
if (!this.config.server.url || typeof this.config.server.url !== 'string') {
errors.push('LDAP server URL is not configured or invalid')
}
if (!this.config.server.bindDN || typeof this.config.server.bindDN !== 'string') {
errors.push('LDAP bind DN is not configured or invalid')
}
if (
!this.config.server.bindCredentials ||
typeof this.config.server.bindCredentials !== 'string'
) {
errors.push('LDAP bind credentials are not configured or invalid')
}
if (!this.config.server.searchBase || typeof this.config.server.searchBase !== 'string') {
errors.push('LDAP search base is not configured or invalid')
}
if (!this.config.server.searchFilter || typeof this.config.server.searchFilter !== 'string') {
errors.push('LDAP search filter is not configured or invalid')
}
}
if (errors.length > 0) {
logger.error('❌ LDAP configuration validation failed:', errors)
// Don't throw error during initialization, just log warnings
logger.warn('⚠️ LDAP authentication may not work properly due to configuration errors')
} else {
logger.info('✅ LDAP configuration validation passed')
}
}
// 🔍 解析Windows AD用户名格式
parseActiveDirectoryUsername(username) {
if (!this.isActiveDirectory) {
return { username, domain: null, format: 'simple' }
}
const trimmedUsername = username.trim()
// 检查UPN格式 (user@domain.com)
if (trimmedUsername.includes('@')) {
const parts = trimmedUsername.split('@')
if (parts.length === 2 && parts[0] && parts[1]) {
return {
username: parts[0],
domain: parts[1],
format: 'upn',
fullUsername: trimmedUsername
}
}
}
// 检查域\用户名格式 (DOMAIN\user)
if (trimmedUsername.includes('\\')) {
const parts = trimmedUsername.split('\\')
if (parts.length === 2 && parts[0] && parts[1]) {
return {
username: parts[1],
domain: parts[0],
format: 'domain',
fullUsername: trimmedUsername
}
}
}
// 简单用户名格式
return {
username: trimmedUsername,
domain: null,
format: 'simple',
fullUsername: trimmedUsername
}
}
// 🔍 获取服务器类型特定的搜索过滤器
getServerSpecificSearchFilter(usernameInfo) {
if (this.isActiveDirectory) {
const { username, fullUsername } = usernameInfo
// Windows AD: 支持 sAMAccountName 和 userPrincipalName
if (fullUsername && fullUsername.includes('@')) {
// 如果是UPN格式优先使用userPrincipalName搜索
return `(|(userPrincipalName=${fullUsername})(sAMAccountName=${username}))`
} else {
// 否则同时搜索两个属性
return `(|(sAMAccountName=${username})(userPrincipalName=${username}))`
}
} else {
// OpenLDAP: 使用配置的搜索过滤器或默认的uid
const filterTemplate = this.config.server.searchFilter || '(uid={{username}})'
return filterTemplate.replace('{{username}}', usernameInfo.username)
}
}
// 🔍 获取服务器类型特定的搜索属性
getServerSpecificSearchAttributes() {
if (this.isActiveDirectory) {
// Windows AD 特定属性
return (
this.config.server.searchAttributes || [
'dn',
'sAMAccountName',
'userPrincipalName',
'cn',
'displayName',
'mail',
'givenName',
'sn',
'memberOf',
'objectClass',
'userAccountControl'
]
)
} else {
// OpenLDAP 默认属性
return this.config.server.searchAttributes || ['dn', 'uid', 'cn', 'mail', 'givenName', 'sn']
}
}
// 🔍 提取LDAP条目的DN
extractDN(ldapEntry) {
if (!ldapEntry) {
return null
}
// Try different ways to get the DN
let dn = null
// Method 1: Direct dn property
if (ldapEntry.dn) {
;({ dn } = ldapEntry)
}
// Method 2: objectName property (common in some LDAP implementations)
else if (ldapEntry.objectName) {
dn = ldapEntry.objectName
}
// Method 3: distinguishedName property
else if (ldapEntry.distinguishedName) {
dn = ldapEntry.distinguishedName
}
// Method 4: Check if the entry itself is a DN string
else if (typeof ldapEntry === 'string' && ldapEntry.includes('=')) {
dn = ldapEntry
}
// Convert DN to string if it's an object
if (dn && typeof dn === 'object') {
if (dn.toString && typeof dn.toString === 'function') {
dn = dn.toString()
} else if (dn.dn && typeof dn.dn === 'string') {
;({ dn } = dn)
}
}
// Validate the DN format
if (typeof dn === 'string' && dn.trim() !== '' && dn.includes('=')) {
return dn.trim()
}
return null
}
// 🔗 创建LDAP客户端连接
createClient() {
try {
const clientOptions = {
url: this.config.server.url,
timeout: this.config.server.timeout,
connectTimeout: this.config.server.connectTimeout,
reconnect: true
}
// 如果使用 LDAPS (SSL/TLS),添加 TLS 选项
if (this.config.server.url.toLowerCase().startsWith('ldaps://')) {
const tlsOptions = {}
// 证书验证设置
if (this.config.server.tls) {
if (typeof this.config.server.tls.rejectUnauthorized === 'boolean') {
tlsOptions.rejectUnauthorized = this.config.server.tls.rejectUnauthorized
}
// CA 证书
if (this.config.server.tls.ca) {
tlsOptions.ca = this.config.server.tls.ca
}
// 客户端证书和私钥 (双向认证)
if (this.config.server.tls.cert) {
tlsOptions.cert = this.config.server.tls.cert
}
if (this.config.server.tls.key) {
tlsOptions.key = this.config.server.tls.key
}
// 服务器名称 (SNI)
if (this.config.server.tls.servername) {
tlsOptions.servername = this.config.server.tls.servername
}
}
clientOptions.tlsOptions = tlsOptions
logger.debug('🔒 Creating LDAPS client with TLS options:', {
url: this.config.server.url,
rejectUnauthorized: tlsOptions.rejectUnauthorized,
hasCA: !!tlsOptions.ca,
hasCert: !!tlsOptions.cert,
hasKey: !!tlsOptions.key,
servername: tlsOptions.servername
})
}
const client = ldap.createClient(clientOptions)
// 设置错误处理
client.on('error', (err) => {
if (err.code === 'CERT_HAS_EXPIRED' || err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
logger.error('🔒 LDAP TLS certificate error:', {
code: err.code,
message: err.message,
hint: 'Consider setting LDAP_TLS_REJECT_UNAUTHORIZED=false for self-signed certificates'
})
} else {
logger.error('🔌 LDAP client error:', err)
}
})
client.on('connect', () => {
if (this.config.server.url.toLowerCase().startsWith('ldaps://')) {
logger.info('🔒 LDAPS client connected successfully')
} else {
logger.info('🔗 LDAP client connected successfully')
}
})
client.on('connectTimeout', () => {
logger.warn('⏱️ LDAP connection timeout')
})
return client
} catch (error) {
logger.error('❌ Failed to create LDAP client:', error)
throw error
}
}
// 🔒 绑定LDAP连接管理员认证
async bindClient(client) {
return new Promise((resolve, reject) => {
// 验证绑定凭据
const { bindDN } = this.config.server
const { bindCredentials } = this.config.server
if (!bindDN || typeof bindDN !== 'string') {
const error = new Error('LDAP bind DN is not configured or invalid')
logger.error('❌ LDAP configuration error:', error.message)
reject(error)
return
}
if (!bindCredentials || typeof bindCredentials !== 'string') {
const error = new Error('LDAP bind credentials are not configured or invalid')
logger.error('❌ LDAP configuration error:', error.message)
reject(error)
return
}
client.bind(bindDN, bindCredentials, (err) => {
if (err) {
logger.error('❌ LDAP bind failed:', err)
reject(err)
} else {
logger.debug('🔑 LDAP bind successful')
resolve()
}
})
})
}
// 🔍 搜索用户
async searchUser(client, username) {
return new Promise((resolve, reject) => {
// 解析用户名对Windows AD进行特殊处理
const usernameInfo = this.parseActiveDirectoryUsername(username)
// 防止LDAP注入转义特殊字符
// 根据RFC 4515需要转义的特殊字符* ( ) \ NUL
const escapedUsername = usernameInfo.username
.replace(/\\/g, '\\5c') // 反斜杠必须先转义
.replace(/\*/g, '\\2a') // 星号
.replace(/\(/g, '\\28') // 左括号
.replace(/\)/g, '\\29') // 右括号
.replace(/\0/g, '\\00') // NUL字符
.replace(/\//g, '\\2f') // 斜杠
// 如果是UPN格式也需要转义完整用户名
let escapedFullUsername = usernameInfo.fullUsername
if (escapedFullUsername && escapedFullUsername !== usernameInfo.username) {
escapedFullUsername = escapedFullUsername
.replace(/\\/g, '\\5c')
.replace(/\*/g, '\\2a')
.replace(/\(/g, '\\28')
.replace(/\)/g, '\\29')
.replace(/\0/g, '\\00')
.replace(/\//g, '\\2f')
}
// 构建转义后的用户名信息
const escapedUsernameInfo = {
...usernameInfo,
username: escapedUsername,
fullUsername: escapedFullUsername
}
// 获取服务器特定的搜索过滤器和属性
const searchFilter = this.getServerSpecificSearchFilter(escapedUsernameInfo)
const searchAttributes = this.getServerSpecificSearchAttributes()
const searchOptions = {
scope: 'sub',
filter: searchFilter,
attributes: searchAttributes
}
logger.debug(
`🔍 Searching for user: ${username} (${usernameInfo.format} format) with filter: ${searchFilter}`
)
if (this.isActiveDirectory && usernameInfo.domain) {
logger.debug(`🏢 Domain detected: ${usernameInfo.domain}`)
}
const entries = []
client.search(this.config.server.searchBase, searchOptions, (err, res) => {
if (err) {
logger.error('❌ LDAP search error:', err)
reject(err)
return
}
res.on('searchEntry', (entry) => {
logger.debug('🔍 LDAP search entry received:', {
dn: entry.dn,
objectName: entry.objectName,
type: typeof entry.dn,
entryType: typeof entry,
hasAttributes: !!entry.attributes,
attributeCount: entry.attributes ? entry.attributes.length : 0,
serverType: this.serverType
})
entries.push(entry)
})
res.on('searchReference', (referral) => {
logger.debug('🔗 LDAP search referral:', referral.uris)
})
res.on('error', (error) => {
logger.error('❌ LDAP search result error:', error)
reject(error)
})
res.on('end', (result) => {
logger.debug(
`✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries (${this.serverType})`
)
if (entries.length === 0) {
resolve(null)
} else {
// Log the structure of the first entry for debugging
if (entries[0]) {
logger.debug('🔍 Full LDAP entry structure:', {
entryType: typeof entries[0],
entryConstructor: entries[0].constructor?.name,
entryKeys: Object.keys(entries[0]),
entryStringified: JSON.stringify(entries[0], null, 2).substring(0, 500),
serverType: this.serverType
})
}
if (entries.length === 1) {
resolve(entries[0])
} else {
logger.warn(
`⚠️ Multiple LDAP entries found for username: ${username} (${this.serverType})`
)
resolve(entries[0]) // 使用第一个结果
}
}
})
})
})
}
// 🔐 验证用户密码
async authenticateUser(userDN, password) {
return new Promise((resolve, reject) => {
// 验证输入参数
if (!userDN || typeof userDN !== 'string') {
const error = new Error('User DN is not provided or invalid')
logger.error('❌ LDAP authentication error:', error.message)
reject(error)
return
}
if (!password || typeof password !== 'string') {
logger.debug(`🚫 Invalid or empty password for DN: ${userDN}`)
resolve(false)
return
}
const authClient = this.createClient()
authClient.bind(userDN, password, (err) => {
authClient.unbind() // 立即关闭认证客户端
if (err) {
if (err.name === 'InvalidCredentialsError') {
logger.debug(`🚫 Invalid credentials for DN: ${userDN}`)
resolve(false)
} else {
logger.error('❌ LDAP authentication error:', err)
reject(err)
}
} else {
logger.debug(`✅ Authentication successful for DN: ${userDN}`)
resolve(true)
}
})
})
}
// 📝 提取用户信息
extractUserInfo(ldapEntry, username) {
try {
const attributes = ldapEntry.attributes || []
const userInfo = { username }
// 创建属性映射
const attrMap = {}
attributes.forEach((attr) => {
const name = attr.type || attr.name
const values = Array.isArray(attr.values) ? attr.values : [attr.values]
attrMap[name] = values.length === 1 ? values[0] : values
})
// 根据服务器类型和配置映射用户属性
if (this.isActiveDirectory) {
// Windows AD 特定属性映射
const mapping = this.config.userMapping || {}
// 显示名称优先使用displayName其次cn
userInfo.displayName =
attrMap[mapping.displayName || 'displayName'] ||
attrMap[mapping.displayName || 'cn'] ||
attrMap['displayName'] ||
attrMap['cn'] ||
username
// 邮箱
userInfo.email =
attrMap[mapping.email || 'mail'] ||
attrMap['mail'] ||
attrMap['userPrincipalName'] || // UPN作为后备邮箱
''
// 名字
userInfo.firstName = attrMap[mapping.firstName || 'givenName'] || attrMap['givenName'] || ''
// 姓氏
userInfo.lastName = attrMap[mapping.lastName || 'sn'] || attrMap['sn'] || ''
// Windows AD 特有信息
userInfo.sAMAccountName = attrMap['sAMAccountName'] || username
userInfo.userPrincipalName = attrMap['userPrincipalName'] || ''
// 检查用户账户是否被禁用
const { userAccountControl } = attrMap
if (userAccountControl) {
// 检查 ADS_UF_ACCOUNTDISABLE 标志位 (0x02)
const isDisabled = (parseInt(userAccountControl) & 0x02) !== 0
if (isDisabled) {
userInfo.accountDisabled = true
logger.warn(`⚠️ Windows AD account is disabled: ${username}`)
}
}
logger.debug('📋 Extracted Windows AD user info:', {
username: userInfo.username,
displayName: userInfo.displayName,
email: userInfo.email,
sAMAccountName: userInfo.sAMAccountName,
userPrincipalName: userInfo.userPrincipalName,
accountDisabled: userInfo.accountDisabled || false
})
} else {
// OpenLDAP 标准属性映射
const mapping = this.config.userMapping || {}
userInfo.displayName = attrMap[mapping.displayName || 'cn'] || attrMap['cn'] || username
userInfo.email = attrMap[mapping.email || 'mail'] || attrMap['mail'] || ''
userInfo.firstName = attrMap[mapping.firstName || 'givenName'] || attrMap['givenName'] || ''
userInfo.lastName = attrMap[mapping.lastName || 'sn'] || attrMap['sn'] || ''
logger.debug('📋 Extracted OpenLDAP user info:', {
username: userInfo.username,
displayName: userInfo.displayName,
email: userInfo.email
})
}
// 如果没有displayName尝试组合firstName和lastName
if (!userInfo.displayName || userInfo.displayName === username) {
if (userInfo.firstName || userInfo.lastName) {
userInfo.displayName = `${userInfo.firstName || ''} ${userInfo.lastName || ''}`.trim()
}
}
return userInfo
} catch (error) {
logger.error('❌ Error extracting user info:', error)
return { username }
}
}
// 🔍 验证和清理用户名
validateAndSanitizeUsername(username) {
if (!username || typeof username !== 'string' || username.trim() === '') {
throw new Error('Username is required and must be a non-empty string')
}
const trimmedUsername = username.trim()
if (this.isActiveDirectory) {
// Windows AD 用户名验证:支持 UPN 和 domain\username 格式
// UPN 格式user@domain.com
if (trimmedUsername.includes('@')) {
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
if (!emailRegex.test(trimmedUsername)) {
throw new Error('Invalid UPN format (user@domain.com)')
}
if (trimmedUsername.length > 256) {
throw new Error('UPN cannot exceed 256 characters')
}
return trimmedUsername
}
// Domain\username 格式
if (trimmedUsername.includes('\\')) {
const parts = trimmedUsername.split('\\')
if (parts.length !== 2 || !parts[0] || !parts[1]) {
throw new Error('Invalid domain\\username format')
}
const domain = parts[0]
const user = parts[1]
// 验证域名(允许字母数字和连字符)
const domainRegex = /^[a-zA-Z0-9-]+$/
if (!domainRegex.test(domain)) {
throw new Error('Domain name can only contain letters, numbers, and hyphens')
}
// 验证用户名部分
const userRegex = /^[a-zA-Z0-9._-]+$/
if (!userRegex.test(user)) {
throw new Error(
'Username can only contain letters, numbers, dots, underscores, and hyphens'
)
}
if (trimmedUsername.length > 256) {
throw new Error('Domain\\username cannot exceed 256 characters')
}
return trimmedUsername
}
// 简单用户名格式sAMAccountName
const samAccountRegex = /^[a-zA-Z0-9._-]+$/
if (!samAccountRegex.test(trimmedUsername)) {
throw new Error(
'sAMAccountName can only contain letters, numbers, dots, underscores, and hyphens'
)
}
// sAMAccountName 长度限制AD 限制为 20 字符)
if (trimmedUsername.length > 20) {
throw new Error('sAMAccountName cannot exceed 20 characters')
}
return trimmedUsername
} else {
// OpenLDAP 用户名验证(原有逻辑)
// 用户名只能包含字母、数字、下划线和连字符
const usernameRegex = /^[a-zA-Z0-9_-]+$/
if (!usernameRegex.test(trimmedUsername)) {
throw new Error('Username can only contain letters, numbers, underscores, and hyphens')
}
// 长度限制 (防止过长的输入)
if (trimmedUsername.length > 64) {
throw new Error('Username cannot exceed 64 characters')
}
// 不能以连字符开头或结尾
if (trimmedUsername.startsWith('-') || trimmedUsername.endsWith('-')) {
throw new Error('Username cannot start or end with a hyphen')
}
return trimmedUsername
}
}
// 🔐 主要的登录验证方法
async authenticateUserCredentials(username, password) {
if (!this.config.enabled) {
throw new Error('LDAP authentication is not enabled')
}
// 验证和清理用户名 (防止LDAP注入)
const sanitizedUsername = this.validateAndSanitizeUsername(username)
if (!password || typeof password !== 'string' || password.trim() === '') {
throw new Error('Password is required and must be a non-empty string')
}
// 验证LDAP服务器配置
if (!this.config.server || !this.config.server.url) {
throw new Error('LDAP server URL is not configured')
}
if (!this.config.server.bindDN || typeof this.config.server.bindDN !== 'string') {
throw new Error('LDAP bind DN is not configured')
}
if (
!this.config.server.bindCredentials ||
typeof this.config.server.bindCredentials !== 'string'
) {
throw new Error('LDAP bind credentials are not configured')
}
if (!this.config.server.searchBase || typeof this.config.server.searchBase !== 'string') {
throw new Error('LDAP search base is not configured')
}
const client = this.createClient()
try {
// 1. 使用管理员凭据绑定
await this.bindClient(client)
// 2. 搜索用户 (使用已验证的用户名)
const ldapEntry = await this.searchUser(client, sanitizedUsername)
if (!ldapEntry) {
logger.info(`🚫 User not found in LDAP: ${sanitizedUsername}`)
return { success: false, message: 'Invalid username or password' }
}
// 3. 获取用户DN
logger.debug('🔍 LDAP entry details for DN extraction:', {
hasEntry: !!ldapEntry,
entryType: typeof ldapEntry,
entryKeys: Object.keys(ldapEntry || {}),
dn: ldapEntry.dn,
objectName: ldapEntry.objectName,
dnType: typeof ldapEntry.dn,
objectNameType: typeof ldapEntry.objectName
})
// Use the helper method to extract DN
const userDN = this.extractDN(ldapEntry)
logger.debug(`👤 Extracted user DN: ${userDN} (type: ${typeof userDN})`)
// 验证用户DN
if (!userDN) {
logger.error(`❌ Invalid or missing DN for user: ${sanitizedUsername}`, {
ldapEntryDn: ldapEntry.dn,
ldapEntryObjectName: ldapEntry.objectName,
ldapEntryType: typeof ldapEntry,
extractedDN: userDN
})
return { success: false, message: 'Authentication service error' }
}
// 4. 验证用户密码
const isPasswordValid = await this.authenticateUser(userDN, password)
if (!isPasswordValid) {
logger.info(`🚫 Invalid password for user: ${sanitizedUsername}`)
return { success: false, message: 'Invalid username or password' }
}
// 5. 提取用户信息
const userInfo = this.extractUserInfo(ldapEntry, sanitizedUsername)
// 6. Windows AD 特定检查:验证账户是否被禁用
if (this.isActiveDirectory && userInfo.accountDisabled) {
logger.security(
`🔒 Disabled Windows AD account login attempt: ${sanitizedUsername} from LDAP authentication`
)
return {
success: false,
message: 'Your account has been disabled. Please contact administrator.'
}
}
// 7. 创建或更新本地用户
const user = await userService.createOrUpdateUser(userInfo)
// 8. 检查用户是否被禁用
if (!user.isActive) {
logger.security(
`🔒 Disabled user LDAP login attempt: ${sanitizedUsername} from LDAP authentication`
)
return {
success: false,
message: 'Your account has been disabled. Please contact administrator.'
}
}
// 9. 记录登录
await userService.recordUserLogin(user.id)
// 10. 创建用户会话
const sessionToken = await userService.createUserSession(user.id)
logger.info(
`✅ LDAP authentication successful for user: ${sanitizedUsername} (${this.serverType})`
)
return {
success: true,
user,
sessionToken,
message: 'Authentication successful'
}
} catch (error) {
// 记录详细错误供调试,但不向用户暴露
logger.error('❌ LDAP authentication error:', {
username: sanitizedUsername,
error: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
})
// 返回通用错误消息,避免信息泄露
// 不要尝试解析具体的错误信息因为不同LDAP服务器返回的格式不同
return {
success: false,
message: 'Authentication service unavailable'
}
} finally {
// 确保客户端连接被关闭
if (client) {
client.unbind((err) => {
if (err) {
logger.debug('Error unbinding LDAP client:', err)
}
})
}
}
}
// 🔍 测试LDAP连接
async testConnection() {
if (!this.config.enabled) {
return { success: false, message: 'LDAP is not enabled' }
}
const client = this.createClient()
try {
await this.bindClient(client)
return {
success: true,
message: 'LDAP connection successful',
server: this.config.server.url,
searchBase: this.config.server.searchBase
}
} catch (error) {
logger.error('❌ LDAP connection test failed:', {
error: error.message,
server: this.config.server.url,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
})
// 提供通用错误消息,避免泄露系统细节
let userMessage = 'LDAP connection failed'
// 对于某些已知错误类型,提供有用但不泄露细节的信息
if (error.code === 'ECONNREFUSED') {
userMessage = 'Unable to connect to LDAP server'
} else if (error.code === 'ETIMEDOUT') {
userMessage = 'LDAP server connection timeout'
} else if (error.name === 'InvalidCredentialsError') {
userMessage = 'LDAP bind credentials are invalid'
}
return {
success: false,
message: userMessage,
server: this.config.server.url.replace(/:[^:]*@/, ':***@') // 隐藏密码部分
}
} finally {
if (client) {
client.unbind((err) => {
if (err) {
logger.debug('Error unbinding test LDAP client:', err)
}
})
}
}
}
// 📊 获取LDAP配置信息不包含敏感信息
getConfigInfo() {
const configInfo = {
enabled: this.config.enabled,
serverType: this.serverType,
isActiveDirectory: this.isActiveDirectory,
server: {
url: this.config.server.url,
searchBase: this.config.server.searchBase,
searchFilter: this.config.server.searchFilter,
timeout: this.config.server.timeout,
connectTimeout: this.config.server.connectTimeout
},
userMapping: this.config.userMapping
}
// 添加 TLS 配置信息(不包含敏感数据)
if (this.config.server.url.toLowerCase().startsWith('ldaps://') && this.config.server.tls) {
configInfo.server.tls = {
rejectUnauthorized: this.config.server.tls.rejectUnauthorized,
hasCA: !!this.config.server.tls.ca,
hasCert: !!this.config.server.tls.cert,
hasKey: !!this.config.server.tls.key,
servername: this.config.server.tls.servername
}
}
// Windows AD 特定配置信息
if (this.isActiveDirectory) {
configInfo.activeDirectoryFeatures = {
supportsUPN: true,
supportsDomainUsername: true,
supportsGlobalCatalog: true,
checksAccountDisabled: true
}
}
return configInfo
}
}
module.exports = new LdapService()