Merge pull request #292 from iRubbish/dev

feat: 新增AD域控用户认证系统
This commit is contained in:
Wesley Liddick
2025-08-28 08:43:21 +08:00
committed by GitHub
16 changed files with 3448 additions and 30 deletions

View File

@@ -23,6 +23,7 @@ const openaiClaudeRoutes = require('./routes/openaiClaudeRoutes')
const openaiRoutes = require('./routes/openaiRoutes')
const azureOpenaiRoutes = require('./routes/azureOpenaiRoutes')
const webhookRoutes = require('./routes/webhook')
const ldapRoutes = require('./routes/ldapRoutes')
// Import middleware
const {
@@ -244,6 +245,7 @@ class Application {
this.app.use('/openai', openaiRoutes)
this.app.use('/azure', azureOpenaiRoutes)
this.app.use('/admin/webhook', webhookRoutes)
this.app.use('/admin/ldap', ldapRoutes)
// 🏠 根路径重定向到新版管理界面
this.app.get('/', (req, res) => {

View File

@@ -791,6 +791,8 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params
const {
name,
description,
tokenLimit,
concurrencyLimit,
rateLimitWindow,
@@ -814,6 +816,30 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
// 只允许更新指定字段
const updates = {}
// 处理name字段
if (name !== undefined) {
if (name === null || name === '') {
return res.status(400).json({ error: 'Name cannot be empty' })
}
if (typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({ error: 'Name must be a non-empty string' })
}
if (name.length > 100) {
return res.status(400).json({ error: 'Name must be less than 100 characters' })
}
updates.name = name.trim()
}
// 处理description字段
if (description !== undefined) {
if (description && (typeof description !== 'string' || description.length > 500)) {
return res
.status(400)
.json({ error: 'Description must be a string with less than 500 characters' })
}
updates.description = description || ''
}
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) {
return res.status(400).json({ error: 'Token limit must be a non-negative integer' })
@@ -954,12 +980,20 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
updates.isActive = isActive
}
logger.info(`🔧 Admin updating API key: ${keyId}`, {
updates: Object.keys(updates),
updatesData: updates
})
await apiKeyService.updateApiKey(keyId, updates)
logger.success(`📝 Admin updated API key: ${keyId}`)
return res.json({ success: true, message: 'API key updated successfully' })
} catch (error) {
logger.error('❌ Failed to update API key:', error)
logger.error(`❌ Failed to update API key ${req.params.keyId}:`, {
error: error.message,
stack: error.stack
})
return res.status(500).json({ error: 'Failed to update API key', message: error.message })
}
})

689
src/routes/ldapRoutes.js Normal file
View File

@@ -0,0 +1,689 @@
const express = require('express')
const ldapService = require('../services/ldapService')
const logger = require('../utils/logger')
const router = express.Router()
/**
* 测试LDAP/AD连接
*/
router.get('/test-connection', async (req, res) => {
try {
logger.info('LDAP connection test requested')
const result = await ldapService.testConnection()
if (result.success) {
res.json({
success: true,
message: 'LDAP/AD connection successful',
data: result
})
} else {
res.status(500).json({
success: false,
message: 'LDAP/AD connection failed',
error: result.error,
config: result.config
})
}
} catch (error) {
logger.error('LDAP connection test error:', error)
res.status(500).json({
success: false,
message: 'LDAP connection test failed',
error: error.message
})
}
})
/**
* 获取LDAP配置信息
*/
router.get('/config', (req, res) => {
try {
const config = ldapService.getConfig()
res.json({
success: true,
config
})
} catch (error) {
logger.error('Get LDAP config error:', error)
res.status(500).json({
success: false,
message: 'Failed to get LDAP config',
error: error.message
})
}
})
/**
* 搜索用户
*/
router.post('/search-user', async (req, res) => {
try {
const { username } = req.body
if (!username) {
return res.status(400).json({
success: false,
message: 'Username is required'
})
}
logger.info(`Searching for user: ${username}`)
await ldapService.createConnection()
await ldapService.bind()
const users = await ldapService.searchUser(username)
res.json({
success: true,
message: `Found ${users.length} users`,
users
})
} catch (error) {
logger.error('User search error:', error)
res.status(500).json({
success: false,
message: 'User search failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* 列出所有用户模拟Python代码的describe_ou功能
*/
router.get('/list-users', async (req, res) => {
try {
const { limit = 20, type = 'human' } = req.query
const limitNum = parseInt(limit)
logger.info(`Listing users with limit: ${limitNum}, type: ${type}`)
await ldapService.createConnection()
await ldapService.bind()
const users = await ldapService.listAllUsers(limitNum, type)
res.json({
success: true,
message: `Found ${users.length} users`,
users,
total: users.length,
limit: limitNum,
type
})
} catch (error) {
logger.error('List users error:', error)
res.status(500).json({
success: false,
message: 'List users failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* 测试用户认证
*/
router.post('/test-auth', async (req, res) => {
try {
const { username, password } = req.body
if (!username || !password) {
return res.status(400).json({
success: false,
message: 'Username and password are required'
})
}
logger.info(`Testing authentication for user: ${username}`)
const result = await ldapService.authenticateUser(username, password)
res.json({
success: true,
message: 'Authentication successful',
user: result.user
})
} catch (error) {
logger.error('User authentication test error:', error)
res.status(401).json({
success: false,
message: 'Authentication failed',
error: error.message
})
}
})
/**
* 列出所有OU
*/
router.get('/list-ous', async (req, res) => {
try {
logger.info('Listing all OUs in domain')
await ldapService.createConnection()
await ldapService.bind()
const ous = await ldapService.listOUs()
res.json({
success: true,
message: `Found ${ous.length} OUs`,
ous
})
} catch (error) {
logger.error('List OUs error:', error)
res.status(500).json({
success: false,
message: 'List OUs failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* 验证OU是否存在
*/
router.get('/verify-ou', async (req, res) => {
try {
const defaultOU = process.env.LDAP_DEFAULT_OU || 'YourOU'
const { ou = defaultOU } = req.query
// 使用配置的baseDN来构建测试DN而不是硬编码域名
const config = ldapService.getConfig()
// 从baseDN中提取域部分替换OU部分
const baseDNParts = config.baseDN.split(',')
const domainParts = baseDNParts.filter((part) => part.trim().startsWith('DC='))
const testDN = `OU=${ou},${domainParts.join(',')}`
logger.info(`Verifying OU exists: ${testDN}`)
await ldapService.createConnection()
await ldapService.bind()
const result = await ldapService.verifyOU(testDN)
res.json({
success: true,
message: 'OU verification completed',
testDN,
result
})
} catch (error) {
logger.error('OU verification error:', error)
res.status(500).json({
success: false,
message: 'OU verification failed',
error: error.message
})
} finally {
ldapService.disconnect()
}
})
/**
* LDAP服务状态检查
*/
router.get('/status', async (req, res) => {
try {
const config = ldapService.getConfig()
// 简单的连接测试
const connectionTest = await ldapService.testConnection()
res.json({
success: true,
status: connectionTest.success ? 'connected' : 'disconnected',
config,
lastTest: new Date().toISOString(),
testResult: connectionTest
})
} catch (error) {
logger.error('LDAP status check error:', error)
res.status(500).json({
success: false,
status: 'error',
message: 'Status check failed',
error: error.message
})
}
})
/**
* AD用户登录认证
*/
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
})
}
logger.info(`AD用户登录尝试: ${username}`)
// 使用AD认证用户
const authResult = await ldapService.authenticateUser(username, password)
// 生成用户会话token
const jwt = require('jsonwebtoken')
const config = require('../../config/config')
const userInfo = {
type: 'ad_user',
username: authResult.user.username || authResult.user.cn,
displayName: authResult.user.displayName,
email: authResult.user.email,
groups: authResult.user.groups,
loginTime: new Date().toISOString()
}
const token = jwt.sign(userInfo, config.security.jwtSecret, {
expiresIn: '8h' // 8小时过期
})
logger.info(`AD用户登录成功: ${username}`)
res.json({
success: true,
message: '登录成功',
token,
user: userInfo
})
} catch (error) {
logger.error('AD用户登录失败:', error)
res.status(401).json({
success: false,
message: '用户名或密码错误',
error: error.message
})
}
})
/**
* AD用户token验证
*/
router.get('/verify-token', (req, res) => {
try {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: '未提供有效的认证token'
})
}
const token = authHeader.substring(7)
const jwt = require('jsonwebtoken')
const config = require('../../config/config')
const decoded = jwt.verify(token, config.security.jwtSecret)
if (decoded.type !== 'ad_user') {
return res.status(403).json({
success: false,
message: '无效的用户类型'
})
}
res.json({
success: true,
user: decoded
})
} catch (error) {
logger.error('Token验证失败:', error)
res.status(401).json({
success: false,
message: 'Token无效或已过期'
})
}
})
/**
* AD用户认证中间件
*/
const authenticateUser = (req, res, next) => {
try {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: '未提供有效的认证token'
})
}
const token = authHeader.substring(7)
const jwt = require('jsonwebtoken')
const config = require('../../config/config')
const decoded = jwt.verify(token, config.security.jwtSecret)
if (decoded.type !== 'ad_user') {
return res.status(403).json({
success: false,
message: '无效的用户类型'
})
}
req.user = decoded
next()
} catch (error) {
logger.error('用户认证失败:', error)
res.status(401).json({
success: false,
message: 'Token无效或已过期'
})
}
}
/**
* 获取用户的API Keys
*
* 自动关联逻辑说明:
* 系统迁移过程中存在历史API Key这些Key是在AD集成前手动创建的
* 创建时使用的name字段恰好与AD用户的displayName一致
* 例如: AD用户displayName为"测试用户"对应的API Key name也是"测试用户"
* 为了避免用户重复创建Key系统会自动关联这些历史Key
* 关联规则:
* 1. 优先匹配owner字段(新建的Key)
* 2. 如果没有owner匹配则尝试匹配name字段与displayName
* 3. 找到匹配的历史Key后自动将owner设置为当前用户完成关联
*/
router.get('/user/api-keys', authenticateUser, async (req, res) => {
try {
const apiKeyService = require('../services/apiKeyService')
const redis = require('../models/redis')
const { username, displayName } = req.user
logger.info(`获取用户API Keys: ${username}, displayName: ${displayName}`)
// 使用与admin相同的API Key服务获取所有API Keys的完整信息
const allApiKeys = await apiKeyService.getAllApiKeys()
const userKeys = []
let foundHistoricalKey = false
// 筛选属于该用户的API Keys并处理自动关联
for (const apiKey of allApiKeys) {
logger.debug(
`检查API Key: ${apiKey.id}, name: "${apiKey.name}", owner: "${apiKey.owner || '无'}", displayName: "${displayName}"`
)
// 规则1: 直接owner匹配(已关联的Key)
if (apiKey.owner === username) {
logger.info(`找到已关联的API Key: ${apiKey.id}`)
userKeys.push(apiKey)
}
// 规则2: 历史Key自动关联(name字段匹配displayName且无owner)
else if (displayName && apiKey.name === displayName && !apiKey.owner) {
logger.info(
`🔗 发现历史API Key需要关联: id=${apiKey.id}, name="${apiKey.name}", displayName="${displayName}"`
)
// 自动关联: 设置owner为当前用户
await redis.getClient().hset(`apikey:${apiKey.id}`, 'owner', username)
foundHistoricalKey = true
// 更新本地数据并添加到用户Key列表
apiKey.owner = username
userKeys.push(apiKey)
logger.info(`✅ 历史API Key关联成功: ${apiKey.id} -> ${username}`)
}
}
if (foundHistoricalKey) {
logger.info(`用户 ${username} 自动关联了历史API Key`)
}
res.json({
success: true,
apiKeys: userKeys
})
} catch (error) {
logger.error('获取用户API Keys失败:', error)
res.status(500).json({
success: false,
message: '获取API Keys失败'
})
}
})
/**
* 创建用户API Key
*/
router.post('/user/api-keys', authenticateUser, async (req, res) => {
try {
const { username } = req.user
// 用户创建的API Key不需要任何输入参数都使用默认值
// const { limit } = req.body // 不再从请求体获取limit
// 检查用户是否已有API Key
const redis = require('../models/redis')
const allKeysPattern = 'apikey:*'
const keys = await redis.getClient().keys(allKeysPattern)
let userKeyCount = 0
for (const key of keys) {
const apiKeyData = await redis.getClient().hgetall(key)
if (apiKeyData && apiKeyData.owner === username) {
userKeyCount++
}
}
if (userKeyCount >= 1) {
return res.status(400).json({
success: false,
message: '每个用户只能创建一个API Key'
})
}
// 使用与admin相同的API Key生成服务确保数据结构一致性
const apiKeyService = require('../services/apiKeyService')
// 获取用户的显示名称
const { displayName } = req.user
// 用户创建的API Key名称固定为displayName不允许自定义
const defaultName = displayName || username
const keyParams = {
name: defaultName, // 使用displayName作为API Key名称
tokenLimit: 0, // 固定为无限制
description: `AD用户${username}创建的API Key`,
// AD用户创建的Key添加owner信息以区分用户归属
owner: username,
ownerType: 'ad_user',
// 确保用户创建的Key默认激活
isActive: true,
// 设置基本权限与admin创建保持一致
permissions: 'all',
// 设置合理的并发和速率限制与admin创建保持一致
concurrencyLimit: 0,
rateLimitWindow: 0,
rateLimitRequests: 0,
// 添加标签标识AD用户创建
tags: ['ad-user', 'user-created']
}
const newKey = await apiKeyService.generateApiKey(keyParams)
logger.info(`用户${username}创建API Key成功: ${newKey.id}`)
res.json({
success: true,
message: 'API Key创建成功',
apiKey: {
id: newKey.id,
key: newKey.apiKey, // 返回完整的API Key
name: newKey.name,
tokenLimit: newKey.tokenLimit || 0,
used: 0,
createdAt: newKey.createdAt,
isActive: true,
usage: {
daily: { requests: 0, tokens: 0 },
total: { requests: 0, tokens: 0 }
},
dailyCost: 0
}
})
} catch (error) {
logger.error('创建用户API Key失败:', error)
res.status(500).json({
success: false,
message: '创建API Key失败'
})
}
})
/**
* 获取用户API Key使用统计
*/
router.get('/user/usage-stats', authenticateUser, async (req, res) => {
try {
const { username } = req.user
const redis = require('../models/redis')
// 获取用户的API Keys
const allKeysPattern = 'apikey:*'
const keys = await redis.getClient().keys(allKeysPattern)
let totalUsage = 0
let totalLimit = 0
const userKeys = []
for (const key of keys) {
const apiKeyData = await redis.getClient().hgetall(key)
if (apiKeyData && apiKeyData.owner === username) {
const used = parseInt(apiKeyData.used) || 0
const limit = parseInt(apiKeyData.limit) || 0
totalUsage += used
totalLimit += limit
userKeys.push({
id: apiKeyData.id,
name: apiKeyData.name,
used,
limit,
percentage: limit > 0 ? Math.round((used / limit) * 100) : 0
})
}
}
res.json({
success: true,
stats: {
totalUsage,
totalLimit,
percentage: totalLimit > 0 ? Math.round((totalUsage / totalLimit) * 100) : 0,
keyCount: userKeys.length,
keys: userKeys
}
})
} catch (error) {
logger.error('获取用户使用统计失败:', error)
res.status(500).json({
success: false,
message: '获取使用统计失败'
})
}
})
/**
* 更新用户API Key
*/
router.put('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
try {
const { username } = req.user
const { keyId } = req.params
const updates = req.body
// 验证用户只能编辑自己的API Key
const apiKeyService = require('../services/apiKeyService')
const allApiKeys = await apiKeyService.getAllApiKeys()
const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username)
if (!apiKey) {
return res.status(404).json({
success: false,
message: 'API Key 不存在或无权限'
})
}
// 限制用户只能修改特定字段不允许修改name
const allowedFields = ['description', 'isActive']
const filteredUpdates = {}
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key)) {
filteredUpdates[key] = value
}
}
await apiKeyService.updateApiKey(keyId, filteredUpdates)
logger.info(`用户 ${username} 更新了 API Key: ${keyId}`)
res.json({
success: true,
message: 'API Key 更新成功'
})
} catch (error) {
logger.error('更新用户API Key失败:', error)
res.status(500).json({
success: false,
message: '更新 API Key 失败'
})
}
})
/**
* 删除用户API Key
*/
router.delete('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
try {
const { username } = req.user
const { keyId } = req.params
// 验证用户只能删除自己的API Key
const apiKeyService = require('../services/apiKeyService')
const allApiKeys = await apiKeyService.getAllApiKeys()
const apiKey = allApiKeys.find((key) => key.id === keyId && key.owner === username)
if (!apiKey) {
return res.status(404).json({
success: false,
message: 'API Key 不存在或无权限'
})
}
await apiKeyService.deleteApiKey(keyId)
logger.info(`用户 ${username} 删除了 API Key: ${keyId}`)
res.json({
success: true,
message: 'API Key 删除成功'
})
} catch (error) {
logger.error('删除用户API Key失败:', error)
res.status(500).json({
success: false,
message: '删除 API Key 失败'
})
}
})
module.exports = router

