mirror of
https://github.com/opsre/LiteOps.git
synced 2026-02-27 18:40:46 +08:00
✨ feat: 新增LDAP认证
This commit is contained in:
@@ -73,6 +73,14 @@ LiteOps 是一个专注实用性的 CI/CD 平台。只解决真问题 —— 自
|
||||
<img src="image/cerdentials_kubeconfig.png" alt="凭据配置" width="90%"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="image/notify.png" alt="通知设置" width="90%"/>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="image/basic_ldap.png" alt="认证设置" width="70%"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 技术架构
|
||||
|
||||
@@ -10,6 +10,8 @@ class User(models.Model):
|
||||
name = models.CharField(max_length=50, null=True, verbose_name='姓名')
|
||||
password = models.CharField(max_length=128, null=True, verbose_name='密码')
|
||||
email = models.EmailField(max_length=100, unique=True, null=True, verbose_name='邮箱')
|
||||
user_type = models.CharField(max_length=20, default='system', null=True, verbose_name='用户类型')
|
||||
ldap_dn = models.CharField(max_length=255, null=True, blank=True, verbose_name='LDAP DN')
|
||||
status = models.SmallIntegerField(null=True, verbose_name='状态')
|
||||
login_time = models.DateTimeField(null=True, verbose_name='最后登录时间')
|
||||
create_time = models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')
|
||||
@@ -388,4 +390,30 @@ class LoginAttempt(models.Model):
|
||||
unique_together = ['user', 'ip_address'] # 确保用户和IP组合唯一
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username if self.user else 'Unknown'} - {self.ip_address}"
|
||||
return f"{self.user.username if self.user else 'Unknown'} - {self.ip_address}"
|
||||
|
||||
|
||||
class LDAPConfig(models.Model):
|
||||
"""
|
||||
LDAP配置表 - 存储LDAP服务器配置信息
|
||||
"""
|
||||
id = models.AutoField(primary_key=True)
|
||||
enabled = models.BooleanField(default=False, verbose_name='启用LDAP认证')
|
||||
server_host = models.CharField(max_length=255, null=True, verbose_name='LDAP服务器地址')
|
||||
server_port = models.IntegerField(default=389, verbose_name='LDAP服务器端口')
|
||||
use_ssl = models.BooleanField(default=False, verbose_name='使用SSL/TLS')
|
||||
base_dn = models.CharField(max_length=255, null=True, verbose_name='Base DN')
|
||||
bind_dn = models.CharField(max_length=255, null=True, blank=True, verbose_name='绑定DN', help_text='管理员DN,用于搜索用户')
|
||||
bind_password = models.CharField(max_length=255, null=True, blank=True, verbose_name='绑定密码')
|
||||
user_search_filter = models.CharField(max_length=255, default='(uid={username})', verbose_name='用户搜索过滤器')
|
||||
user_attr_map = models.JSONField(default=dict, verbose_name='用户属性映射', help_text='LDAP属性到系统属性的映射')
|
||||
timeout = models.IntegerField(default=10, verbose_name='连接超时时间(秒)')
|
||||
update_time = models.DateTimeField(auto_now=True, null=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'ldap_config'
|
||||
verbose_name = 'LDAP配置'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return f"LDAP配置 - {self.server_host}:{self.server_port}"
|
||||
44
backend/apps/utils/crypto.py
Normal file
44
backend/apps/utils/crypto.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import base64
|
||||
from cryptography.fernet import Fernet
|
||||
from django.conf import settings
|
||||
import hashlib
|
||||
|
||||
class CryptoUtils:
|
||||
"""加密解密工具类"""
|
||||
|
||||
@staticmethod
|
||||
def _get_key():
|
||||
"""获取加密密钥"""
|
||||
key_material = settings.SECRET_KEY.encode('utf-8')
|
||||
digest = hashlib.sha256(key_material).digest()
|
||||
key = base64.urlsafe_b64encode(digest)
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def encrypt_password(password):
|
||||
"""加密密码"""
|
||||
if not password:
|
||||
return password
|
||||
|
||||
try:
|
||||
key = CryptoUtils._get_key()
|
||||
fernet = Fernet(key)
|
||||
encrypted = fernet.encrypt(password.encode('utf-8'))
|
||||
return base64.urlsafe_b64encode(encrypted).decode('utf-8')
|
||||
except Exception:
|
||||
return password
|
||||
|
||||
@staticmethod
|
||||
def decrypt_password(encrypted_password):
|
||||
"""解密密码"""
|
||||
if not encrypted_password:
|
||||
return encrypted_password
|
||||
|
||||
try:
|
||||
key = CryptoUtils._get_key()
|
||||
fernet = Fernet(key)
|
||||
encrypted_data = base64.urlsafe_b64decode(encrypted_password.encode('utf-8'))
|
||||
decrypted = fernet.decrypt(encrypted_data)
|
||||
return decrypted.decode('utf-8')
|
||||
except Exception:
|
||||
return encrypted_password
|
||||
490
backend/apps/views/ldap.py
Normal file
490
backend/apps/views/ldap.py
Normal file
@@ -0,0 +1,490 @@
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
import ldap3
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db import transaction
|
||||
from ..models import User, LDAPConfig
|
||||
from ..utils.auth import jwt_auth_required
|
||||
from ..utils.crypto import CryptoUtils
|
||||
|
||||
logger = logging.getLogger('apps')
|
||||
|
||||
def generate_id():
|
||||
"""生成唯一ID"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
class LDAPError(Exception):
|
||||
"""LDAP相关异常"""
|
||||
pass
|
||||
|
||||
class APIResponse:
|
||||
"""统一的API响应处理"""
|
||||
@staticmethod
|
||||
def success(message, data=None):
|
||||
return JsonResponse({'code': 200, 'message': message, 'data': data})
|
||||
|
||||
@staticmethod
|
||||
def error(code, message):
|
||||
return JsonResponse({'code': code, 'message': message})
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class LDAPConfigView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def get(self, request):
|
||||
"""获取LDAP配置"""
|
||||
try:
|
||||
config = self._get_or_create_config()
|
||||
return APIResponse.success('获取LDAP配置成功', self._serialize_config(config))
|
||||
except Exception as e:
|
||||
logger.error(f'获取LDAP配置失败: {str(e)}', exc_info=True)
|
||||
return APIResponse.error(500, f'服务器错误: {str(e)}')
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def put(self, request):
|
||||
"""更新LDAP配置"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
data = json.loads(request.body)
|
||||
config = self._get_or_create_config()
|
||||
self._update_config(config, data)
|
||||
config.save()
|
||||
return APIResponse.success('更新LDAP配置成功')
|
||||
except Exception as e:
|
||||
logger.error(f'更新LDAP配置失败: {str(e)}', exc_info=True)
|
||||
return APIResponse.error(500, f'服务器错误: {str(e)}')
|
||||
|
||||
def _get_or_create_config(self):
|
||||
"""获取或创建LDAP配置"""
|
||||
config, created = LDAPConfig.objects.get_or_create(
|
||||
defaults={
|
||||
'enabled': False,
|
||||
'server_host': '',
|
||||
'server_port': 389,
|
||||
'use_ssl': False,
|
||||
'base_dn': '',
|
||||
'bind_dn': '',
|
||||
'bind_password': '',
|
||||
'user_search_filter': '(cn={username})',
|
||||
'user_attr_map': {'username': 'cn', 'name': 'uid', 'email': 'mail'},
|
||||
'timeout': 10
|
||||
}
|
||||
)
|
||||
return config
|
||||
|
||||
def _serialize_config(self, config):
|
||||
"""序列化配置,隐藏敏感信息"""
|
||||
return {
|
||||
'enabled': config.enabled,
|
||||
'server_host': config.server_host,
|
||||
'server_port': config.server_port,
|
||||
'use_ssl': config.use_ssl,
|
||||
'base_dn': config.base_dn,
|
||||
'bind_dn': config.bind_dn,
|
||||
'bind_password': '******' if config.bind_password else '', # 隐藏密码或显示为空
|
||||
'user_search_filter': config.user_search_filter,
|
||||
'user_attr_map': config.user_attr_map,
|
||||
'timeout': config.timeout,
|
||||
'update_time': config.update_time.strftime('%Y-%m-%d %H:%M:%S') if config.update_time else None
|
||||
}
|
||||
|
||||
def _update_config(self, config, data):
|
||||
"""更新配置字段"""
|
||||
update_fields = [
|
||||
'enabled', 'server_host', 'server_port', 'use_ssl', 'base_dn',
|
||||
'bind_dn', 'user_search_filter', 'user_attr_map', 'timeout'
|
||||
]
|
||||
|
||||
# 检查是否禁用LDAP认证
|
||||
if 'enabled' in data and not data['enabled']:
|
||||
# 如果禁用LDAP,清除所有配置数据
|
||||
config.enabled = False
|
||||
config.server_host = ''
|
||||
config.server_port = 389
|
||||
config.use_ssl = False
|
||||
config.base_dn = ''
|
||||
config.bind_dn = ''
|
||||
config.bind_password = ''
|
||||
config.user_search_filter = '(cn={username})'
|
||||
config.user_attr_map = {'username': 'cn', 'name': 'uid', 'email': 'mail'}
|
||||
config.timeout = 10
|
||||
else:
|
||||
# 启用状态下正常更新字段
|
||||
for field in update_fields:
|
||||
if field in data:
|
||||
setattr(config, field, data[field])
|
||||
|
||||
if 'bind_password' in data:
|
||||
if data['bind_password']:
|
||||
config.bind_password = CryptoUtils.encrypt_password(data['bind_password'])
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class LDAPTestView(View):
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""测试LDAP管理员账户连接"""
|
||||
try:
|
||||
config = self._get_config()
|
||||
|
||||
if not all([config.bind_dn, config.bind_password]):
|
||||
return APIResponse.error(400, '绑定DN和绑定密码是必需的')
|
||||
|
||||
result = self._test_admin_connection(config)
|
||||
return APIResponse.success('LDAP服务器连接测试成功', result)
|
||||
|
||||
except LDAPError as e:
|
||||
return APIResponse.error(400, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f'LDAP测试失败: {str(e)}', exc_info=True)
|
||||
return APIResponse.error(500, f'服务器错误: {str(e)}')
|
||||
|
||||
def _get_config(self):
|
||||
"""获取LDAP配置"""
|
||||
try:
|
||||
config = LDAPConfig.objects.get()
|
||||
except LDAPConfig.DoesNotExist:
|
||||
raise LDAPError('LDAP配置不存在,请先配置LDAP服务器')
|
||||
|
||||
if not all([config.server_host, config.base_dn, config.bind_dn]):
|
||||
raise LDAPError('LDAP配置不完整,请检查服务器地址、Base DN和绑定DN')
|
||||
|
||||
return config
|
||||
|
||||
def _test_admin_connection(self, config):
|
||||
"""测试LDAP管理员账户连接"""
|
||||
try:
|
||||
server_uri = f"{'ldaps://' if config.use_ssl else 'ldap://'}{config.server_host}:{config.server_port}"
|
||||
server = ldap3.Server(server_uri, use_ssl=config.use_ssl, connect_timeout=config.timeout)
|
||||
|
||||
logger.info(f"LDAP测试 - 服务器: {server_uri}")
|
||||
logger.info(f"LDAP测试 - 绑定DN: {config.bind_dn}")
|
||||
logger.info(f"LDAP测试 - Base DN: {config.base_dn}")
|
||||
|
||||
# 解密绑定密码
|
||||
bind_password = CryptoUtils.decrypt_password(config.bind_password)
|
||||
|
||||
# 使用管理员账户连接
|
||||
connection = ldap3.Connection(
|
||||
server,
|
||||
user=config.bind_dn,
|
||||
password=bind_password,
|
||||
auto_bind=True
|
||||
)
|
||||
|
||||
# 测试基本搜索功能
|
||||
connection.search(
|
||||
search_base=config.base_dn,
|
||||
search_filter='(objectClass=*)',
|
||||
search_scope=ldap3.BASE,
|
||||
attributes=['*']
|
||||
)
|
||||
|
||||
connection.unbind()
|
||||
|
||||
return {
|
||||
'server': server_uri,
|
||||
'bind_dn': config.bind_dn,
|
||||
'base_dn': config.base_dn,
|
||||
'connection_status': '连接成功'
|
||||
}
|
||||
|
||||
except ldap3.core.exceptions.LDAPBindError as e:
|
||||
logger.error(f"LDAP绑定错误: {str(e)}")
|
||||
if 'invalidCredentials' in str(e):
|
||||
raise LDAPError('管理员账户认证失败,请检查绑定DN和密码')
|
||||
elif 'invalidDNSyntax' in str(e):
|
||||
raise LDAPError('绑定DN格式错误,请检查DN语法是否正确')
|
||||
else:
|
||||
raise LDAPError(f'绑定失败: {str(e)}')
|
||||
except ldap3.core.exceptions.LDAPSocketOpenError as e:
|
||||
logger.error(f"LDAP连接错误: {str(e)}")
|
||||
raise LDAPError('LDAP服务器连接失败,请检查服务器地址和端口')
|
||||
except ldap3.core.exceptions.LDAPSocketReceiveError as e:
|
||||
logger.error(f"LDAP超时错误: {str(e)}")
|
||||
raise LDAPError('LDAP服务器连接超时,请检查网络连接')
|
||||
except ldap3.core.exceptions.LDAPException as e:
|
||||
logger.error(f"LDAP通用错误: {str(e)}")
|
||||
raise LDAPError(f'LDAP错误: {str(e)}')
|
||||
except Exception as e:
|
||||
logger.error(f"LDAP测试未知错误: {str(e)}", exc_info=True)
|
||||
raise LDAPError(f'连接失败: {str(e)}')
|
||||
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class LDAPSyncView(View):
|
||||
"""LDAP用户同步视图"""
|
||||
|
||||
@method_decorator(jwt_auth_required)
|
||||
def post(self, request):
|
||||
"""同步LDAP用户到系统"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
action = data.get('action')
|
||||
|
||||
config = self._get_config()
|
||||
|
||||
if action == 'search':
|
||||
# 搜索LDAP用户
|
||||
search_filter = data.get('search_filter', '')
|
||||
users = self._search_ldap_users(config, search_filter)
|
||||
return APIResponse.success('搜索LDAP用户成功', {'users': users})
|
||||
|
||||
elif action == 'sync':
|
||||
# 同步用户到系统
|
||||
users_data = data.get('users', [])
|
||||
if not users_data:
|
||||
return APIResponse.error(400, '请选择要同步的用户')
|
||||
|
||||
synced_users = self._sync_users_to_system(config, users_data)
|
||||
return APIResponse.success(
|
||||
f'成功同步{len(synced_users)}个用户',
|
||||
{'synced_users': synced_users}
|
||||
)
|
||||
|
||||
else:
|
||||
return APIResponse.error(400, '无效的操作类型')
|
||||
|
||||
except LDAPError as e:
|
||||
return APIResponse.error(400, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f'LDAP用户同步失败: {str(e)}', exc_info=True)
|
||||
return APIResponse.error(500, f'服务器错误: {str(e)}')
|
||||
|
||||
def _get_config(self):
|
||||
"""获取LDAP配置"""
|
||||
try:
|
||||
config = LDAPConfig.objects.get()
|
||||
if not config.enabled:
|
||||
raise LDAPError('LDAP未启用')
|
||||
except LDAPConfig.DoesNotExist:
|
||||
raise LDAPError('LDAP配置不存在')
|
||||
|
||||
if not all([config.server_host, config.base_dn, config.bind_dn, config.bind_password]):
|
||||
raise LDAPError('LDAP配置不完整')
|
||||
|
||||
return config
|
||||
|
||||
def _search_ldap_users(self, config, search_filter=''):
|
||||
"""搜索LDAP用户"""
|
||||
try:
|
||||
server_uri = f"{'ldaps://' if config.use_ssl else 'ldap://'}{config.server_host}:{config.server_port}"
|
||||
server = ldap3.Server(server_uri, use_ssl=config.use_ssl, connect_timeout=config.timeout)
|
||||
|
||||
# 解密绑定密码
|
||||
bind_password = CryptoUtils.decrypt_password(config.bind_password)
|
||||
|
||||
# 使用管理员账户连接
|
||||
connection = ldap3.Connection(
|
||||
server,
|
||||
user=config.bind_dn,
|
||||
password=bind_password,
|
||||
auto_bind=True
|
||||
)
|
||||
|
||||
# 构建搜索过滤器
|
||||
if search_filter:
|
||||
final_filter = f"(&(objectClass=person)({search_filter}))"
|
||||
else:
|
||||
final_filter = "(objectClass=person)"
|
||||
|
||||
# 搜索用户
|
||||
connection.search(
|
||||
search_base=config.base_dn,
|
||||
search_filter=final_filter,
|
||||
attributes=list(config.user_attr_map.values())
|
||||
)
|
||||
|
||||
users = []
|
||||
for entry in connection.entries:
|
||||
user_info = self._extract_user_info(entry, config.user_attr_map)
|
||||
if user_info.get('username'): # 确保有用户名
|
||||
users.append(user_info)
|
||||
|
||||
connection.unbind()
|
||||
return users
|
||||
|
||||
except ldap3.core.exceptions.LDAPException as e:
|
||||
logger.error(f"LDAP搜索用户错误: {str(e)}")
|
||||
raise LDAPError(f'搜索用户失败: {str(e)}')
|
||||
except Exception as e:
|
||||
logger.error(f"搜索LDAP用户未知错误: {str(e)}", exc_info=True)
|
||||
raise LDAPError(f'搜索失败: {str(e)}')
|
||||
|
||||
def _extract_user_info(self, entry, attr_map):
|
||||
"""提取用户信息"""
|
||||
user_info = {'dn': entry.entry_dn}
|
||||
for local_attr, ldap_attr in attr_map.items():
|
||||
if hasattr(entry, ldap_attr):
|
||||
attr_value = getattr(entry, ldap_attr)
|
||||
if attr_value:
|
||||
user_info[local_attr] = str(attr_value.value) if hasattr(attr_value, 'value') else str(attr_value)
|
||||
return user_info
|
||||
|
||||
def _sync_users_to_system(self, config, users_data):
|
||||
"""同步用户到系统"""
|
||||
synced_users = []
|
||||
|
||||
for user_data in users_data:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
username = user_data.get('username')
|
||||
if not username:
|
||||
continue
|
||||
|
||||
# 检查用户是否已存在
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
if user.user_type != 'ldap':
|
||||
logger.warning(f'用户{username}已存在但不是LDAP用户,跳过')
|
||||
continue
|
||||
|
||||
# 更新用户信息
|
||||
user.name = user_data.get('name', username)
|
||||
user.email = user_data.get('email')
|
||||
user.ldap_dn = user_data.get('dn')
|
||||
user.save()
|
||||
|
||||
synced_users.append({
|
||||
'username': username,
|
||||
'action': 'updated'
|
||||
})
|
||||
|
||||
except User.DoesNotExist:
|
||||
# 创建新用户
|
||||
user = User.objects.create(
|
||||
user_id=generate_id(),
|
||||
username=username,
|
||||
name=user_data.get('name', username),
|
||||
email=user_data.get('email'),
|
||||
user_type='ldap',
|
||||
ldap_dn=user_data.get('dn'),
|
||||
status=1
|
||||
)
|
||||
|
||||
synced_users.append({
|
||||
'username': username,
|
||||
'action': 'created'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'同步用户{user_data.get("username")}失败: {str(e)}')
|
||||
continue
|
||||
|
||||
return synced_users
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class LDAPStatusView(View):
|
||||
def get(self, request):
|
||||
"""检查LDAP是否启用(无需认证)"""
|
||||
try:
|
||||
try:
|
||||
config = LDAPConfig.objects.get()
|
||||
enabled = config.enabled
|
||||
except LDAPConfig.DoesNotExist:
|
||||
enabled = False
|
||||
|
||||
return APIResponse.success('获取LDAP状态成功', {'enabled': enabled})
|
||||
except Exception as e:
|
||||
logger.error(f'获取LDAP状态失败: {str(e)}', exc_info=True)
|
||||
return APIResponse.error(500, f'服务器错误: {str(e)}')
|
||||
|
||||
class LDAPAuthenticator:
|
||||
"""LDAP认证器"""
|
||||
|
||||
@staticmethod
|
||||
def authenticate(username, password):
|
||||
"""LDAP用户认证 - 只认证已存在的用户"""
|
||||
try:
|
||||
config = LDAPAuthenticator._validate_config()
|
||||
# 先检查用户是否存在于本地数据库
|
||||
user = LDAPAuthenticator._get_existing_user(username)
|
||||
# 进行LDAP认证
|
||||
LDAPAuthenticator._ldap_authenticate(config, username, password)
|
||||
return True, user
|
||||
except LDAPError as e:
|
||||
return False, str(e)
|
||||
except Exception as e:
|
||||
logger.error(f'LDAP认证失败: {str(e)}', exc_info=True)
|
||||
return False, f'认证失败: {str(e)}'
|
||||
|
||||
@staticmethod
|
||||
def _validate_config():
|
||||
"""验证LDAP配置"""
|
||||
try:
|
||||
config = LDAPConfig.objects.get()
|
||||
if not config.enabled:
|
||||
raise LDAPError('未启用LDAP认证')
|
||||
except LDAPConfig.DoesNotExist:
|
||||
raise LDAPError('LDAP配置不存在')
|
||||
|
||||
if not all([config.server_host, config.base_dn, config.bind_dn, config.bind_password]):
|
||||
raise LDAPError('LDAP配置不完整,缺少必要的服务器信息或管理员账户')
|
||||
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def _ldap_authenticate(config, username, password):
|
||||
"""执行LDAP认证 - 只使用绑定DN模式"""
|
||||
try:
|
||||
server_uri = f"{'ldaps://' if config.use_ssl else 'ldap://'}{config.server_host}:{config.server_port}"
|
||||
server = ldap3.Server(server_uri, use_ssl=config.use_ssl, connect_timeout=config.timeout)
|
||||
|
||||
# 解密绑定密码
|
||||
bind_password = CryptoUtils.decrypt_password(config.bind_password)
|
||||
|
||||
# 先用管理员账户连接并搜索用户
|
||||
admin_conn = ldap3.Connection(server, user=config.bind_dn, password=bind_password, auto_bind=True)
|
||||
|
||||
# 搜索用户 - 支持两种过滤器格式
|
||||
search_filter = config.user_search_filter
|
||||
if '{username}' in search_filter:
|
||||
search_filter = search_filter.format(username=username)
|
||||
elif '%(user)s' in search_filter:
|
||||
search_filter = search_filter.replace('%(user)s', username)
|
||||
else:
|
||||
# 如果都不匹配,尝试直接替换
|
||||
search_filter = search_filter.format(username=username)
|
||||
admin_conn.search(
|
||||
search_base=config.base_dn,
|
||||
search_filter=search_filter,
|
||||
attributes=list(config.user_attr_map.values())
|
||||
)
|
||||
|
||||
if not admin_conn.entries:
|
||||
admin_conn.unbind()
|
||||
raise LDAPError('用户不存在于LDAP服务器')
|
||||
|
||||
# 获取搜索到的用户DN
|
||||
found_user_dn = admin_conn.entries[0].entry_dn
|
||||
admin_conn.unbind()
|
||||
|
||||
# DN和用户密码进行认证
|
||||
user_conn = ldap3.Connection(server, user=found_user_dn, password=password, auto_bind=True)
|
||||
user_conn.unbind()
|
||||
|
||||
except ldap3.core.exceptions.LDAPBindError as e:
|
||||
if 'invalidCredentials' in str(e):
|
||||
raise LDAPError('用户名或密码错误')
|
||||
else:
|
||||
raise LDAPError('LDAP认证失败')
|
||||
except ldap3.core.exceptions.LDAPSocketOpenError:
|
||||
raise LDAPError('LDAP服务器连接失败')
|
||||
except ldap3.core.exceptions.LDAPSocketReceiveError:
|
||||
raise LDAPError('LDAP服务器连接超时')
|
||||
except ldap3.core.exceptions.LDAPException as e:
|
||||
raise LDAPError(f'LDAP错误: {str(e)}')
|
||||
|
||||
@staticmethod
|
||||
def _get_existing_user(username):
|
||||
"""获取已存在的LDAP用户"""
|
||||
try:
|
||||
return User.objects.get(username=username, user_type='ldap')
|
||||
except User.DoesNotExist:
|
||||
raise LDAPError(f'用户{username}不存在,请联系管理员同步LDAP用户')
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.views.decorators.http import require_http_methods
|
||||
from django.conf import settings
|
||||
from ..models import User, UserToken, LoginLog
|
||||
from ..utils.security import SecurityValidator
|
||||
from .ldap import LDAPAuthenticator
|
||||
|
||||
def generate_token_id():
|
||||
"""生成token_id"""
|
||||
@@ -18,6 +19,135 @@ def generate_log_id():
|
||||
"""生成log_id"""
|
||||
return hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()[:32]
|
||||
|
||||
class LoginHandler:
|
||||
"""登录处理器"""
|
||||
|
||||
def __init__(self, username, password, ip_address, user_agent):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.ip_address = ip_address
|
||||
self.user_agent = user_agent
|
||||
self.log_id = generate_log_id()
|
||||
|
||||
def authenticate_system_user(self):
|
||||
"""系统用户认证"""
|
||||
try:
|
||||
user = User.objects.get(username=self.username)
|
||||
except User.DoesNotExist:
|
||||
self._log_failed_login(fail_reason='用户不存在')
|
||||
return JsonResponse({'code': 404, 'message': '用户不存在'})
|
||||
|
||||
# 检查账户锁定
|
||||
is_not_locked, lockout_message = SecurityValidator.check_account_lockout(user, self.ip_address)
|
||||
if not is_not_locked:
|
||||
self._log_failed_login(user=user, fail_reason=lockout_message)
|
||||
return JsonResponse({'code': 423, 'message': lockout_message})
|
||||
|
||||
# 验证密码
|
||||
password_hash = hashlib.sha256(self.password.encode()).hexdigest()
|
||||
if user.password != password_hash:
|
||||
return self._handle_password_error(user)
|
||||
|
||||
# 检查用户状态
|
||||
if user.status == 0:
|
||||
self._log_failed_login(user=user, fail_reason='账号已被锁定')
|
||||
return JsonResponse({'code': 423, 'message': '账号已被锁定,请联系管理员解锁'})
|
||||
|
||||
# 登录成功
|
||||
SecurityValidator.record_successful_login(user, self.ip_address)
|
||||
return self._create_login_response(user)
|
||||
|
||||
def authenticate_ldap_user(self):
|
||||
"""LDAP用户认证"""
|
||||
success, result = LDAPAuthenticator.authenticate(self.username, self.password)
|
||||
|
||||
if not success:
|
||||
self._log_failed_login(fail_reason=f'LDAP认证失败: {result}')
|
||||
return JsonResponse({'code': 401, 'message': result})
|
||||
|
||||
user = result
|
||||
if user.status == 0:
|
||||
self._log_failed_login(user=user, fail_reason='账号已被锁定')
|
||||
return JsonResponse({'code': 423, 'message': '账号已被锁定,请联系管理员解锁'})
|
||||
|
||||
return self._create_login_response(user)
|
||||
|
||||
def _handle_password_error(self, user):
|
||||
"""处理密码错误"""
|
||||
failed_attempts, max_attempts = SecurityValidator.record_failed_login(user, self.ip_address)
|
||||
self._log_failed_login(user=user, fail_reason='密码错误')
|
||||
|
||||
remaining_attempts = max_attempts - failed_attempts
|
||||
if remaining_attempts > 0:
|
||||
message = f'密码错误,还可尝试{remaining_attempts}次'
|
||||
else:
|
||||
config = SecurityValidator.get_security_config()
|
||||
message = f'密码错误次数过多,账户已被锁定{config.lockout_duration}分钟'
|
||||
|
||||
return JsonResponse({'code': 401, 'message': message})
|
||||
|
||||
def _create_login_response(self, user):
|
||||
"""创建登录成功响应"""
|
||||
config = SecurityValidator.get_security_config()
|
||||
|
||||
# 生成token
|
||||
token_id = generate_token_id()
|
||||
token_payload = {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'exp': datetime.utcnow() + timedelta(minutes=config.session_timeout)
|
||||
}
|
||||
token = jwt.encode(token_payload, settings.SECRET_KEY, algorithm='HS256')
|
||||
|
||||
# 更新token记录
|
||||
UserToken.objects.update_or_create(
|
||||
user=user,
|
||||
defaults={'token_id': token_id, 'token': token}
|
||||
)
|
||||
|
||||
# 更新登录时间
|
||||
user.login_time = datetime.now()
|
||||
user.save()
|
||||
|
||||
# 记录成功登录日志
|
||||
self._log_successful_login(user)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '登录成功',
|
||||
'data': {
|
||||
'token_id': token_id,
|
||||
'token': token,
|
||||
'user': {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'name': user.name,
|
||||
'email': user.email
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def _log_failed_login(self, user=None, fail_reason=''):
|
||||
"""记录失败登录日志"""
|
||||
LoginLog.objects.create(
|
||||
log_id=self.log_id,
|
||||
user=user,
|
||||
ip_address=self.ip_address,
|
||||
user_agent=self.user_agent,
|
||||
status='failed',
|
||||
fail_reason=fail_reason
|
||||
)
|
||||
|
||||
def _log_successful_login(self, user):
|
||||
"""记录成功登录日志"""
|
||||
LoginLog.objects.create(
|
||||
log_id=self.log_id,
|
||||
user=user,
|
||||
ip_address=self.ip_address,
|
||||
user_agent=self.user_agent,
|
||||
status='success'
|
||||
)
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def login(request):
|
||||
@@ -25,160 +155,26 @@ def login(request):
|
||||
data = json.loads(request.body)
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
auth_type = data.get('auth_type', 'system')
|
||||
|
||||
# 获取客户端IP和用户代理
|
||||
if not username or not password:
|
||||
return JsonResponse({'code': 400, 'message': '用户名和密码不能为空'})
|
||||
|
||||
# 获取客户端信息
|
||||
ip_address = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', ''))
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not username or not password:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '用户名和密码不能为空'
|
||||
})
|
||||
|
||||
# 密码加密
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
# 记录登录日志
|
||||
log_id = generate_log_id()
|
||||
|
||||
# 检查账户是否被锁定
|
||||
is_not_locked, lockout_message = SecurityValidator.check_account_lockout(user, ip_address)
|
||||
if not is_not_locked:
|
||||
# 账户被锁定,记录失败登录
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason=lockout_message
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 423, # 423 Locked
|
||||
'message': lockout_message
|
||||
})
|
||||
|
||||
if user.password != password_hash:
|
||||
# 密码错误,记录失败尝试
|
||||
failed_attempts, max_attempts = SecurityValidator.record_failed_login(user, ip_address)
|
||||
|
||||
# 记录失败登录日志
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason='密码错误'
|
||||
)
|
||||
|
||||
# 提供剩余尝试次数信息
|
||||
remaining_attempts = max_attempts - failed_attempts
|
||||
if remaining_attempts > 0:
|
||||
message = f'密码错误,还可尝试{remaining_attempts}次'
|
||||
else:
|
||||
config = SecurityValidator.get_security_config()
|
||||
message = f'密码错误次数过多,账户已被锁定{config.lockout_duration}分钟'
|
||||
|
||||
return JsonResponse({
|
||||
'code': 401,
|
||||
'message': message
|
||||
})
|
||||
|
||||
if user.status == 0:
|
||||
# 账号锁定,记录失败登录
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason='账号已被锁定'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 423, # 423 Locked
|
||||
'message': '账号已被锁定,请联系管理员解锁'
|
||||
})
|
||||
|
||||
# 登录成功,重置失败尝试次数
|
||||
SecurityValidator.record_successful_login(user, ip_address)
|
||||
|
||||
# 检查会话超时设置
|
||||
config = SecurityValidator.get_security_config()
|
||||
|
||||
# 生成token_id和JWT token
|
||||
token_id = generate_token_id()
|
||||
token_payload = {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'exp': datetime.utcnow() + timedelta(minutes=config.session_timeout) # 使用配置的会话超时时间
|
||||
}
|
||||
token = jwt.encode(token_payload, settings.SECRET_KEY, algorithm='HS256')
|
||||
|
||||
# 更新或创建token记录
|
||||
UserToken.objects.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
'token_id': token_id,
|
||||
'token': token
|
||||
}
|
||||
)
|
||||
|
||||
# 更新登录时间
|
||||
user.login_time = datetime.now()
|
||||
user.save()
|
||||
|
||||
# 记录成功登录日志
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='success'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '登录成功',
|
||||
'data': {
|
||||
'token_id': token_id,
|
||||
'token': token,
|
||||
'user': {
|
||||
'user_id': user.user_id,
|
||||
'username': user.username,
|
||||
'name': user.name,
|
||||
'email': user.email
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except User.DoesNotExist:
|
||||
# 用户不存在,记录失败登录
|
||||
log_id = generate_log_id()
|
||||
LoginLog.objects.create(
|
||||
log_id=log_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status='failed',
|
||||
fail_reason='用户不存在'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '用户不存在'
|
||||
})
|
||||
|
||||
# 创建登录处理器
|
||||
handler = LoginHandler(username, password, ip_address, user_agent)
|
||||
|
||||
# 根据认证类型进行认证
|
||||
if auth_type == 'ldap':
|
||||
return handler.authenticate_ldap_user()
|
||||
else:
|
||||
return handler.authenticate_system_user()
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
return JsonResponse({'code': 500, 'message': f'服务器错误: {str(e)}'})
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
@@ -186,36 +182,18 @@ def logout(request):
|
||||
try:
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '未提供Token'
|
||||
})
|
||||
return JsonResponse({'code': 400, 'message': '未提供Token'})
|
||||
|
||||
try:
|
||||
# 解析token获取user_id
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
user_id = payload.get('user_id')
|
||||
|
||||
# 删除该用户的token记录
|
||||
UserToken.objects.filter(user_id=user_id).delete()
|
||||
return JsonResponse({'code': 200, 'message': '退出成功'})
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '退出成功'
|
||||
})
|
||||
except jwt.ExpiredSignatureError:
|
||||
return JsonResponse({
|
||||
'code': 401,
|
||||
'message': 'Token已过期'
|
||||
})
|
||||
return JsonResponse({'code': 401, 'message': 'Token已过期'})
|
||||
except jwt.InvalidTokenError:
|
||||
return JsonResponse({
|
||||
'code': 401,
|
||||
'message': '无效的Token'
|
||||
})
|
||||
return JsonResponse({'code': 401, 'message': '无效的Token'})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
return JsonResponse({'code': 500, 'message': f'服务器错误: {str(e)}'})
|
||||
@@ -57,6 +57,7 @@ class UserView(View):
|
||||
'email': user.email,
|
||||
'status': user.status,
|
||||
'roles': roles,
|
||||
'user_type': user.user_type,
|
||||
'login_time': user.login_time.strftime('%Y-%m-%d %H:%M:%S') if user.login_time else None,
|
||||
'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
})
|
||||
@@ -188,7 +189,6 @@ class UserView(View):
|
||||
})
|
||||
user.email = email
|
||||
if status is not None:
|
||||
# 如果是解锁操作(从0改为1),使用安全验证器的解锁方法
|
||||
if user.status == 0 and status == 1:
|
||||
success, message = SecurityValidator.unlock_user_account(user)
|
||||
if not success:
|
||||
@@ -196,7 +196,6 @@ class UserView(View):
|
||||
'code': 400,
|
||||
'message': message
|
||||
})
|
||||
# 如果是锁定操作(从1改为0),使用安全验证器的锁定方法
|
||||
elif user.status == 1 and status == 0:
|
||||
success, message = SecurityValidator.lock_user_account(user)
|
||||
if not success:
|
||||
@@ -315,6 +314,7 @@ class UserProfileView(View):
|
||||
'email': user.email,
|
||||
'status': user.status,
|
||||
'roles': roles,
|
||||
'user_type': user.user_type,
|
||||
'login_time': user.login_time.strftime('%Y-%m-%d %H:%M:%S') if user.login_time else None,
|
||||
'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S') if user.create_time else None,
|
||||
'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S') if user.update_time else None,
|
||||
|
||||
@@ -16,6 +16,7 @@ from apps.views.dashboard import DashboardStatsView, BuildTrendView, BuildDetail
|
||||
from apps.views.webhook import GitLabWebhookView
|
||||
|
||||
from apps.views.security import SecurityConfigView, get_build_tasks_for_cleanup, cleanup_build_logs, cleanup_login_logs, get_watermark_config, get_current_user_info
|
||||
from apps.views.ldap import LDAPConfigView, LDAPTestView, LDAPStatusView, LDAPSyncView
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
@@ -73,4 +74,10 @@ urlpatterns = [
|
||||
path('api/system/security/cleanup-login-logs/', cleanup_login_logs, name='cleanup-login-logs'),
|
||||
path('api/system/watermark/', get_watermark_config, name='watermark-config'),
|
||||
path('api/user/current/', get_current_user_info, name='current-user-info'),
|
||||
|
||||
# LDAP配置相关路由
|
||||
path('api/system/ldap/', LDAPConfigView.as_view(), name='ldap-config'),
|
||||
path('api/system/ldap/test/', LDAPTestView.as_view(), name='ldap-test'),
|
||||
path('api/system/ldap/status/', LDAPStatusView.as_view(), name='ldap-status'),
|
||||
path('api/system/ldap/sync/', LDAPSyncView.as_view(), name='ldap-sync'),
|
||||
]
|
||||
|
||||
@@ -9,4 +9,5 @@ uvicorn==0.34.0
|
||||
decorator==5.1.1
|
||||
django-cors-headers==4.2.0
|
||||
cryptography==42.0.5
|
||||
PyYAML==6.0.1
|
||||
PyYAML==6.0.1
|
||||
ldap3==2.9.1
|
||||
BIN
image/basic_ldap.png
Normal file
BIN
image/basic_ldap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 850 KiB |
BIN
image/notify.png
Normal file
BIN
image/notify.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
@@ -38,6 +38,22 @@
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<!-- LDAP认证选项 -->
|
||||
<a-form-item v-if="ldapEnabled">
|
||||
<div class="auth-type-selection">
|
||||
<a-switch
|
||||
v-model:checked="useLDAP"
|
||||
checked-children="LDAP"
|
||||
un-checked-children="系统"
|
||||
@change="handleAuthTypeChange"
|
||||
/>
|
||||
<span class="auth-type-label">
|
||||
{{ useLDAP ? 'LDAP认证' : '系统认证' }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
@@ -53,14 +69,14 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="footer-text">
|
||||
<p>© 2023 LiteOps 胡图图不涂涂</p>
|
||||
<p>© 2024 LiteOps 胡图图不涂涂</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -69,6 +85,8 @@ import { initUserPermissions } from '../../utils/permission';
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const useLDAP = ref(false);
|
||||
const ldapEnabled = ref(false);
|
||||
|
||||
const formState = reactive({
|
||||
username: '',
|
||||
@@ -89,12 +107,30 @@ const rules = {
|
||||
],
|
||||
};
|
||||
|
||||
const handleAuthTypeChange = (checked) => {
|
||||
console.log('认证类型变更:', checked ? 'LDAP' : '系统');
|
||||
};
|
||||
|
||||
// 检查LDAP是否启用
|
||||
const checkLdapStatus = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/system/ldap/status/');
|
||||
if (response.data.code === 200) {
|
||||
ldapEnabled.value = response.data.data.enabled;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查LDAP状态失败:', error);
|
||||
ldapEnabled.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await axios.post('/api/login/', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
auth_type: useLDAP.value ? 'ldap' : 'system'
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
@@ -117,6 +153,11 @@ const handleSubmit = async (values) => {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时检查LDAP状态
|
||||
onMounted(() => {
|
||||
checkLdapStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -155,6 +196,24 @@ const handleSubmit = async (values) => {
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
.auth-type-selection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.auth-type-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-switch-checked) {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
height: 55px;
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -94,7 +94,6 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<a-divider>水印配置</a-divider>
|
||||
|
||||
<!-- 水印功能 -->
|
||||
@@ -159,7 +158,6 @@
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<a-divider>日志清理</a-divider>
|
||||
|
||||
<!-- 日志清理功能 -->
|
||||
@@ -294,6 +292,277 @@
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 认证配置 -->
|
||||
<a-tab-pane key="auth-config" tab="认证">
|
||||
<a-card>
|
||||
<a-form
|
||||
ref="ldapFormRef"
|
||||
:model="ldapConfig"
|
||||
:rules="ldapRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item>
|
||||
<a-switch
|
||||
v-model:checked="ldapConfig.enabled"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
@change="handleLdapEnabledChange"
|
||||
/>
|
||||
<span style="margin-left: 12px; font-weight: 500;">启用LDAP认证</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<template v-if="ldapConfig.enabled">
|
||||
<a-divider>服务器配置</a-divider>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="服务器地址" name="server_host">
|
||||
<a-input
|
||||
v-model:value="ldapConfig.server_host"
|
||||
placeholder="例如: ldap.example.com"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="端口" name="server_port">
|
||||
<a-input-number
|
||||
v-model:value="ldapConfig.server_port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="使用SSL">
|
||||
<a-switch v-model:checked="ldapConfig.use_ssl" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="Base DN" name="base_dn">
|
||||
<a-input
|
||||
v-model:value="ldapConfig.base_dn"
|
||||
placeholder="例如: dc=example,dc=com"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
LDAP搜索的起始点,通常是你的域名,如: dc=company,dc=com
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
|
||||
|
||||
<a-divider>高级配置</a-divider>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="绑定DN" name="bind_dn">
|
||||
<a-input
|
||||
v-model:value="ldapConfig.bind_dn"
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
管理员账户DN,用于连接LDAP服务器搜索和认证用户
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="绑定密码" name="bind_password">
|
||||
<a-input-password
|
||||
v-model:value="ldapConfig.bind_password"
|
||||
placeholder="输入新密码或留空保持原密码"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
管理员账户密码
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="18">
|
||||
<a-form-item label="用户搜索过滤器">
|
||||
<a-input
|
||||
v-model:value="ldapConfig.user_search_filter"
|
||||
placeholder="例如: (uid={username})"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
用于搜索用户的LDAP过滤器,{username} 将被替换为实际用户名
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="连接超时(秒)">
|
||||
<a-input-number
|
||||
v-model:value="ldapConfig.timeout"
|
||||
:min="1"
|
||||
:max="60"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="属性映射配置" name="user_attr_map">
|
||||
<a-textarea
|
||||
v-model:value="userAttrMapJson"
|
||||
placeholder='示例:
|
||||
{
|
||||
"username": "cn",
|
||||
"name": "uid",
|
||||
"email": "mail"
|
||||
}'
|
||||
:rows="6"
|
||||
@blur="handleAttrMapChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider>用户同步</a-divider>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="16">
|
||||
<a-form-item label="搜索条件">
|
||||
<a-input
|
||||
v-model:value="ldapSyncForm.searchFilter"
|
||||
placeholder="例如: uid=user* 或 mail=搜索email"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
可以指定搜索条件来过滤LDAP用户,留空则搜索所有用户
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="操作">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleSearchLdapUsers"
|
||||
:loading="ldapSyncLoading"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
搜索用户
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
@click="handleSyncSelectedUsers"
|
||||
:loading="ldapSyncLoading"
|
||||
:disabled="selectedUsers.length === 0"
|
||||
>
|
||||
同步选中
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<div v-if="ldapUsers.length > 0">
|
||||
<h4>搜索结果:</h4>
|
||||
<a-table
|
||||
:columns="ldapUserColumns"
|
||||
:data-source="ldapUsers"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:row-selection="{ selectedRowKeys: selectedUsers, onChange: onSelectUsers }"
|
||||
row-key="username"
|
||||
size="small"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'exists'">
|
||||
<a-tag :color="record.exists ? 'rgba(56, 158, 13, 0.8)' : 'rgba(22,119,255,0.8)'">
|
||||
{{ record.exists ? '已存在' : '新用户' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 同步结果 -->
|
||||
<div v-if="syncResult">
|
||||
<a-alert
|
||||
:message="syncResult.success ? '用户同步成功' : '用户同步失败'"
|
||||
:description="syncResult.message"
|
||||
:type="syncResult.success ? 'success' : 'error'"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<div v-if="syncResult.success && syncResult.synced_users">
|
||||
<h4>同步详情:</h4>
|
||||
<ul>
|
||||
<li v-for="user in syncResult.synced_users" :key="user.username">
|
||||
<strong>{{ user.username }}</strong> - {{ user.action === 'created' ? '新建' : '更新' }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-row v-if="ldapTestResult" :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-alert
|
||||
:message="ldapTestResult.success ? 'LDAP连接测试成功' : 'LDAP连接测试失败'"
|
||||
:description="ldapTestResult.message"
|
||||
:type="ldapTestResult.success ? 'success' : 'error'"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<div v-if="ldapTestResult.success && ldapTestResult.connection_info">
|
||||
<h4>连接信息:</h4>
|
||||
<a-descriptions size="small" bordered>
|
||||
<a-descriptions-item label="服务器">
|
||||
{{ ldapTestResult.connection_info.server }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="绑定DN">
|
||||
{{ ldapTestResult.connection_info.bind_dn }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Base DN">
|
||||
{{ ldapTestResult.connection_info.base_dn }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="连接状态">
|
||||
{{ ldapTestResult.connection_info.connection_status }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="saveLdapConfig"
|
||||
:loading="ldapConfigLoading"
|
||||
v-if="hasFunctionPermission('system_basic', 'edit')"
|
||||
>
|
||||
保存配置
|
||||
</a-button>
|
||||
<a-button
|
||||
@click="handleLdapTest"
|
||||
:loading="ldapTestLoading"
|
||||
:disabled="!canTestConnection"
|
||||
v-if="hasFunctionPermission('system_basic', 'edit')"
|
||||
>
|
||||
测试连接
|
||||
</a-button>
|
||||
</a-space>
|
||||
<div v-if="!canTestConnection" class="form-item-help" style="margin-top: 8px;">
|
||||
请先启用LDAP认证并完成所有必要配置,点击保存配置后再测试连接
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 添加/编辑机器人抽屉 -->
|
||||
@@ -440,7 +709,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import axios from 'axios';
|
||||
@@ -448,6 +717,127 @@ import { hasFunctionPermission, checkPermission } from '../../utils/permission';
|
||||
|
||||
const activeTabKey = ref('security-config');
|
||||
|
||||
// LDAP配置
|
||||
const ldapFormRef = ref();
|
||||
const ldapConfigLoading = ref(false);
|
||||
const ldapTestLoading = ref(false);
|
||||
const ldapTestResult = ref(null);
|
||||
|
||||
// 是否可以测试连接
|
||||
const canTestConnection = computed(() => {
|
||||
// 是否启用且所有必要配置都已填写
|
||||
const hasPassword = ldapConfig.bind_password &&
|
||||
ldapConfig.bind_password.trim() &&
|
||||
ldapConfig.bind_password !== '';
|
||||
|
||||
return ldapConfig.enabled &&
|
||||
hasPassword &&
|
||||
ldapConfig.server_host &&
|
||||
ldapConfig.server_host.trim() &&
|
||||
ldapConfig.base_dn &&
|
||||
ldapConfig.base_dn.trim() &&
|
||||
ldapConfig.bind_dn &&
|
||||
ldapConfig.bind_dn.trim();
|
||||
});
|
||||
|
||||
const ldapConfig = reactive({
|
||||
enabled: false,
|
||||
server_host: '',
|
||||
server_port: 389,
|
||||
use_ssl: false,
|
||||
base_dn: '',
|
||||
bind_dn: '',
|
||||
bind_password: '',
|
||||
user_search_filter: '(cn={username})',
|
||||
user_attr_map: {
|
||||
username: 'cn',
|
||||
name: 'uid',
|
||||
email: 'mail'
|
||||
},
|
||||
timeout: 10
|
||||
});
|
||||
|
||||
// LDAP用户同步相关
|
||||
const ldapSyncForm = reactive({
|
||||
searchFilter: ''
|
||||
});
|
||||
|
||||
const ldapUsers = ref([]);
|
||||
const selectedUsers = ref([]);
|
||||
const ldapSyncLoading = ref(false);
|
||||
const syncResult = ref(null);
|
||||
|
||||
// 用户属性映射JSON字符串
|
||||
const userAttrMapJson = ref('');
|
||||
|
||||
// LDAP用户表格列定义
|
||||
const ldapUserColumns = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'exists',
|
||||
key: 'exists',
|
||||
},
|
||||
];
|
||||
|
||||
// 验证JSON格式的自定义验证器
|
||||
const validateAttrMapJson = (rule, value) => {
|
||||
// 使用userAttrMapJson的值进行验证
|
||||
const jsonValue = userAttrMapJson.value;
|
||||
|
||||
if (!jsonValue || !jsonValue.trim()) {
|
||||
return Promise.reject('请输入属性映射配置');
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonValue);
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
return Promise.reject('属性映射必须是一个JSON对象');
|
||||
}
|
||||
|
||||
// 检查是否包含必要的字段
|
||||
if (!parsed.username) {
|
||||
return Promise.reject('属性映射必须包含 username 字段');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject('JSON格式错误,请检查语法');
|
||||
}
|
||||
};
|
||||
|
||||
const ldapRules = {
|
||||
server_host: [
|
||||
{ required: true, message: '请输入服务器地址', trigger: 'blur' }
|
||||
],
|
||||
base_dn: [
|
||||
{ required: true, message: '请输入Base DN', trigger: 'blur' }
|
||||
],
|
||||
bind_dn: [
|
||||
{ required: true, message: '请输入绑定DN', trigger: 'blur' }
|
||||
],
|
||||
bind_password: [
|
||||
{ required: true, message: '请输入绑定密码', trigger: 'blur' }
|
||||
],
|
||||
user_attr_map: [
|
||||
{ validator: validateAttrMapJson, trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
// 安全配置
|
||||
const securityFormRef = ref();
|
||||
const securityConfig = reactive({
|
||||
@@ -495,7 +885,6 @@ const robotColumns = [
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
// width: 120,
|
||||
},
|
||||
{
|
||||
title: '机器人名称',
|
||||
@@ -512,7 +901,6 @@ const robotColumns = [
|
||||
title: '安全设置',
|
||||
dataIndex: 'security_type',
|
||||
key: 'security_type',
|
||||
// width: 120,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
@@ -524,12 +912,10 @@ const robotColumns = [
|
||||
title: '创建时间',
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time',
|
||||
// width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
// width: 200,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
@@ -548,7 +934,6 @@ const robotForm = reactive({
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 加载状态
|
||||
const securityLoading = ref(false);
|
||||
|
||||
// 标签页切换
|
||||
@@ -557,6 +942,8 @@ const handleTabChange = (key) => {
|
||||
loadRobotList();
|
||||
} else if (key === 'security-config') {
|
||||
loadBuildTasksList();
|
||||
} else if (key === 'auth-config') {
|
||||
fetchLdapConfig();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -879,9 +1266,277 @@ const handleLogCleanup = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// LDAP配置相关方法
|
||||
const fetchLdapConfig = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.get('/api/system/ldap/', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
const configData = response.data.data;
|
||||
Object.assign(ldapConfig, configData);
|
||||
|
||||
if (configData.bind_password === '******') {
|
||||
ldapConfig.bind_password = '******'; // 保持标识,表示后端有密码
|
||||
}
|
||||
|
||||
// 初始化属性映射JSON字符串
|
||||
initUserAttrMapJson();
|
||||
} else {
|
||||
message.error(response.data.message || '获取LDAP配置失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取LDAP配置失败:', error);
|
||||
message.error('获取LDAP配置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const saveLdapConfig = async () => {
|
||||
try {
|
||||
await ldapFormRef.value?.validate();
|
||||
|
||||
// 验证属性映射JSON格式
|
||||
try {
|
||||
if (userAttrMapJson.value.trim()) {
|
||||
const parsedMap = JSON.parse(userAttrMapJson.value);
|
||||
ldapConfig.user_attr_map = parsedMap;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('属性映射JSON格式错误,请检查语法');
|
||||
return;
|
||||
}
|
||||
|
||||
ldapConfigLoading.value = true;
|
||||
|
||||
// 准备发送的数据
|
||||
const configToSave = { ...ldapConfig };
|
||||
|
||||
if (configToSave.bind_password === '******') {
|
||||
delete configToSave.bind_password;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.put('/api/system/ldap/', configToSave, {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
message.success('LDAP配置保存成功');
|
||||
// 重新获取配置保状态同步
|
||||
await fetchLdapConfig();
|
||||
} else {
|
||||
message.error(response.data.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存LDAP配置失败:', error);
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
ldapConfigLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// LDAP启用状态变化
|
||||
const handleLdapEnabledChange = (enabled) => {
|
||||
if (!enabled) {
|
||||
// 禁用LDAP时清除所有配置
|
||||
ldapConfig.server_host = '';
|
||||
ldapConfig.server_port = 389;
|
||||
ldapConfig.use_ssl = false;
|
||||
ldapConfig.base_dn = '';
|
||||
ldapConfig.bind_dn = '';
|
||||
ldapConfig.bind_password = '';
|
||||
ldapConfig.user_search_filter = '(cn={username})';
|
||||
ldapConfig.user_attr_map = { username: 'cn', name: 'uid', email: 'mail' };
|
||||
ldapConfig.timeout = 10;
|
||||
|
||||
// 重置属性映射JSON字符串
|
||||
initUserAttrMapJson();
|
||||
|
||||
// 清除测试结果
|
||||
ldapTestResult.value = null;
|
||||
syncResult.value = null;
|
||||
ldapUsers.value = [];
|
||||
selectedUsers.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 处理属性映射JSON变化
|
||||
const handleAttrMapChange = () => {
|
||||
try {
|
||||
if (userAttrMapJson.value.trim()) {
|
||||
const parsedMap = JSON.parse(userAttrMapJson.value);
|
||||
ldapConfig.user_attr_map = parsedMap;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('JSON格式错误,请检查语法');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化属性映射JSON字符串
|
||||
const initUserAttrMapJson = () => {
|
||||
if (ldapConfig.user_attr_map && Object.keys(ldapConfig.user_attr_map).length > 0) {
|
||||
userAttrMapJson.value = JSON.stringify(ldapConfig.user_attr_map, null, 2);
|
||||
} else {
|
||||
// 提供默认的映射配置
|
||||
const defaultMapping = {
|
||||
username: 'cn',
|
||||
name: 'uid',
|
||||
email: 'mail'
|
||||
};
|
||||
userAttrMapJson.value = JSON.stringify(defaultMapping, null, 2);
|
||||
ldapConfig.user_attr_map = defaultMapping;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLdapTest = async () => {
|
||||
try {
|
||||
ldapTestLoading.value = true;
|
||||
ldapTestResult.value = null;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.post('/api/system/ldap/test/', {}, {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
ldapTestResult.value = {
|
||||
success: true,
|
||||
message: response.data.message,
|
||||
connection_info: response.data.data
|
||||
};
|
||||
} else {
|
||||
ldapTestResult.value = {
|
||||
success: false,
|
||||
message: response.data.message
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LDAP测试失败:', error);
|
||||
ldapTestResult.value = {
|
||||
success: false,
|
||||
message: '测试失败,请稍后重试'
|
||||
};
|
||||
} finally {
|
||||
ldapTestLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索LDAP用户
|
||||
const handleSearchLdapUsers = async () => {
|
||||
try {
|
||||
ldapSyncLoading.value = true;
|
||||
syncResult.value = null;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.post('/api/system/ldap/sync/', {
|
||||
action: 'search',
|
||||
search_filter: ldapSyncForm.searchFilter
|
||||
}, {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
// 检查用户是否已存在
|
||||
const existingUsers = await getExistingUsers();
|
||||
ldapUsers.value = response.data.data.users.map(user => ({
|
||||
...user,
|
||||
exists: existingUsers.includes(user.username)
|
||||
}));
|
||||
selectedUsers.value = [];
|
||||
message.success(`找到${ldapUsers.value.length}个LDAP用户`);
|
||||
} else {
|
||||
message.error(response.data.message || '搜索用户失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索LDAP用户失败:', error);
|
||||
message.error('搜索用户失败');
|
||||
} finally {
|
||||
ldapSyncLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取已存在的用户列表
|
||||
const getExistingUsers = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.get('/api/users/', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
return response.data.data
|
||||
.filter(user => user.user_type === 'ldap')
|
||||
.map(user => user.username);
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取已存在用户失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 选择用户
|
||||
const onSelectUsers = (selectedRowKeys) => {
|
||||
selectedUsers.value = selectedRowKeys;
|
||||
};
|
||||
|
||||
// 同步选中用户
|
||||
const handleSyncSelectedUsers = async () => {
|
||||
if (selectedUsers.value.length === 0) {
|
||||
message.warning('请选择要同步的用户');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ldapSyncLoading.value = true;
|
||||
syncResult.value = null;
|
||||
|
||||
const usersToSync = ldapUsers.value.filter(user =>
|
||||
selectedUsers.value.includes(user.username)
|
||||
);
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.post('/api/system/ldap/sync/', {
|
||||
action: 'sync',
|
||||
users: usersToSync
|
||||
}, {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
|
||||
if (response.data.code === 200) {
|
||||
syncResult.value = {
|
||||
success: true,
|
||||
message: response.data.message,
|
||||
synced_users: response.data.data.synced_users
|
||||
};
|
||||
|
||||
// 刷新用户列表
|
||||
await handleSearchLdapUsers();
|
||||
} else {
|
||||
syncResult.value = {
|
||||
success: false,
|
||||
message: response.data.message
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('同步用户失败:', error);
|
||||
syncResult.value = {
|
||||
success: false,
|
||||
message: '同步用户失败,请稍后重试'
|
||||
};
|
||||
} finally {
|
||||
ldapSyncLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchSecurityConfig();
|
||||
loadBuildTasksList();
|
||||
if (!userAttrMapJson.value) {
|
||||
initUserAttrMapJson();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -942,4 +1597,12 @@ onMounted(() => {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-input) {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
:deep(textarea) {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -40,6 +40,11 @@
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'user_type'">
|
||||
<a-tag :color="record.user_type === 'ldap' ? 'rgba(56, 158, 13, 0.8)' : 'rgba(22,119,255,0.8)'">
|
||||
{{ record.user_type === 'ldap' ? 'LDAP' : '系统' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="showEditModal(record)">编辑</a>
|
||||
@@ -87,12 +92,20 @@
|
||||
<a-input v-model:value="formState.username" :disabled="!!formState.user_id" />
|
||||
</a-form-item>
|
||||
<a-form-item label="姓名" name="name">
|
||||
<a-input v-model:value="formState.name" />
|
||||
<a-input
|
||||
v-model:value="formState.name"
|
||||
:disabled="formState.user_type === 'ldap'"
|
||||
/>
|
||||
<div v-if="formState.user_type === 'ldap'" class="form-help">LDAP用户信息由LDAP服务器管理</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formState.email" />
|
||||
<a-input
|
||||
v-model:value="formState.email"
|
||||
:disabled="formState.user_type === 'ldap'"
|
||||
/>
|
||||
<div v-if="formState.user_type === 'ldap'" class="form-help">LDAP用户信息由LDAP服务器管理</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码" name="password">
|
||||
<a-form-item label="密码" name="password" v-if="formState.user_type !== 'ldap'">
|
||||
<a-input-password v-model:value="formState.password" />
|
||||
<div v-if="!!formState.user_id" class="form-help">不修改请留空</div>
|
||||
</a-form-item>
|
||||
@@ -148,6 +161,11 @@ const columns = [
|
||||
dataIndex: 'roles',
|
||||
key: 'roles',
|
||||
},
|
||||
{
|
||||
title: '用户类型',
|
||||
dataIndex: 'user_type',
|
||||
key: 'user_type',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -254,11 +272,16 @@ const formState = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
role_ids: [],
|
||||
status: 1
|
||||
status: 1,
|
||||
user_type: 'system'
|
||||
});
|
||||
|
||||
// 动态密码验证规则
|
||||
const passwordValidator = (rule, value) => {
|
||||
// LDAP用户不需要验证密码
|
||||
if (formState.user_type === 'ldap') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (formState.user_id && !value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -350,6 +373,7 @@ const showEditModal = (record) => {
|
||||
formState.name = record.name;
|
||||
formState.email = record.email;
|
||||
formState.status = record.status;
|
||||
formState.user_type = record.user_type || 'system';
|
||||
formState.role_ids = record.roles.map(role => role.role_id);
|
||||
modalVisible.value = true;
|
||||
};
|
||||
@@ -366,7 +390,8 @@ const resetForm = () => {
|
||||
email: '',
|
||||
password: '',
|
||||
role_ids: [],
|
||||
status: 1
|
||||
status: 1,
|
||||
user_type: 'system'
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a>删除</a>
|
||||
<a-button type="link" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="showPermissionModal(record)">权限配置</a>
|
||||
|
||||
Reference in New Issue
Block a user