mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 完整实现AD域控用户认证系统
主要功能: - 新增LDAP服务连接AD域控服务器 - 实现多格式AD用户认证(sAMAccountName, UPN, 域\用户名, DN) - 支持中文显示名和拼音用户名搜索 - 添加用户账户状态检查(禁用账户检测) - 实现JWT token认证和用户会话管理 新增文件: - src/services/ldapService.js - LDAP核心服务 - src/routes/ldapRoutes.js - AD认证API路由 - src/services/userMappingService.js - 用户映射服务 - web/admin-spa/src/views/UserDashboardView.vue - 用户控制台 - web/admin-spa/src/components/user/ - 用户组件目录 修改功能: - ApiStatsView.vue 增加用户登录按钮和模态框 - 路由系统增加用户专用页面 - 安装ldapjs和jsonwebtoken依赖 技术特性: - 多种认证格式自动尝试 - LDAP referral错误处理 - 详细认证日志和错误码记录 - 前后端完整用户认证流程 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
562
src/routes/ldapRoutes.js
Normal file
562
src/routes/ldapRoutes.js
Normal file
@@ -0,0 +1,562 @@
|
||||
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 { ou = '微店' } = req.query
|
||||
const testDN = `OU=${ou},DC=corp,DC=weidian-inc,DC=com`
|
||||
|
||||
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
|
||||
*/
|
||||
router.get('/user/api-keys', authenticateUser, async (req, res) => {
|
||||
try {
|
||||
const redis = require('../models/redis')
|
||||
const { username } = req.user
|
||||
|
||||
logger.info(`获取用户API Keys: ${username}`)
|
||||
|
||||
// 获取所有API Keys
|
||||
const allKeysPattern = 'api_key:*'
|
||||
const keys = await redis.getClient().keys(allKeysPattern)
|
||||
|
||||
const userKeys = []
|
||||
|
||||
// 筛选属于该用户的API Keys
|
||||
for (const key of keys) {
|
||||
const apiKeyData = await redis.getClient().hgetall(key)
|
||||
if (apiKeyData && apiKeyData.owner === username) {
|
||||
userKeys.push({
|
||||
id: apiKeyData.id,
|
||||
name: apiKeyData.name || '未命名',
|
||||
key: apiKeyData.key,
|
||||
limit: parseInt(apiKeyData.limit) || 1000000,
|
||||
used: parseInt(apiKeyData.used) || 0,
|
||||
createdAt: apiKeyData.createdAt,
|
||||
status: apiKeyData.status || 'active'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const { name, limit } = req.body
|
||||
|
||||
// 检查用户是否已有API Key
|
||||
const redis = require('../models/redis')
|
||||
const allKeysPattern = 'api_key:*'
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
// 生成API Key
|
||||
const crypto = require('crypto')
|
||||
const uuid = require('uuid')
|
||||
|
||||
const keyId = uuid.v4()
|
||||
const apiKey = `cr_${crypto.randomBytes(32).toString('hex')}`
|
||||
|
||||
const keyData = {
|
||||
id: keyId,
|
||||
key: apiKey,
|
||||
name: name || 'AD用户密钥',
|
||||
limit: limit || 100000,
|
||||
used: 0,
|
||||
owner: username,
|
||||
ownerType: 'ad_user',
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'active'
|
||||
}
|
||||
|
||||
// 存储到Redis
|
||||
await redis.getClient().hset(`api_key:${keyId}`, keyData)
|
||||
|
||||
// 创建哈希映射以快速查找
|
||||
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex')
|
||||
await redis.getClient().set(`api_key_hash:${keyHash}`, keyId)
|
||||
|
||||
logger.info(`用户${username}创建API Key成功: ${keyId}`)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'API Key创建成功',
|
||||
apiKey: {
|
||||
id: keyId,
|
||||
key: apiKey,
|
||||
name: keyData.name,
|
||||
limit: keyData.limit,
|
||||
used: 0,
|
||||
createdAt: keyData.createdAt,
|
||||
status: keyData.status
|
||||
}
|
||||
})
|
||||
} 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 = 'api_key:*'
|
||||
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: '获取使用统计失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
750
src/services/ldapService.js
Normal file
750
src/services/ldapService.js
Normal file
@@ -0,0 +1,750 @@
|
||||
const ldap = require('ldapjs')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
class LDAPService {
|
||||
constructor() {
|
||||
this.client = null
|
||||
this.config = {
|
||||
url: process.env.LDAP_URL || 'ldap://172.25.3.100:389',
|
||||
bindDN: process.env.LDAP_BIND_DN || 'LDAP-Proxy-Read',
|
||||
bindPassword: process.env.LDAP_BIND_PASSWORD || 'Y%77JsVK8W',
|
||||
baseDN: process.env.LDAP_BASE_DN || 'OU=微店,DC=corp,DC=weidian-inc,DC=com',
|
||||
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()
|
||||
195
src/services/userMappingService.js
Normal file
195
src/services/userMappingService.js
Normal 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
|
||||
Reference in New Issue
Block a user