|
|
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 |
|
|
|
|
|
|
|
|
if (this.config && this.config.enabled) { |
|
|
this.validateConfiguration() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
logger.warn('⚠️ LDAP authentication may not work properly due to configuration errors') |
|
|
} else { |
|
|
logger.info('✅ LDAP configuration validation passed') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
extractDN(ldapEntry) { |
|
|
if (!ldapEntry) { |
|
|
return null |
|
|
} |
|
|
|
|
|
|
|
|
let dn = null |
|
|
|
|
|
|
|
|
if (ldapEntry.dn) { |
|
|
;({ dn } = ldapEntry) |
|
|
} |
|
|
|
|
|
else if (ldapEntry.objectName) { |
|
|
dn = ldapEntry.objectName |
|
|
} |
|
|
|
|
|
else if (ldapEntry.distinguishedName) { |
|
|
dn = ldapEntry.distinguishedName |
|
|
} |
|
|
|
|
|
else if (typeof ldapEntry === 'string' && ldapEntry.includes('=')) { |
|
|
dn = ldapEntry |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof dn === 'string' && dn.trim() !== '' && dn.includes('=')) { |
|
|
return dn.trim() |
|
|
} |
|
|
|
|
|
return null |
|
|
} |
|
|
|
|
|
|
|
|
extractDomainFromDN(dnString) { |
|
|
try { |
|
|
if (!dnString || typeof dnString !== 'string') { |
|
|
return null |
|
|
} |
|
|
|
|
|
|
|
|
const dcMatches = dnString.match(/DC=([^,]+)/gi) |
|
|
if (!dcMatches || dcMatches.length === 0) { |
|
|
return null |
|
|
} |
|
|
|
|
|
|
|
|
const domainParts = dcMatches.map((match) => { |
|
|
const value = match.replace(/DC=/i, '').trim() |
|
|
return value |
|
|
}) |
|
|
|
|
|
if (domainParts.length > 0) { |
|
|
const domain = domainParts.join('.') |
|
|
logger.debug(`🌐 从DN提取域名: ${domain}`) |
|
|
return domain |
|
|
} |
|
|
|
|
|
return null |
|
|
} catch (error) { |
|
|
logger.debug('⚠️ 域名提取失败:', error.message) |
|
|
return null |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
createClient() { |
|
|
try { |
|
|
const clientOptions = { |
|
|
url: this.config.server.url, |
|
|
timeout: this.config.server.timeout, |
|
|
connectTimeout: this.config.server.connectTimeout, |
|
|
reconnect: true |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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) => { |
|
|
|
|
|
|
|
|
const escapedUsername = username |
|
|
.replace(/\\/g, '\\5c') |
|
|
.replace(/\*/g, '\\2a') |
|
|
.replace(/\(/g, '\\28') |
|
|
.replace(/\)/g, '\\29') |
|
|
.replace(/\0/g, '\\00') |
|
|
.replace(/\//g, '\\2f') |
|
|
|
|
|
const searchFilter = this.config.server.searchFilter.replace('{{username}}', escapedUsername) |
|
|
const searchOptions = { |
|
|
scope: 'sub', |
|
|
filter: searchFilter, |
|
|
attributes: this.config.server.searchAttributes |
|
|
} |
|
|
|
|
|
logger.debug(`🔍 Searching for user: ${username} with filter: ${searchFilter}`) |
|
|
|
|
|
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 |
|
|
}) |
|
|
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` |
|
|
) |
|
|
|
|
|
if (entries.length === 0) { |
|
|
resolve(null) |
|
|
} else { |
|
|
|
|
|
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) |
|
|
}) |
|
|
} |
|
|
|
|
|
if (entries.length === 1) { |
|
|
resolve(entries[0]) |
|
|
} else { |
|
|
logger.warn(`⚠️ Multiple LDAP entries found for username: ${username}`) |
|
|
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) |
|
|
} |
|
|
}) |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
async tryWindowsADAuthentication(username, password) { |
|
|
if (!username || !password) { |
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
const domain = this.extractDomainFromDN(this.config.server.searchBase) |
|
|
|
|
|
const adFormats = [] |
|
|
|
|
|
if (domain) { |
|
|
|
|
|
adFormats.push(`${username}@${domain}`) |
|
|
|
|
|
|
|
|
const domainParts = domain.split('.') |
|
|
if (domainParts.length > 1) { |
|
|
adFormats.push(`${username}@${domainParts.slice(-2).join('.')}`) |
|
|
} |
|
|
|
|
|
|
|
|
const firstDomainPart = domainParts[0] |
|
|
if (firstDomainPart) { |
|
|
adFormats.push(`${firstDomainPart}\\${username}`) |
|
|
adFormats.push(`${firstDomainPart.toUpperCase()}\\${username}`) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
adFormats.push(username) |
|
|
|
|
|
logger.info(`🔄 尝试 ${adFormats.length} 种Windows AD认证格式...`) |
|
|
|
|
|
for (const format of adFormats) { |
|
|
try { |
|
|
logger.info(`🔍 尝试格式: ${format}`) |
|
|
const result = await this.tryDirectBind(format, password) |
|
|
if (result) { |
|
|
logger.info(`✅ Windows AD认证成功: ${format}`) |
|
|
return true |
|
|
} |
|
|
logger.debug(`❌ 认证失败: ${format}`) |
|
|
} catch (error) { |
|
|
logger.debug(`认证异常 ${format}:`, error.message) |
|
|
} |
|
|
} |
|
|
|
|
|
logger.info(`🚫 所有Windows AD格式认证都失败了`) |
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
async tryDirectBind(identifier, password) { |
|
|
return new Promise((resolve, reject) => { |
|
|
const authClient = this.createClient() |
|
|
|
|
|
authClient.bind(identifier, password, (err) => { |
|
|
authClient.unbind() |
|
|
|
|
|
if (err) { |
|
|
if (err.name === 'InvalidCredentialsError') { |
|
|
resolve(false) |
|
|
} else { |
|
|
reject(err) |
|
|
} |
|
|
} else { |
|
|
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 |
|
|
}) |
|
|
|
|
|
|
|
|
const mapping = this.config.userMapping |
|
|
|
|
|
userInfo.displayName = attrMap[mapping.displayName] || username |
|
|
userInfo.email = attrMap[mapping.email] || '' |
|
|
userInfo.firstName = attrMap[mapping.firstName] || '' |
|
|
userInfo.lastName = attrMap[mapping.lastName] || '' |
|
|
|
|
|
|
|
|
if (!userInfo.displayName || userInfo.displayName === username) { |
|
|
if (userInfo.firstName || userInfo.lastName) { |
|
|
userInfo.displayName = `${userInfo.firstName || ''} ${userInfo.lastName || ''}`.trim() |
|
|
} |
|
|
} |
|
|
|
|
|
logger.debug('📋 Extracted user info:', { |
|
|
username: userInfo.username, |
|
|
displayName: userInfo.displayName, |
|
|
email: userInfo.email |
|
|
}) |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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') |
|
|
} |
|
|
|
|
|
|
|
|
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') |
|
|
} |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
await this.bindClient(client) |
|
|
|
|
|
|
|
|
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' } |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
}) |
|
|
|
|
|
|
|
|
const userDN = this.extractDN(ldapEntry) |
|
|
|
|
|
logger.debug(`👤 Extracted user DN: ${userDN} (type: ${typeof userDN})`) |
|
|
|
|
|
|
|
|
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' } |
|
|
} |
|
|
|
|
|
|
|
|
let isPasswordValid = false |
|
|
|
|
|
|
|
|
try { |
|
|
isPasswordValid = await this.authenticateUser(userDN, password) |
|
|
if (isPasswordValid) { |
|
|
logger.info(`✅ DN authentication successful for user: ${sanitizedUsername}`) |
|
|
} |
|
|
} catch (error) { |
|
|
logger.debug( |
|
|
`DN authentication failed for user: ${sanitizedUsername}, error: ${error.message}` |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
if (!isPasswordValid) { |
|
|
logger.debug(`🔄 Trying Windows AD authentication formats for user: ${sanitizedUsername}`) |
|
|
isPasswordValid = await this.tryWindowsADAuthentication(sanitizedUsername, password) |
|
|
if (isPasswordValid) { |
|
|
logger.info(`✅ Windows AD authentication successful for user: ${sanitizedUsername}`) |
|
|
} |
|
|
} |
|
|
|
|
|
if (!isPasswordValid) { |
|
|
logger.info(`🚫 All authentication methods failed for user: ${sanitizedUsername}`) |
|
|
return { success: false, message: 'Invalid username or password' } |
|
|
} |
|
|
|
|
|
|
|
|
const userInfo = this.extractUserInfo(ldapEntry, sanitizedUsername) |
|
|
|
|
|
|
|
|
const user = await userService.createOrUpdateUser(userInfo) |
|
|
|
|
|
|
|
|
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.' |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
await userService.recordUserLogin(user.id) |
|
|
|
|
|
|
|
|
const sessionToken = await userService.createUserSession(user.id) |
|
|
|
|
|
logger.info(`✅ LDAP authentication successful for user: ${sanitizedUsername}`) |
|
|
|
|
|
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 |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
success: false, |
|
|
message: 'Authentication service unavailable' |
|
|
} |
|
|
} finally { |
|
|
|
|
|
if (client) { |
|
|
client.unbind((err) => { |
|
|
if (err) { |
|
|
logger.debug('Error unbinding LDAP client:', err) |
|
|
} |
|
|
}) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
}) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
getConfigInfo() { |
|
|
const configInfo = { |
|
|
enabled: this.config.enabled, |
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
} |
|
|
|
|
|
return configInfo |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = new LdapService() |
|
|
|