View File

@@ -32,7 +32,9 @@ class ApiKeyService {
enableClientRestriction = false,
allowedClients = [],
dailyCostLimit = 0,
tags = []
tags = [],
owner = null,
ownerType = null
} = options
// 生成简单的API Key (64字符十六进制)
@@ -66,7 +68,9 @@ class ApiKeyService {
createdAt: new Date().toISOString(),
lastUsedAt: '',
expiresAt: expiresAt || '',
createdBy: 'admin' // 可以根据需要扩展用户系统
createdBy: 'admin', // 可以根据需要扩展用户系统
owner: owner || '',
ownerType: ownerType || ''
}
// 保存API Key数据并建立哈希映射
@@ -99,7 +103,9 @@ class ApiKeyService {
tags: JSON.parse(keyData.tags || '[]'),
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
createdBy: keyData.createdBy
createdBy: keyData.createdBy,
owner: keyData.owner,
ownerType: keyData.ownerType
}
}
@@ -294,11 +300,21 @@ class ApiKeyService {
// 📝 更新API Key
async updateApiKey(keyId, updates) {
try {
logger.debug(`🔧 Updating API key ${keyId} with:`, updates)
const keyData = await redis.getApiKey(keyId)
if (!keyData || Object.keys(keyData).length === 0) {
logger.error(`❌ API key not found: ${keyId}`)
throw new Error('API key not found')
}
logger.debug(`📋 Current API key data:`, {
id: keyData.id,
name: keyData.name,
owner: keyData.owner,
ownerType: keyData.ownerType
})
// 允许更新的字段
const allowedUpdates = [
'name',
@@ -344,7 +360,10 @@ class ApiKeyService {
// 更新时不需要重新建立哈希映射因为API Key本身没有变化
await redis.setApiKey(keyId, updatedData)
logger.success(`📝 Updated API key: ${keyId}`)
logger.success(`📝 Updated API key: ${keyId}`, {
updatedFields: Object.keys(updates),
newName: updatedData.name
})
return { success: true }
} catch (error) {

761
src/services/ldapService.js Normal file
View File

@@ -0,0 +1,761 @@
const ldap = require('ldapjs')
const logger = require('../utils/logger')
class LDAPService {
constructor() {
this.client = null
// 检查必需的LDAP配置
if (
!process.env.LDAP_URL ||
!process.env.LDAP_BIND_DN ||
!process.env.LDAP_BIND_PASSWORD ||
!process.env.LDAP_BASE_DN
) {
logger.warn('⚠️ LDAP配置不完整请检查.env文件中的LDAP配置项')
}
this.config = {
url: process.env.LDAP_URL || '',
bindDN: process.env.LDAP_BIND_DN || '',
bindPassword: process.env.LDAP_BIND_PASSWORD || '',
baseDN: process.env.LDAP_BASE_DN || '',
searchFilter: process.env.LDAP_SEARCH_FILTER || '(&(objectClass=user)(cn={username}))',
timeout: parseInt(process.env.LDAP_TIMEOUT) || 10000,
connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000
}
}
/**
* 创建LDAP连接
*/
createConnection() {
return new Promise((resolve, reject) => {
const options = {
url: this.config.url,
timeout: this.config.timeout,
connectTimeout: this.config.connectTimeout,
reconnect: false,
// 匹配Python代码中的设置禁用referrals
followReferrals: false,
// LDAP协议版本3
version: 3,
// 增加兼容性选项
strictDN: false
}
this.client = ldap.createClient(options)
// 连接超时处理
const timeoutTimer = setTimeout(() => {
this.client.destroy()
reject(new Error(`LDAP connection timeout after ${this.config.connectTimeout}ms`))
}, this.config.connectTimeout)
// 连接成功
this.client.on('connect', () => {
clearTimeout(timeoutTimer)
logger.info('LDAP connection established successfully')
resolve()
})
// 连接错误
this.client.on('error', (err) => {
clearTimeout(timeoutTimer)
logger.error('LDAP connection error:', err)
reject(err)
})
// 连接关闭
this.client.on('close', () => {
logger.info('LDAP connection closed')
})
})
}
/**
* 绑定LDAP连接认证
*/
bind() {
return new Promise((resolve, reject) => {
if (!this.client) {
return reject(new Error('LDAP client not initialized'))
}
this.client.bind(this.config.bindDN, this.config.bindPassword, (err) => {
if (err) {
logger.error('LDAP bind failed:', err)
reject(err)
} else {
logger.info('LDAP bind successful')
resolve()
}
})
})
}
/**
* 测试AD域控连接
*/
async testConnection() {
try {
logger.info('Testing LDAP/AD connection...')
logger.info(`Connecting to: ${this.config.url}`)
logger.info(`Bind DN: ${this.config.bindDN}`)
logger.info(`Base DN: ${this.config.baseDN}`)
await this.createConnection()
await this.bind()
// 先测试连接和绑定是否真的成功
logger.info('LDAP connection and bind successful')
// 尝试简单的根 DSE 查询来验证连接
let searchResult = null
try {
searchResult = await this.testRootDSE()
logger.info('Root DSE query successful')
} catch (searchError) {
logger.warn('Root DSE query failed, trying base search:', searchError.message)
try {
searchResult = await this.testSearch()
} catch (baseSearchError) {
logger.warn('Base search also failed:', baseSearchError.message)
// 连接成功但搜索失败,仍然返回部分成功
return {
success: true,
message:
'LDAP connection and authentication successful, but search requires DN adjustment',
connectionTest: 'SUCCESS',
authTest: 'SUCCESS',
searchTest: `FAILED - ${baseSearchError.message}`,
config: {
url: this.config.url,
bindDN: this.config.bindDN,
baseDN: this.config.baseDN,
searchFilter: this.config.searchFilter
}
}
}
}
logger.info('LDAP/AD full connection test successful')
return {
success: true,
message: 'LDAP/AD connection test successful',
connectionTest: 'SUCCESS',
authTest: 'SUCCESS',
searchTest: 'SUCCESS',
config: {
url: this.config.url,
bindDN: this.config.bindDN,
baseDN: this.config.baseDN,
searchFilter: this.config.searchFilter
},
searchResult
}
} catch (error) {
logger.error('LDAP/AD connection test failed:', error)
return {
success: false,
message: `LDAP/AD connection test failed: ${error.message}`,
error: error.message,
connectionTest: error.message.includes('connect') ? 'FAILED' : 'UNKNOWN',
authTest:
error.message.includes('bind') || error.message.includes('authentication')
? 'FAILED'
: 'UNKNOWN',
config: {
url: this.config.url,
bindDN: this.config.bindDN,
baseDN: this.config.baseDN
}
}
} finally {
this.disconnect()
}
}
/**
* 测试根DSE查询最基本的LDAP查询
*/
testRootDSE() {
return new Promise((resolve, reject) => {
const searchOptions = {
filter: '(objectClass=*)',
scope: 'base',
attributes: ['*']
}
this.client.search('', searchOptions, (err, res) => {
if (err) {
reject(err)
return
}
let rootDSE = null
res.on('searchEntry', (entry) => {
rootDSE = {
dn: entry.dn,
namingContexts: entry.object?.namingContexts || entry.attributes?.namingContexts,
supportedLDAPVersion:
entry.object?.supportedLDAPVersion || entry.attributes?.supportedLDAPVersion,
defaultNamingContext:
entry.object?.defaultNamingContext || entry.attributes?.defaultNamingContext,
raw: entry.object || entry.attributes
}
})
res.on('referral', (referral) => {
logger.info(`Root DSE referral: ${referral}`)
})
res.on('error', (error) => {
if (error.message && error.message.toLowerCase().includes('referral')) {
logger.warn(`Root DSE referral error (ignored): ${error.message}`)
return
}
reject(error)
})
res.on('end', () => {
if (rootDSE) {
logger.info('Root DSE query completed successfully')
resolve(rootDSE)
} else {
resolve({ message: 'No Root DSE data returned' })
}
})
})
})
}
/**
* 执行测试搜索
*/
testSearch() {
return new Promise((resolve, reject) => {
// 匹配Python代码的搜索查找用户对象获取CN和userAccountControl属性
const searchOptions = {
filter: '(objectClass=user)',
scope: 'sub', // SCOPE_SUBTREE in Python
attributes: ['CN', 'userAccountControl'],
sizeLimit: 10 // 限制结果数量
}
this.client.search(this.config.baseDN, searchOptions, (err, res) => {
if (err) {
reject(err)
return
}
let entryCount = 0
const entries = []
res.on('searchEntry', (entry) => {
entryCount++
entries.push({
dn: entry.dn,
cn: entry.object.CN || entry.object.cn,
userAccountControl: entry.object.userAccountControl
})
})
res.on('referral', (referral) => {
// 记录referral但不作为错误处理
logger.info(`LDAP referral received: ${referral}`)
})
res.on('error', (error) => {
// 如果是referral相关错误不视为失败
if (error.message && error.message.toLowerCase().includes('referral')) {
logger.warn(`LDAP referral error (ignored): ${error.message}`)
return
}
reject(error)
})
res.on('end', (result) => {
logger.info(
`Search test completed. Found ${entryCount} entries, status: ${result.status}`
)
resolve({
entryCount,
status: result.status,
entries: entries.slice(0, 5)
})
})
})
})
}
/**
* 根据用户名搜索用户
*/
searchUser(username) {
return new Promise((resolve, reject) => {
if (!this.client) {
return reject(new Error('LDAP client not initialized'))
}
const filter = this.config.searchFilter.replace(/{username}/g, username)
const searchOptions = {
filter,
scope: 'sub',
attributes: [
'dn',
'sAMAccountName',
'displayName',
'mail',
'memberOf',
'cn',
'userAccountControl'
]
}
logger.info(`Searching for user: ${username}, Filter: ${filter}`)
this.client.search(this.config.baseDN, searchOptions, (err, res) => {
if (err) {
reject(err)
return
}
const users = []
res.on('searchEntry', (entry) => {
const obj = entry.object || {}
const attrs = entry.attributes || []
// 创建属性查找函数
const getAttr = (name) => {
if (obj[name]) {
return obj[name]
}
const attr = attrs.find((a) => a.type === name)
return attr ? (Array.isArray(attr.values) ? attr.values[0] : attr.values) : null
}
const user = {
dn: entry.dn,
username: getAttr('sAMAccountName'),
displayName: getAttr('displayName'),
email: getAttr('mail'),
cn: getAttr('cn'),
userAccountControl: getAttr('userAccountControl'),
groups: (() => {
const memberOf = getAttr('memberOf')
return Array.isArray(memberOf) ? memberOf : memberOf ? [memberOf] : []
})()
}
users.push(user)
})
res.on('referral', (referral) => {
logger.info(`LDAP referral received during user search: ${referral}`)
})
res.on('error', (error) => {
if (error.message && error.message.toLowerCase().includes('referral')) {
logger.warn(`LDAP referral error during user search (ignored): ${error.message}`)
return
}
reject(error)
})
res.on('end', () => {
logger.info(`Found ${users.length} users for username: ${username}`)
resolve(users)
})
})
})
}
/**
* 列出所有用户模拟Python代码的describe_ou功能
*/
listAllUsers(limit = 20, type = 'human') {
return new Promise((resolve, reject) => {
if (!this.client) {
return reject(new Error('LDAP client not initialized'))
}
// 根据类型选择不同的搜索过滤器
let filter
if (type === 'computer') {
// 只显示计算机账户
filter = '(&(objectClass=user)(sAMAccountName=*$))'
} else if (type === 'human') {
// 只显示人员账户(排除计算机账户)
filter = '(&(objectClass=user)(!(sAMAccountName=*$)))'
} else {
// 显示所有用户
filter = '(objectClass=user)'
}
const searchOptions = {
filter,
scope: 'sub', // SCOPE_SUBTREE
attributes: ['CN', 'userAccountControl', 'sAMAccountName', 'displayName', 'mail', 'dn']
// 不使用 sizeLimit而是在客户端限制结果数量
}
logger.info(`Listing all users with filter: ${searchOptions.filter}, limit: ${limit}`)
this.client.search(this.config.baseDN, searchOptions, (err, res) => {
if (err) {
reject(err)
return
}
const users = []
res.on('searchEntry', (entry) => {
// 如果已经达到限制,停止处理
if (users.length >= limit) {
return
}
const obj = entry.object || {}
const attrs = entry.attributes || []
// 创建属性查找函数
const getAttr = (name) => {
if (obj[name]) {
return obj[name]
}
const attr = attrs.find((a) => a.type === name)
return attr ? (Array.isArray(attr.values) ? attr.values[0] : attr.values) : null
}
const user = {
dn: entry.dn,
cn: getAttr('CN') || getAttr('cn'),
sAMAccountName: getAttr('sAMAccountName'),
displayName: getAttr('displayName'),
email: getAttr('mail'),
userAccountControl: getAttr('userAccountControl'),
// 为了兼容Python代码的数据结构
org: entry.dn,
// 调试信息 (限制原始数据大小)
raw: users.length < 3 ? { object: entry.object, attributes: entry.attributes } : null
}
users.push(user)
})
res.on('referral', (referral) => {
logger.info(`LDAP referral received during user listing: ${referral}`)
})
res.on('error', (error) => {
if (error.message && error.message.toLowerCase().includes('referral')) {
logger.warn(`LDAP referral error during user listing (ignored): ${error.message}`)
return
}
reject(error)
})
res.on('end', () => {
logger.info(`Found ${users.length} users total`)
resolve(users)
})
})
})
}
/**
* 验证用户凭据
*/
async authenticateUser(username, password) {
try {
// 先搜索用户获取DN
await this.createConnection()
await this.bind()
const users = await this.searchUser(username)
if (users.length === 0) {
throw new Error('User not found')
}
// 修复DN提取逻辑处理ldapjs的DN对象
let userDN = users[0].dn
if (userDN && typeof userDN === 'object') {
// ldapjs返回的是DN对象需要正确转换为字符串
if (userDN.toString && typeof userDN.toString === 'function') {
userDN = userDN.toString()
} else if (userDN.format && typeof userDN.format === 'function') {
userDN = userDN.format()
} else {
// 从dn对象中提取rdns信息手动构建DN字符串
logger.info('User DN object structure:', JSON.stringify(userDN, null, 2))
throw new Error('Unable to extract user DN from object')
}
} else if (typeof userDN !== 'string') {
throw new Error('Invalid DN format')
}
logger.info(`Attempting to authenticate with DN: ${userDN}`)
logger.info(`User sAMAccountName: ${users[0].sAMAccountName || users[0].username}`)
logger.info(`User Account Control: ${users[0].userAccountControl}`)
// 检查账户状态
const userAccountControl = parseInt(users[0].userAccountControl) || 0
if (userAccountControl & 2) {
// UF_ACCOUNTDISABLE = 2
throw new Error('User account is disabled')
}
// 断开管理员连接
this.disconnect()
// 尝试多种认证格式
const sAMAccountName = users[0].sAMAccountName || users[0].username
const authFormats = [
sAMAccountName, // 直接使用sAMAccountName
`${sAMAccountName}@corp.weidian-inc.com`, // UPN格式
`${sAMAccountName}@weidian-inc.com`, // 简化UPN格式
`corp\\${sAMAccountName}`, // 域\\用户名格式
`CORP\\${sAMAccountName}`, // 大写域\\用户名格式
`weidian-inc\\${sAMAccountName}`, // 完整域名\\用户名格式
userDN // 完整DN最后尝试
].filter(Boolean)
logger.info(`Trying authentication with formats: ${JSON.stringify(authFormats)}`)
for (const authFormat of authFormats) {
try {
logger.info(`Attempting authentication with: ${authFormat}`)
const userClient = ldap.createClient({
url: this.config.url,
timeout: 10000,
connectTimeout: 10000,
idleTimeout: 30000
})
const authResult = await new Promise((resolve, reject) => {
let resolved = false
// 设置错误处理
userClient.on('error', (err) => {
if (!resolved) {
resolved = true
logger.warn(`Connection error with ${authFormat}:`, err.message)
userClient.destroy()
reject(err)
}
})
userClient.on('connect', () => {
logger.info(`Connected for authentication with: ${authFormat}`)
// 尝试使用用户凭据绑定
userClient.bind(authFormat, password, (err) => {
if (!resolved) {
resolved = true
if (err) {
logger.warn(
`Bind failed with ${authFormat}: ${err.name} - ${err.message} (Code: ${err.code})`
)
userClient.destroy()
reject(err)
} else {
logger.info(`Bind successful with ${authFormat}`)
userClient.unbind()
resolve(true)
}
}
})
})
// 超时处理
setTimeout(() => {
if (!resolved) {
resolved = true
userClient.destroy()
reject(new Error('Authentication timeout'))
}
}, 5000)
})
if (authResult) {
logger.info(`User ${username} authenticated successfully with format: ${authFormat}`)
return {
success: true,
user: users[0]
}
}
} catch (err) {
logger.warn(
`Authentication failed with format ${authFormat}: ${err.name} - ${err.message}`
)
continue
}
}
// 所有格式都失败
throw new Error('Invalid username or password')
} catch (error) {
logger.error('User authentication error:', error)
throw error
} finally {
this.disconnect()
}
}
/**
* 关闭连接
*/
disconnect() {
if (this.client) {
this.client.destroy()
this.client = null
logger.info('LDAP connection closed')
}
}
/**
* 列出所有OU
*/
listOUs() {
return new Promise((resolve, reject) => {
if (!this.client) {
return reject(new Error('LDAP client not initialized'))
}
const searchOptions = {
filter: '(objectClass=organizationalUnit)',
scope: 'sub',
attributes: ['ou', 'dn', 'objectClass', 'description']
}
// 从域根开始搜索所有OU
const baseDN = 'DC=corp,DC=weidian-inc,DC=com'
logger.info(`Searching for all OUs in: ${baseDN}`)
this.client.search(baseDN, searchOptions, (err, res) => {
if (err) {
reject(err)
return
}
const ous = []
res.on('searchEntry', (entry) => {
const obj = entry.object || {}
const attrs = entry.attributes || []
const getAttr = (name) => {
if (obj[name]) {
return obj[name]
}
const attr = attrs.find((a) => a.type === name)
return attr ? (Array.isArray(attr.values) ? attr.values[0] : attr.values) : null
}
const ou = {
dn: entry.dn,
ou: getAttr('ou'),
description: getAttr('description'),
objectClass: getAttr('objectClass')
}
ous.push(ou)
})
res.on('referral', (referral) => {
logger.info(`OUs search referral: ${referral}`)
})
res.on('error', (error) => {
if (error.message && error.message.toLowerCase().includes('referral')) {
logger.warn(`OUs search referral error (ignored): ${error.message}`)
return
}
reject(error)
})
res.on('end', () => {
logger.info(`Found ${ous.length} OUs total`)
resolve(ous)
})
})
})
}
/**
* 验证OU是否存在
*/
verifyOU(ouDN) {
return new Promise((resolve, reject) => {
if (!this.client) {
return reject(new Error('LDAP client not initialized'))
}
const searchOptions = {
filter: '(objectClass=organizationalUnit)',
scope: 'base',
attributes: ['ou', 'dn', 'objectClass']
}
logger.info(`Searching for OU: ${ouDN}`)
this.client.search(ouDN, searchOptions, (err, res) => {
if (err) {
reject(err)
return
}
let found = false
let ouInfo = null
res.on('searchEntry', (entry) => {
found = true
ouInfo = {
dn: entry.dn,
ou: entry.object?.ou || entry.attributes?.find((a) => a.type === 'ou')?.values,
objectClass:
entry.object?.objectClass ||
entry.attributes?.find((a) => a.type === 'objectClass')?.values
}
})
res.on('referral', (referral) => {
logger.info(`OU search referral: ${referral}`)
})
res.on('error', (error) => {
if (error.message && error.message.toLowerCase().includes('referral')) {
logger.warn(`OU search referral error (ignored): ${error.message}`)
return
}
reject(error)
})
res.on('end', () => {
resolve({
exists: found,
dn: ouDN,
info: ouInfo
})
})
})
})
}
/**
* 获取配置信息(不包含密码)
*/
getConfig() {
return {
url: this.config.url,
bindDN: this.config.bindDN,
baseDN: this.config.baseDN,
searchFilter: this.config.searchFilter,
timeout: this.config.timeout,
connectTimeout: this.config.connectTimeout
}
}
}
module.exports = new LDAPService()

View File

@@ -0,0 +1,195 @@
const logger = require('../utils/logger')
/**
* 用户映射服务 - 处理AD用户数据转换和过滤
*/
class UserMappingService {
/**
* 解析AD用户账户控制状态
*/
static parseUserAccountControl(uac) {
if (!uac) {
return { disabled: true, description: 'Unknown' }
}
const uacValue = parseInt(uac)
const flags = {
SCRIPT: 0x00000001,
ACCOUNTDISABLE: 0x00000002,
HOMEDIR_REQUIRED: 0x00000008,
LOCKOUT: 0x00000010,
PASSWD_NOTREQD: 0x00000020,
PASSWD_CANT_CHANGE: 0x00000040,
ENCRYPTED_TEXT_PASSWORD_ALLOWED: 0x00000080,
TEMP_DUPLICATE_ACCOUNT: 0x00000100,
NORMAL_ACCOUNT: 0x00000200,
INTERDOMAIN_TRUST_ACCOUNT: 0x00000800,
WORKSTATION_TRUST_ACCOUNT: 0x00001000,
SERVER_TRUST_ACCOUNT: 0x00002000,
DONT_EXPIRE_PASSWD: 0x00010000,
MNS_LOGON_ACCOUNT: 0x00020000,
SMARTCARD_REQUIRED: 0x00040000,
TRUSTED_FOR_DELEGATION: 0x00080000,
NOT_DELEGATED: 0x00100000,
USE_DES_KEY_ONLY: 0x00200000,
DONT_REQUIRE_PREAUTH: 0x00400000,
PASSWORD_EXPIRED: 0x00800000,
TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: 0x01000000,
PARTIAL_SECRETS_ACCOUNT: 0x04000000
}
const status = {
disabled: !!(uacValue & flags.ACCOUNTDISABLE),
locked: !!(uacValue & flags.LOCKOUT),
passwordExpired: !!(uacValue & flags.PASSWORD_EXPIRED),
normalAccount: !!(uacValue & flags.NORMAL_ACCOUNT),
passwordNotRequired: !!(uacValue & flags.PASSWD_NOTREQD),
dontExpirePassword: !!(uacValue & flags.DONT_EXPIRE_PASSWD),
description: this.getUserAccountControlDescription(uacValue)
}
return status
}
/**
* 获取用户账户控制的描述
*/
static getUserAccountControlDescription(uac) {
const uacValue = parseInt(uac)
if (uacValue & 0x00000002) {
return 'Account Disabled'
}
if (uacValue & 0x00000010) {
return 'Account Locked'
}
if (uacValue & 0x00800000) {
return 'Password Expired'
}
if (uacValue & 0x00000200) {
return 'Normal User Account'
}
return `UAC: ${uacValue}`
}
/**
* 过滤和映射AD用户数据
* 模拟Python代码中的get_ad()函数逻辑
*/
static mapAdUsers(searchResults) {
if (!Array.isArray(searchResults)) {
return []
}
// 移除第一个元素Python代码中的slist.pop(0)
const userList = searchResults.slice(1)
const mappedUsers = []
for (const user of userList) {
try {
const userObj = {
org: user.dn || user.distinguishedName,
cn: null,
userAccountControl: null,
accountStatus: null
}
// 提取CN
if (user.cn || user.CN) {
userObj.cn = user.cn || user.CN
} else {
// 如果没有CN属性跳过此用户
continue
}
// 提取userAccountControl
if (user.userAccountControl) {
userObj.userAccountControl = user.userAccountControl
userObj.accountStatus = this.parseUserAccountControl(user.userAccountControl)
} else {
// 如果没有userAccountControl跳过此用户
continue
}
mappedUsers.push(userObj)
} catch (error) {
logger.warn(`Error processing user entry: ${error.message}`, { user })
continue
}
}
return mappedUsers
}
/**
* 过滤活跃用户(未禁用的账户)
*/
static filterActiveUsers(users) {
return users.filter((user) => user.accountStatus && !user.accountStatus.disabled)
}
/**
* 根据用户名搜索(支持模糊匹配)
*/
static searchUsersByName(users, searchTerm) {
if (!searchTerm) {
return users
}
const term = searchTerm.toLowerCase()
return users.filter((user) => user.cn && user.cn.toLowerCase().includes(term))
}
/**
* 格式化用户信息用于显示
*/
static formatUserInfo(user) {
return {
name: user.cn,
distinguishedName: user.org,
accountControl: user.userAccountControl,
status: user.accountStatus
? {
enabled: !user.accountStatus.disabled,
locked: user.accountStatus.locked,
description: user.accountStatus.description
}
: null
}
}
/**
* 获取用户统计信息
*/
static getUserStats(users) {
const stats = {
total: users.length,
active: 0,
disabled: 0,
locked: 0,
passwordExpired: 0
}
users.forEach((user) => {
if (user.accountStatus) {
if (!user.accountStatus.disabled) {
stats.active++
}
if (user.accountStatus.disabled) {
stats.disabled++
}
if (user.accountStatus.locked) {
stats.locked++
}
if (user.accountStatus.passwordExpired) {
stats.passwordExpired++
}
}
})
return stats
}
}
module.exports = UserMappingService