feat: LDAP HA

pull/14125/head
wangruidong 2024-09-04 15:49:59 +08:00 committed by Bryan
parent 512e727ac6
commit c2784c44ad
13 changed files with 572 additions and 260 deletions

View File

@ -1,6 +1,6 @@
# coding:utf-8 # coding:utf-8
# #
import abc
import ldap import ldap
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
@ -15,13 +15,16 @@ from .base import JMSBaseAuthBackend
logger = _LDAPConfig.get_logger() logger = _LDAPConfig.get_logger()
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend): class LDAPBaseBackend(LDAPBackend):
"""
Override this class to override _LDAPUser to LDAPUser @abc.abstractmethod
""" def is_enabled(self):
@staticmethod raise NotImplementedError('is_enabled')
def is_enabled():
return settings.AUTH_LDAP @property
@abc.abstractmethod
def is_user_login_only_in_users(self):
raise NotImplementedError('is_authenticated')
def get_or_build_user(self, username, ldap_user): def get_or_build_user(self, username, ldap_user):
""" """
@ -56,38 +59,6 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
return user, built return user, built
def pre_check(self, username, password):
if not settings.AUTH_LDAP:
error = 'Not enabled auth ldap'
return False, error
if not username:
error = 'Username is None'
return False, error
if not password:
error = 'Password is None'
return False, error
if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS:
user_model = self.get_user_model()
exist = user_model.objects.filter(username=username).exists()
if not exist:
error = 'user ({}) is not in the user list'.format(username)
return False, error
return True, ''
def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend')
if username is None or password is None:
logger.info('No username or password')
return None
match, msg = self.pre_check(username, password)
if not match:
logger.info('Authenticate failed: {}'.format(msg))
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
logger.info('Authenticate user: {}'.format(user))
return user if self.user_can_authenticate(user) else None
def get_user(self, user_id): def get_user(self, user_id):
user = None user = None
try: try:
@ -111,6 +82,67 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
user = ldap_user.populate_user() user = ldap_user.populate_user()
return user return user
def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend')
if username is None or password is None:
logger.info('No username or password')
return None
match, msg = self.pre_check(username, password)
if not match:
logger.info('Authenticate failed: {}'.format(msg))
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
logger.info('Authenticate user: {}'.format(user))
return user if self.user_can_authenticate(user) else None
def pre_check(self, username, password):
if not self.is_enabled():
error = 'Not enabled auth ldap'
return False, error
if not username:
error = 'Username is None'
return False, error
if not password:
error = 'Password is None'
return False, error
if self.is_user_login_only_in_users:
user_model = self.get_user_model()
exist = user_model.objects.filter(username=username).exists()
if not exist:
error = 'user ({}) is not in the user list'.format(username)
return False, error
return True, ''
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
@staticmethod
def is_enabled():
return settings.AUTH_LDAP
@property
def is_user_login_only_in_users(self):
return settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
class LDAPHAAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
settings_prefix = "AUTH_LDAP_HA_"
@staticmethod
def is_enabled():
return settings.AUTH_LDAP_HA
@property
def is_user_login_only_in_users(self):
return settings.AUTH_LDAP_HA_USER_LOGIN_ONLY_IN_USERS
class LDAPUser(_LDAPUser): class LDAPUser(_LDAPUser):
@ -126,13 +158,18 @@ class LDAPUser(_LDAPUser):
configuration in the settings.py file configuration in the settings.py file
is configured with a `lambda` problem value is configured with a `lambda` problem value
""" """
if isinstance(self.backend, LDAPAuthorizationBackend):
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_SEARCH_OU
else:
search_filter = settings.AUTH_LDAP_HA_SEARCH_FILTER
search_ou = settings.AUTH_LDAP_HA_SEARCH_OU
user_search_union = [ user_search_union = [
LDAPSearch( LDAPSearch(
USER_SEARCH, ldap.SCOPE_SUBTREE, USER_SEARCH, ldap.SCOPE_SUBTREE,
settings.AUTH_LDAP_SEARCH_FILTER search_filter
) )
for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|") for USER_SEARCH in str(search_ou).split("|")
] ]
search = LDAPSearchUnion(*user_search_union) search = LDAPSearchUnion(*user_search_union)
@ -169,7 +206,8 @@ class LDAPUser(_LDAPUser):
else: else:
value = is_true(value) value = is_true(value)
except LookupError: except LookupError:
logger.warning("{} does not have a value for the attribute {}".format(self.dn, attr)) logger.warning(
"{} does not have a value for the attribute {}".format(self.dn, attr))
else: else:
if not hasattr(self._user, field): if not hasattr(self._user, field):
continue continue

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-10 16:58+0800\n" "POT-Creation-Date: 2024-09-11 18:15+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -124,9 +124,10 @@ msgstr "成功: %s, 失败: %s, 总数: %s"
#: authentication/forms.py:28 #: authentication/forms.py:28
#: authentication/templates/authentication/login.html:362 #: authentication/templates/authentication/login.html:362
#: settings/serializers/auth/ldap.py:26 settings/serializers/auth/ldap.py:52 #: settings/serializers/auth/ldap.py:26 settings/serializers/auth/ldap.py:52
#: settings/serializers/msg.py:37 settings/serializers/terminal.py:28 #: settings/serializers/auth/ldap_ha.py:34 settings/serializers/msg.py:37
#: terminal/serializers/storage.py:123 terminal/serializers/storage.py:142 #: settings/serializers/terminal.py:28 terminal/serializers/storage.py:123
#: users/forms/profile.py:21 users/serializers/user.py:144 #: terminal/serializers/storage.py:142 users/forms/profile.py:21
#: users/serializers/user.py:144
#: users/templates/users/_msg_user_created.html:13 #: users/templates/users/_msg_user_created.html:13
#: users/templates/users/user_password_verify.html:18 #: users/templates/users/user_password_verify.html:18
#: xpack/plugins/cloud/serializers/account_attrs.py:28 #: xpack/plugins/cloud/serializers/account_attrs.py:28
@ -549,7 +550,8 @@ msgstr "SSH 密钥推送方式"
#: accounts/models/automations/gather_account.py:58 #: accounts/models/automations/gather_account.py:58
#: accounts/serializers/account/backup.py:40 #: accounts/serializers/account/backup.py:40
#: accounts/serializers/automations/change_secret.py:58 #: accounts/serializers/automations/change_secret.py:58
#: settings/serializers/auth/ldap.py:100 settings/serializers/msg.py:45 #: settings/serializers/auth/ldap.py:100
#: settings/serializers/auth/ldap_ha.py:82 settings/serializers/msg.py:45
msgid "Recipient" msgid "Recipient"
msgstr "收件人" msgstr "收件人"
@ -707,8 +709,8 @@ msgstr "密码规则"
#: authentication/models/ssh_key.py:12 #: authentication/models/ssh_key.py:12
#: authentication/serializers/connect_token_secret.py:113 #: authentication/serializers/connect_token_secret.py:113
#: authentication/serializers/connect_token_secret.py:169 labels/models.py:11 #: authentication/serializers/connect_token_secret.py:169 labels/models.py:11
#: ops/mixin.py:28 ops/models/adhoc.py:20 ops/models/celery.py:15 #: ops/mixin.py:28 ops/models/adhoc.py:19 ops/models/celery.py:15
#: ops/models/celery.py:81 ops/models/job.py:142 ops/models/playbook.py:28 #: ops/models/celery.py:81 ops/models/job.py:142 ops/models/playbook.py:30
#: ops/serializers/job.py:18 orgs/models.py:82 #: ops/serializers/job.py:18 orgs/models.py:82
#: perms/models/asset_permission.py:61 rbac/models/role.py:29 #: perms/models/asset_permission.py:61 rbac/models/role.py:29
#: rbac/serializers/role.py:28 settings/models.py:35 settings/models.py:184 #: rbac/serializers/role.py:28 settings/models.py:35 settings/models.py:184
@ -1037,8 +1039,8 @@ msgid ""
msgstr "关联平台,可配置推送参数,如果不关联,将使用默认参数" msgstr "关联平台,可配置推送参数,如果不关联,将使用默认参数"
#: accounts/serializers/account/virtual.py:19 assets/models/cmd_filter.py:40 #: accounts/serializers/account/virtual.py:19 assets/models/cmd_filter.py:40
#: assets/models/cmd_filter.py:88 common/db/models.py:36 ops/models/adhoc.py:26 #: assets/models/cmd_filter.py:88 common/db/models.py:36 ops/models/adhoc.py:25
#: ops/models/job.py:158 ops/models/playbook.py:31 rbac/models/role.py:37 #: ops/models/job.py:158 ops/models/playbook.py:33 rbac/models/role.py:37
#: settings/models.py:40 terminal/models/applet/applet.py:46 #: settings/models.py:40 terminal/models/applet/applet.py:46
#: terminal/models/applet/applet.py:332 terminal/models/applet/host.py:143 #: terminal/models/applet/applet.py:332 terminal/models/applet/host.py:143
#: terminal/models/component/endpoint.py:25 #: terminal/models/component/endpoint.py:25
@ -2974,9 +2976,9 @@ msgstr "用户会话"
msgid "Offline user session" msgid "Offline user session"
msgstr "下线用户会话" msgstr "下线用户会话"
#: audits/serializers.py:33 ops/models/adhoc.py:25 ops/models/base.py:16 #: audits/serializers.py:33 ops/models/adhoc.py:24 ops/models/base.py:16
#: ops/models/base.py:53 ops/models/celery.py:87 ops/models/job.py:151 #: ops/models/base.py:53 ops/models/celery.py:87 ops/models/job.py:151
#: ops/models/job.py:240 ops/models/playbook.py:30 #: ops/models/job.py:240 ops/models/playbook.py:32
#: terminal/models/session/sharing.py:25 #: terminal/models/session/sharing.py:25
msgid "Creator" msgid "Creator"
msgstr "创建者" msgstr "创建者"
@ -3031,7 +3033,7 @@ msgstr "认证令牌"
#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73 #: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73
#: authentication/views/login.py:78 notifications/backends/__init__.py:11 #: authentication/views/login.py:78 notifications/backends/__init__.py:11
#: settings/serializers/auth/wecom.py:11 settings/serializers/auth/wecom.py:16 #: settings/serializers/auth/wecom.py:11 settings/serializers/auth/wecom.py:16
#: users/models/user/__init__.py:122 users/models/user/_source.py:18 #: users/models/user/__init__.py:122 users/models/user/_source.py:19
msgid "WeCom" msgid "WeCom"
msgstr "企业微信" msgstr "企业微信"
@ -3039,21 +3041,21 @@ msgstr "企业微信"
#: authentication/views/login.py:90 notifications/backends/__init__.py:14 #: authentication/views/login.py:90 notifications/backends/__init__.py:14
#: settings/serializers/auth/feishu.py:12 #: settings/serializers/auth/feishu.py:12
#: settings/serializers/auth/feishu.py:14 users/models/user/__init__.py:128 #: settings/serializers/auth/feishu.py:14 users/models/user/__init__.py:128
#: users/models/user/_source.py:20 #: users/models/user/_source.py:21
msgid "FeiShu" msgid "FeiShu"
msgstr "飞书" msgstr "飞书"
#: audits/signal_handlers/login_log.py:40 authentication/views/login.py:102 #: audits/signal_handlers/login_log.py:40 authentication/views/login.py:102
#: authentication/views/slack.py:79 notifications/backends/__init__.py:16 #: authentication/views/slack.py:79 notifications/backends/__init__.py:16
#: settings/serializers/auth/slack.py:11 settings/serializers/auth/slack.py:13 #: settings/serializers/auth/slack.py:11 settings/serializers/auth/slack.py:13
#: users/models/user/__init__.py:134 users/models/user/_source.py:22 #: users/models/user/__init__.py:134 users/models/user/_source.py:23
msgid "Slack" msgid "Slack"
msgstr "Slack" msgstr "Slack"
#: audits/signal_handlers/login_log.py:41 authentication/views/dingtalk.py:151 #: audits/signal_handlers/login_log.py:41 authentication/views/dingtalk.py:151
#: authentication/views/login.py:84 notifications/backends/__init__.py:12 #: authentication/views/login.py:84 notifications/backends/__init__.py:12
#: settings/serializers/auth/dingtalk.py:11 users/models/user/__init__.py:125 #: settings/serializers/auth/dingtalk.py:11 users/models/user/__init__.py:125
#: users/models/user/_source.py:19 #: users/models/user/_source.py:20
msgid "DingTalk" msgid "DingTalk"
msgstr "钉钉" msgstr "钉钉"
@ -3500,7 +3502,7 @@ msgstr "设置手机号码启用"
msgid "Clear phone number to disable" msgid "Clear phone number to disable"
msgstr "清空手机号码禁用" msgstr "清空手机号码禁用"
#: authentication/middleware.py:94 settings/utils/ldap.py:681 #: authentication/middleware.py:94 settings/utils/ldap.py:691
msgid "Authentication failed (before login check failed): {}" msgid "Authentication failed (before login check failed): {}"
msgstr "认证失败 (登录前检查失败): {}" msgstr "认证失败 (登录前检查失败): {}"
@ -3818,7 +3820,7 @@ msgstr "代码错误"
#: authentication/templates/authentication/_msg_oauth_bind.html:3 #: authentication/templates/authentication/_msg_oauth_bind.html:3
#: authentication/templates/authentication/_msg_reset_password.html:3 #: authentication/templates/authentication/_msg_reset_password.html:3
#: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_reset_password_code.html:9
#: jumpserver/conf.py:502 #: jumpserver/conf.py:522
#: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_item_permissions_expire.html:3
#: tickets/templates/tickets/approve_check_password.html:32 #: tickets/templates/tickets/approve_check_password.html:32
#: users/templates/users/_msg_account_expire_reminder.html:4 #: users/templates/users/_msg_account_expire_reminder.html:4
@ -4578,16 +4580,16 @@ msgstr "不能包含特殊字符"
msgid "The mobile phone number format is incorrect" msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确" msgstr "手机号格式不正确"
#: jumpserver/conf.py:496 #: jumpserver/conf.py:516
#, python-brace-format #, python-brace-format
msgid "The verification code is: {code}" msgid "The verification code is: {code}"
msgstr "验证码为: {code}" msgstr "验证码为: {code}"
#: jumpserver/conf.py:501 #: jumpserver/conf.py:521
msgid "Create account successfully" msgid "Create account successfully"
msgstr "创建账号成功" msgstr "创建账号成功"
#: jumpserver/conf.py:503 #: jumpserver/conf.py:523
msgid "Your account has been created successfully" msgid "Your account has been created successfully"
msgstr "你的账号已创建成功" msgstr "你的账号已创建成功"
@ -4749,31 +4751,31 @@ msgid ""
"The task is being created and cannot be interrupted. Please try again later." "The task is being created and cannot be interrupted. Please try again later."
msgstr "正在创建任务,无法中断,请稍后重试。" msgstr "正在创建任务,无法中断,请稍后重试。"
#: ops/api/playbook.py:39 #: ops/api/playbook.py:50
msgid "Currently playbook is being used in a job" msgid "Currently playbook is being used in a job"
msgstr "当前 playbook 正在作业中使用" msgstr "当前 playbook 正在作业中使用"
#: ops/api/playbook.py:97 #: ops/api/playbook.py:113
msgid "Unsupported file content" msgid "Unsupported file content"
msgstr "不支持的文件内容" msgstr "不支持的文件内容"
#: ops/api/playbook.py:99 ops/api/playbook.py:145 ops/api/playbook.py:193 #: ops/api/playbook.py:115 ops/api/playbook.py:161 ops/api/playbook.py:209
msgid "Invalid file path" msgid "Invalid file path"
msgstr "无效的文件路径" msgstr "无效的文件路径"
#: ops/api/playbook.py:171 #: ops/api/playbook.py:187
msgid "This file can not be rename" msgid "This file can not be rename"
msgstr "该文件不能重命名" msgstr "该文件不能重命名"
#: ops/api/playbook.py:190 #: ops/api/playbook.py:206
msgid "File already exists" msgid "File already exists"
msgstr "文件已存在" msgstr "文件已存在"
#: ops/api/playbook.py:208 #: ops/api/playbook.py:224
msgid "File key is required" msgid "File key is required"
msgstr "文件密钥该字段是必填项。" msgstr "文件密钥该字段是必填项。"
#: ops/api/playbook.py:211 #: ops/api/playbook.py:227
msgid "This file can not be delete" msgid "This file can not be delete"
msgstr "无法删除此文件" msgstr "无法删除此文件"
@ -4817,7 +4819,7 @@ msgstr "VCS"
msgid "Adhoc" msgid "Adhoc"
msgstr "命令" msgstr "命令"
#: ops/const.py:39 ops/models/job.py:149 ops/models/playbook.py:88 #: ops/const.py:39 ops/models/job.py:149 ops/models/playbook.py:91
msgid "Playbook" msgid "Playbook"
msgstr "Playbook" msgstr "Playbook"
@ -4881,21 +4883,31 @@ msgstr "超时"
msgid "Command execution disabled" msgid "Command execution disabled"
msgstr "命令执行已禁用" msgstr "命令执行已禁用"
#: ops/const.py:86
msgctxt "scope"
msgid "Public"
msgstr "公有
#: ops/const.py:87
msgid "Private"
msgstr "私有"
#: ops/exception.py:6 #: ops/exception.py:6
msgid "no valid program entry found." msgid "no valid program entry found."
msgstr "没有可用程序入口" msgstr "没有可用程序入口"
#: ops/mixin.py:30 ops/mixin.py:110 settings/serializers/auth/ldap.py:73 #: ops/mixin.py:30 ops/mixin.py:110 settings/serializers/auth/ldap.py:73
#: settings/serializers/auth/ldap_ha.py:55
msgid "Periodic run" msgid "Periodic run"
msgstr "周期执行" msgstr "周期执行"
#: ops/mixin.py:32 ops/mixin.py:96 ops/mixin.py:116 #: ops/mixin.py:32 ops/mixin.py:96 ops/mixin.py:116
#: settings/serializers/auth/ldap.py:80 #: settings/serializers/auth/ldap.py:80 settings/serializers/auth/ldap_ha.py:62
msgid "Interval" msgid "Interval"
msgstr "间隔" msgstr "间隔"
#: ops/mixin.py:35 ops/mixin.py:94 ops/mixin.py:113 #: ops/mixin.py:35 ops/mixin.py:94 ops/mixin.py:113
#: settings/serializers/auth/ldap.py:77 #: settings/serializers/auth/ldap.py:77 settings/serializers/auth/ldap_ha.py:59
msgid "Crontab" msgid "Crontab"
msgstr "Crontab" msgstr "Crontab"
@ -4915,19 +4927,25 @@ msgstr "输入在 {} - {} 范围之间"
msgid "Require interval or crontab setting" msgid "Require interval or crontab setting"
msgstr "需要周期或定期设置" msgstr "需要周期或定期设置"
#: ops/models/adhoc.py:21 #: ops/models/adhoc.py:20
msgid "Pattern" msgid "Pattern"
msgstr "模式" msgstr "模式"
#: ops/models/adhoc.py:23 ops/models/job.py:146 #: ops/models/adhoc.py:22 ops/models/job.py:146
msgid "Module" msgid "Module"
msgstr "模块" msgstr "模块"
#: ops/models/adhoc.py:24 ops/models/celery.py:82 ops/models/job.py:144 #: ops/models/adhoc.py:23 ops/models/celery.py:82 ops/models/job.py:144
#: terminal/models/component/task.py:14 #: terminal/models/component/task.py:14
msgid "Args" msgid "Args"
msgstr "参数" msgstr "参数"
#: ops/models/adhoc.py:26 ops/models/playbook.py:36 ops/serializers/mixin.py:10
#: rbac/models/role.py:31 rbac/models/rolebinding.py:46
#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37
msgid "Scope"
msgstr "范围"
#: ops/models/base.py:19 #: ops/models/base.py:19
msgid "Account policy" msgid "Account policy"
msgstr "账号策略" msgstr "账号策略"
@ -5015,11 +5033,11 @@ msgstr "Material 类型"
msgid "Job Execution" msgid "Job Execution"
msgstr "作业执行" msgstr "作业执行"
#: ops/models/playbook.py:33 #: ops/models/playbook.py:35
msgid "CreateMethod" msgid "CreateMethod"
msgstr "创建方式" msgstr "创建方式"
#: ops/models/playbook.py:34 #: ops/models/playbook.py:37
msgid "VCS URL" msgid "VCS URL"
msgstr "VCS URL" msgstr "VCS URL"
@ -5262,7 +5280,7 @@ msgstr "请选择一个组织后再保存"
#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:91 #: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:91
#: rbac/const.py:7 rbac/models/rolebinding.py:56 #: rbac/const.py:7 rbac/models/rolebinding.py:56
#: rbac/serializers/rolebinding.py:44 settings/serializers/auth/base.py:52 #: rbac/serializers/rolebinding.py:44 settings/serializers/auth/base.py:53
#: terminal/templates/terminal/_msg_command_warning.html:21 #: terminal/templates/terminal/_msg_command_warning.html:21
#: terminal/templates/terminal/_msg_session_sharing.html:14 #: terminal/templates/terminal/_msg_session_sharing.html:14
#: tickets/models/ticket/general.py:303 tickets/serializers/ticket/ticket.py:60 #: tickets/models/ticket/general.py:303 tickets/serializers/ticket/ticket.py:60
@ -5578,11 +5596,6 @@ msgstr "内容类型"
msgid "Permissions" msgid "Permissions"
msgstr "授权" msgstr "授权"
#: rbac/models/role.py:31 rbac/models/rolebinding.py:46
#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37
msgid "Scope"
msgstr "范围"
#: rbac/models/role.py:46 rbac/models/rolebinding.py:52 #: rbac/models/role.py:46 rbac/models/rolebinding.py:52
#: users/models/user/__init__.py:66 #: users/models/user/__init__.py:66
msgid "Role" msgid "Role"
@ -5736,7 +5749,7 @@ msgstr "邮件已经发送{}, 请检查"
msgid "Test smtp setting" msgid "Test smtp setting"
msgstr "测试 smtp 设置" msgstr "测试 smtp 设置"
#: settings/api/ldap.py:90 #: settings/api/ldap.py:92
msgid "" msgid ""
"Users are not synchronized, please click the user synchronization button" "Users are not synchronized, please click the user synchronization button"
msgstr "用户未同步,请点击同步用户按钮" msgstr "用户未同步,请点击同步用户按钮"
@ -5834,58 +5847,62 @@ msgid "LDAP Auth"
msgstr "LDAP 认证" msgstr "LDAP 认证"
#: settings/serializers/auth/base.py:14 #: settings/serializers/auth/base.py:14
msgid "LDAP Auth HA"
msgstr "LDAP HA 认证"
#: settings/serializers/auth/base.py:15
msgid "CAS Auth" msgid "CAS Auth"
msgstr "CAS 认证" msgstr "CAS 认证"
#: settings/serializers/auth/base.py:15 #: settings/serializers/auth/base.py:16
msgid "OPENID Auth" msgid "OPENID Auth"
msgstr "OIDC 认证" msgstr "OIDC 认证"
#: settings/serializers/auth/base.py:16 #: settings/serializers/auth/base.py:17
msgid "SAML2 Auth" msgid "SAML2 Auth"
msgstr "SAML2 认证" msgstr "SAML2 认证"
#: settings/serializers/auth/base.py:17 #: settings/serializers/auth/base.py:18
msgid "OAuth2 Auth" msgid "OAuth2 Auth"
msgstr "OAuth2 认证" msgstr "OAuth2 认证"
#: settings/serializers/auth/base.py:18 #: settings/serializers/auth/base.py:19
msgid "RADIUS Auth" msgid "RADIUS Auth"
msgstr "RADIUS 认证" msgstr "RADIUS 认证"
#: settings/serializers/auth/base.py:19 #: settings/serializers/auth/base.py:20
msgid "DingTalk Auth" msgid "DingTalk Auth"
msgstr "钉钉 认证" msgstr "钉钉 认证"
#: settings/serializers/auth/base.py:20 #: settings/serializers/auth/base.py:21
msgid "FeiShu Auth" msgid "FeiShu Auth"
msgstr "飞书 认证" msgstr "飞书 认证"
#: settings/serializers/auth/base.py:21 #: settings/serializers/auth/base.py:22
msgid "Lark Auth" msgid "Lark Auth"
msgstr "Lark 认证" msgstr "Lark 认证"
#: settings/serializers/auth/base.py:22 #: settings/serializers/auth/base.py:23
msgid "Slack Auth" msgid "Slack Auth"
msgstr "Slack 认证" msgstr "Slack 认证"
#: settings/serializers/auth/base.py:23 #: settings/serializers/auth/base.py:24
msgid "WeCom Auth" msgid "WeCom Auth"
msgstr "企业微信 认证" msgstr "企业微信 认证"
#: settings/serializers/auth/base.py:24 #: settings/serializers/auth/base.py:25
msgid "SSO Auth" msgid "SSO Auth"
msgstr "SSO 令牌认证" msgstr "SSO 令牌认证"
#: settings/serializers/auth/base.py:25 #: settings/serializers/auth/base.py:26
msgid "Passkey Auth" msgid "Passkey Auth"
msgstr "Passkey 认证" msgstr "Passkey 认证"
#: settings/serializers/auth/base.py:27 #: settings/serializers/auth/base.py:28
msgid "Email suffix" msgid "Email suffix"
msgstr "邮件后缀" msgstr "邮件后缀"
#: settings/serializers/auth/base.py:29 #: settings/serializers/auth/base.py:30
msgid "" msgid ""
"After third-party user authentication is successful, if the third-party " "After third-party user authentication is successful, if the third-party "
"authentication service platform does not return the user's email " "authentication service platform does not return the user's email "
@ -5895,19 +5912,19 @@ msgstr ""
"第三方用户认证成功后,若第三方认证服务平台未返回该用户的邮箱信息,系统将自动" "第三方用户认证成功后,若第三方认证服务平台未返回该用户的邮箱信息,系统将自动"
"以此邮箱后缀创建用户" "以此邮箱后缀创建用户"
#: settings/serializers/auth/base.py:36 #: settings/serializers/auth/base.py:37
msgid "Forgot Password URL" msgid "Forgot Password URL"
msgstr "忘记密码链接" msgstr "忘记密码链接"
#: settings/serializers/auth/base.py:37 #: settings/serializers/auth/base.py:38
msgid "The URL for Forgotten Password on the user login page" msgid "The URL for Forgotten Password on the user login page"
msgstr "用户登录页面忘记密码的 URL" msgstr "用户登录页面忘记密码的 URL"
#: settings/serializers/auth/base.py:40 #: settings/serializers/auth/base.py:41
msgid "Login redirection" msgid "Login redirection"
msgstr "启用登录跳转提示" msgstr "启用登录跳转提示"
#: settings/serializers/auth/base.py:42 #: settings/serializers/auth/base.py:43
msgid "" msgid ""
"Should an flash page be displayed before the user is redirected to third-" "Should an flash page be displayed before the user is redirected to third-"
"party authentication when the administrator enables third-party redirect " "party authentication when the administrator enables third-party redirect "
@ -5916,7 +5933,7 @@ msgstr ""
"当管理员启用第三方重定向身份验证时,在用户重定向到第三方身份验证之前是否显示 " "当管理员启用第三方重定向身份验证时,在用户重定向到第三方身份验证之前是否显示 "
"Flash 页面" "Flash 页面"
#: settings/serializers/auth/base.py:54 #: settings/serializers/auth/base.py:55
msgid "" msgid ""
"When you create a user, you associate the user to the organization of your " "When you create a user, you associate the user to the organization of your "
"choice. Users always belong to the Default organization." "choice. Users always belong to the Default organization."
@ -5928,7 +5945,7 @@ msgid "CAS"
msgstr "CAS" msgstr "CAS"
#: settings/serializers/auth/cas.py:15 settings/serializers/auth/ldap.py:44 #: settings/serializers/auth/cas.py:15 settings/serializers/auth/ldap.py:44
#: settings/serializers/auth/oidc.py:61 #: settings/serializers/auth/ldap_ha.py:26 settings/serializers/auth/oidc.py:61
msgid "Server" msgid "Server"
msgstr "服务端地址" msgstr "服务端地址"
@ -5955,9 +5972,10 @@ msgstr "启用属性映射"
#: settings/serializers/auth/cas.py:34 settings/serializers/auth/dingtalk.py:18 #: settings/serializers/auth/cas.py:34 settings/serializers/auth/dingtalk.py:18
#: settings/serializers/auth/feishu.py:18 settings/serializers/auth/lark.py:17 #: settings/serializers/auth/feishu.py:18 settings/serializers/auth/lark.py:17
#: settings/serializers/auth/ldap.py:66 settings/serializers/auth/oauth2.py:60 #: settings/serializers/auth/ldap.py:66 settings/serializers/auth/ldap_ha.py:48
#: settings/serializers/auth/oidc.py:39 settings/serializers/auth/saml2.py:35 #: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:39
#: settings/serializers/auth/slack.py:18 settings/serializers/auth/wecom.py:18 #: settings/serializers/auth/saml2.py:35 settings/serializers/auth/slack.py:18
#: settings/serializers/auth/wecom.py:18
msgid "User attribute" msgid "User attribute"
msgstr "映射属性" msgstr "映射属性"
@ -5999,7 +6017,7 @@ msgstr ""
"用户属性映射,其中 `key` 是 JumpServer 用户属性名称,`value` 是飞书服务用户属" "用户属性映射,其中 `key` 是 JumpServer 用户属性名称,`value` 是飞书服务用户属"
"性名称" "性名称"
#: settings/serializers/auth/lark.py:13 users/models/user/_source.py:21 #: settings/serializers/auth/lark.py:13 users/models/user/_source.py:22
msgid "Lark" msgid "Lark"
msgstr "" msgstr ""
@ -6019,38 +6037,38 @@ msgstr "LDAP"
msgid "LDAP server URI" msgid "LDAP server URI"
msgstr "LDAP 服务域名" msgstr "LDAP 服务域名"
#: settings/serializers/auth/ldap.py:48 #: settings/serializers/auth/ldap.py:48 settings/serializers/auth/ldap_ha.py:30
msgid "Bind DN" msgid "Bind DN"
msgstr "绑定 DN" msgstr "绑定 DN"
#: settings/serializers/auth/ldap.py:49 #: settings/serializers/auth/ldap.py:49 settings/serializers/auth/ldap_ha.py:31
msgid "Binding Distinguished Name" msgid "Binding Distinguished Name"
msgstr "绑定目录管理员" msgstr "绑定目录管理员"
#: settings/serializers/auth/ldap.py:53 #: settings/serializers/auth/ldap.py:53 settings/serializers/auth/ldap_ha.py:35
msgid "Binding password" msgid "Binding password"
msgstr "绑定密码" msgstr "绑定密码"
#: settings/serializers/auth/ldap.py:56 #: settings/serializers/auth/ldap.py:56 settings/serializers/auth/ldap_ha.py:38
msgid "Search OU" msgid "Search OU"
msgstr "用户 OU" msgstr "用户 OU"
#: settings/serializers/auth/ldap.py:58 #: settings/serializers/auth/ldap.py:58 settings/serializers/auth/ldap_ha.py:40
msgid "" msgid ""
"User Search Base, if there are multiple OUs, you can separate them with the " "User Search Base, if there are multiple OUs, you can separate them with the "
"`|` symbol" "`|` symbol"
msgstr "用户搜索库如果有多个OU可以用`|`符号分隔" msgstr "用户搜索库如果有多个OU可以用`|`符号分隔"
#: settings/serializers/auth/ldap.py:62 #: settings/serializers/auth/ldap.py:62 settings/serializers/auth/ldap_ha.py:44
msgid "Search filter" msgid "Search filter"
msgstr "用户过滤器" msgstr "用户过滤器"
#: settings/serializers/auth/ldap.py:63 #: settings/serializers/auth/ldap.py:63 settings/serializers/auth/ldap_ha.py:45
#, python-format #, python-format
msgid "Selection could include (cn|uid|sAMAccountName=%(user)s)" msgid "Selection could include (cn|uid|sAMAccountName=%(user)s)"
msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)"
#: settings/serializers/auth/ldap.py:68 #: settings/serializers/auth/ldap.py:68 settings/serializers/auth/ldap_ha.py:50
msgid "" msgid ""
"User attribute mapping, where the `key` is the JumpServer user attribute " "User attribute mapping, where the `key` is the JumpServer user attribute "
"name and the `value` is the LDAP service user attribute name" "name and the `value` is the LDAP service user attribute name"
@ -6058,11 +6076,11 @@ msgstr ""
"用户属性映射,其中 `key` 是 JumpServer 用户属性名称,`value` 是 LDAP 服务用户" "用户属性映射,其中 `key` 是 JumpServer 用户属性名称,`value` 是 LDAP 服务用户"
"属性名称" "属性名称"
#: settings/serializers/auth/ldap.py:84 #: settings/serializers/auth/ldap.py:84 settings/serializers/auth/ldap_ha.py:66
msgid "Connect timeout (s)" msgid "Connect timeout (s)"
msgstr "连接超时时间 (秒)" msgstr "连接超时时间 (秒)"
#: settings/serializers/auth/ldap.py:89 #: settings/serializers/auth/ldap.py:89 settings/serializers/auth/ldap_ha.py:71
msgid "User DN cache timeout (s)" msgid "User DN cache timeout (s)"
msgstr "User DN 缓存超时时间 (秒)" msgstr "User DN 缓存超时时间 (秒)"
@ -6076,10 +6094,29 @@ msgstr ""
"对用户登录认证时查询出的 User DN 进行缓存,可以有效提高用户认证的速度<br>如果" "对用户登录认证时查询出的 User DN 进行缓存,可以有效提高用户认证的速度<br>如果"
"用户 OU 架构有调整,点击提交即可清除用户 DN 缓存" "用户 OU 架构有调整,点击提交即可清除用户 DN 缓存"
#: settings/serializers/auth/ldap.py:97 #: settings/serializers/auth/ldap.py:97 settings/serializers/auth/ldap_ha.py:79
msgid "Search paged size (piece)" msgid "Search paged size (piece)"
msgstr "搜索分页数量 (条)" msgstr "搜索分页数量 (条)"
#: settings/serializers/auth/ldap_ha.py:23
#: settings/serializers/auth/ldap_ha.py:85
msgid "LDAP HA"
msgstr "LDAP 认证"
#: settings/serializers/auth/ldap_ha.py:27
msgid "LDAP HA server URI"
msgstr "LDAP HA 服务域名"
#: settings/serializers/auth/ldap_ha.py:73
msgid ""
"Caching the User DN obtained during user login authentication can "
"effectivelyimprove the speed of user authentication., 0 means no cache<br>If "
"the user OU structure has been adjusted, click Submit to clear the user DN "
"cache"
msgstr ""
"对用户登录认证时查询出的 User DN 进行缓存,可以有效提高用户认证的速度<br>如果"
"用户 OU 架构有调整,点击提交即可清除用户 DN 缓存"
#: settings/serializers/auth/oauth2.py:19 #: settings/serializers/auth/oauth2.py:19
#: settings/serializers/auth/oauth2.py:22 #: settings/serializers/auth/oauth2.py:22
msgid "OAuth2" msgid "OAuth2"
@ -7085,11 +7122,11 @@ msgid ""
"in the workbench" "in the workbench"
msgstr "*! 如果启用,具有 RBAC 权限的用户将能够使用工作台中的所有工具" msgstr "*! 如果启用,具有 RBAC 权限的用户将能够使用工作台中的所有工具"
#: settings/tasks/ldap.py:29 #: settings/tasks/ldap.py:72
msgid "Periodic import ldap user" msgid "Periodic import ldap user"
msgstr "周期导入 LDAP 用户" msgstr "周期导入 LDAP 用户"
#: settings/tasks/ldap.py:31 #: settings/tasks/ldap.py:74 settings/tasks/ldap.py:86
msgid "" msgid ""
"\n" "\n"
" When LDAP auto-sync is configured, this task will be invoked to " " When LDAP auto-sync is configured, this task will be invoked to "
@ -7099,11 +7136,16 @@ msgstr ""
"\n" "\n"
"当设置了LDAP自动同步将调用该任务进行用户同步" "当设置了LDAP自动同步将调用该任务进行用户同步"
#: settings/tasks/ldap.py:74 #: settings/tasks/ldap.py:84
msgid "Periodic import ldap ha user"
msgstr "周期导入 LDAP HA 用户"
#: settings/tasks/ldap.py:120
msgid "Registration periodic import ldap user task" msgid "Registration periodic import ldap user task"
msgstr "注册周期导入 LDAP 用户 任务" msgstr "注册周期导入 LDAP 用户 任务"
#: settings/tasks/ldap.py:76 #: settings/tasks/ldap.py:122
msgid "" msgid ""
"\n" "\n"
" When LDAP auto-sync parameters change, such as Crontab parameters, " " When LDAP auto-sync parameters change, such as Crontab parameters, "
@ -7113,7 +7155,23 @@ msgid ""
msgstr "" msgstr ""
"\n" "\n"
"当设置了LDAP自动同步参数发生变化时比如Crontab参数重新注册或更新ldap同步任" "当设置了LDAP自动同步参数发生变化时比如Crontab参数重新注册或更新ldap同步任"
"务调用该任务" "务将调用该任务"
#: settings/tasks/ldap.py:138
msgid "Registration periodic import ldap ha user task"
msgstr "注册周期导入 LDAP HA 用户 任务"
#: settings/tasks/ldap.py:140
msgid ""
"\n"
" When LDAP HA auto-sync parameters change, such as Crontab "
"parameters, the LDAP HA sync task \n"
" will be re-registered or updated, and this task will be invoked\n"
" "
msgstr ""
"\n"
"当设置了LDAP HA 自动同步参数发生变化时比如Crontab参数重新注册或更新ldap ha 同步任"
"务将调用该任务"
#: settings/templates/ldap/_msg_import_ldap_user.html:2 #: settings/templates/ldap/_msg_import_ldap_user.html:2
msgid "Sync task finish" msgid "Sync task finish"
@ -7131,108 +7189,108 @@ msgstr "已同步用户"
msgid "No user synchronization required" msgid "No user synchronization required"
msgstr "没有用户需要同步" msgstr "没有用户需要同步"
#: settings/utils/ldap.py:496 #: settings/utils/ldap.py:509
msgid "ldap:// or ldaps:// protocol is used." msgid "ldap:// or ldaps:// protocol is used."
msgstr "使用 ldap:// 或 ldaps:// 协议" msgstr "使用 ldap:// 或 ldaps:// 协议"
#: settings/utils/ldap.py:507 #: settings/utils/ldap.py:520
msgid "Host or port is disconnected: {}" msgid "Host or port is disconnected: {}"
msgstr "主机或端口不可连接: {}" msgstr "主机或端口不可连接: {}"
#: settings/utils/ldap.py:509 #: settings/utils/ldap.py:522
msgid "The port is not the port of the LDAP service: {}" msgid "The port is not the port of the LDAP service: {}"
msgstr "端口不是LDAP服务端口: {}" msgstr "端口不是LDAP服务端口: {}"
#: settings/utils/ldap.py:511 #: settings/utils/ldap.py:524
msgid "Please add certificate: {}" msgid "Please add certificate: {}"
msgstr "请添加证书" msgstr "请添加证书"
#: settings/utils/ldap.py:515 settings/utils/ldap.py:542 #: settings/utils/ldap.py:528 settings/utils/ldap.py:555
#: settings/utils/ldap.py:572 settings/utils/ldap.py:600 #: settings/utils/ldap.py:585 settings/utils/ldap.py:613
msgid "Unknown error: {}" msgid "Unknown error: {}"
msgstr "未知错误: {}" msgstr "未知错误: {}"
#: settings/utils/ldap.py:529 #: settings/utils/ldap.py:542
msgid "Bind DN or Password incorrect" msgid "Bind DN or Password incorrect"
msgstr "绑定DN或密码错误" msgstr "绑定DN或密码错误"
#: settings/utils/ldap.py:536 #: settings/utils/ldap.py:549
msgid "Please enter Bind DN: {}" msgid "Please enter Bind DN: {}"
msgstr "请输入绑定DN: {}" msgstr "请输入绑定DN: {}"
#: settings/utils/ldap.py:538 #: settings/utils/ldap.py:551
msgid "Please enter Password: {}" msgid "Please enter Password: {}"
msgstr "请输入密码: {}" msgstr "请输入密码: {}"
#: settings/utils/ldap.py:540 #: settings/utils/ldap.py:553
msgid "Please enter correct Bind DN and Password: {}" msgid "Please enter correct Bind DN and Password: {}"
msgstr "请输入正确的绑定DN和密码: {}" msgstr "请输入正确的绑定DN和密码: {}"
#: settings/utils/ldap.py:558 #: settings/utils/ldap.py:571
msgid "Invalid User OU or User search filter: {}" msgid "Invalid User OU or User search filter: {}"
msgstr "不合法的用户OU或用户过滤器: {}" msgstr "不合法的用户OU或用户过滤器: {}"
#: settings/utils/ldap.py:589 #: settings/utils/ldap.py:602
msgid "LDAP User attr map not include: {}" msgid "LDAP User attr map not include: {}"
msgstr "LDAP属性映射没有包含: {}" msgstr "LDAP属性映射没有包含: {}"
#: settings/utils/ldap.py:596 #: settings/utils/ldap.py:609
msgid "LDAP User attr map is not dict" msgid "LDAP User attr map is not dict"
msgstr "LDAP属性映射不合法" msgstr "LDAP属性映射不合法"
#: settings/utils/ldap.py:615 #: settings/utils/ldap.py:628
msgid "LDAP authentication is not enabled" msgid "LDAP authentication is not enabled"
msgstr "LDAP认证没有启用" msgstr "LDAP认证没有启用"
#: settings/utils/ldap.py:633 #: settings/utils/ldap.py:646
msgid "Error (Invalid LDAP server): {}" msgid "Error (Invalid LDAP server): {}"
msgstr "错误 (不合法的LDAP服务器地址): {}" msgstr "错误 (不合法的LDAP服务器地址): {}"
#: settings/utils/ldap.py:635 #: settings/utils/ldap.py:648
msgid "Error (Invalid Bind DN): {}" msgid "Error (Invalid Bind DN): {}"
msgstr "错误 (不合法的绑定DN): {}" msgstr "错误 (不合法的绑定DN): {}"
#: settings/utils/ldap.py:637 #: settings/utils/ldap.py:650
msgid "Error (Invalid LDAP User attr map): {}" msgid "Error (Invalid LDAP User attr map): {}"
msgstr "错误 (不合法的LDAP属性映射): {}" msgstr "错误 (不合法的LDAP属性映射): {}"
#: settings/utils/ldap.py:639 #: settings/utils/ldap.py:652
msgid "Error (Invalid User OU or User search filter): {}" msgid "Error (Invalid User OU or User search filter): {}"
msgstr "错误 (不合法的用户OU或用户过滤器): {}" msgstr "错误 (不合法的用户OU或用户过滤器): {}"
#: settings/utils/ldap.py:641 #: settings/utils/ldap.py:654
msgid "Error (Not enabled LDAP authentication): {}" msgid "Error (Not enabled LDAP authentication): {}"
msgstr "错误 (没有启用LDAP认证): {}" msgstr "错误 (没有启用LDAP认证): {}"
#: settings/utils/ldap.py:643 #: settings/utils/ldap.py:656
msgid "Error (Unknown): {}" msgid "Error (Unknown): {}"
msgstr "错误 (未知): {}" msgstr "错误 (未知): {}"
#: settings/utils/ldap.py:646 #: settings/utils/ldap.py:659
msgid "Succeed: Match {} users" msgid "Succeed: Match {} users"
msgstr "成功匹配 {} 个用户" msgstr "成功匹配 {} 个用户"
#: settings/utils/ldap.py:679 #: settings/utils/ldap.py:689
msgid "Authentication failed (configuration incorrect): {}" msgid "Authentication failed (configuration incorrect): {}"
msgstr "认证失败 (配置错误): {}" msgstr "认证失败 (配置错误): {}"
#: settings/utils/ldap.py:683 #: settings/utils/ldap.py:693
msgid "Authentication failed (username or password incorrect): {}" msgid "Authentication failed (username or password incorrect): {}"
msgstr "认证失败 (用户名或密码不正确): {}" msgstr "认证失败 (用户名或密码不正确): {}"
#: settings/utils/ldap.py:685 #: settings/utils/ldap.py:695
msgid "Authentication failed (Unknown): {}" msgid "Authentication failed (Unknown): {}"
msgstr "认证失败: (未知): {}" msgstr "认证失败: (未知): {}"
#: settings/utils/ldap.py:688 #: settings/utils/ldap.py:698
msgid "Authentication success: {}" msgid "Authentication success: {}"
msgstr "认证成功: {}" msgstr "认证成功: {}"
#: settings/ws.py:203 #: settings/ws.py:201
msgid "No LDAP user was found" msgid "No LDAP user was found"
msgstr "没有获取到 LDAP 用户" msgstr "没有获取到 LDAP 用户"
#: settings/ws.py:209 #: settings/ws.py:207
msgid "Total {}, success {}, failure {}" msgid "Total {}, success {}, failure {}"
msgstr "总共 {},成功 {},失败 {}" msgstr "总共 {},成功 {},失败 {}"

View File

@ -288,6 +288,26 @@ class Config(dict):
'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False, 'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False,
'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1, 'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1,
# Auth LDAP HA settings
'AUTH_LDAP_HA': False,
'AUTH_LDAP_HA_SERVER_URI': 'ldap://localhost:389',
'AUTH_LDAP_HA_BIND_DN': 'cn=admin,dc=jumpserver,dc=org',
'AUTH_LDAP_HA_BIND_PASSWORD': '',
'AUTH_LDAP_HA_SEARCH_OU': 'ou=tech,dc=jumpserver,dc=org',
'AUTH_LDAP_HA_SEARCH_FILTER': '(cn=%(user)s)',
'AUTH_LDAP_HA_START_TLS': False,
'AUTH_LDAP_HA_USER_ATTR_MAP': {"username": "cn", "name": "sn", "email": "mail"},
'AUTH_LDAP_HA_CONNECT_TIMEOUT': 10,
'AUTH_LDAP_HA_CACHE_TIMEOUT': 3600 * 24 * 30,
'AUTH_LDAP_HA_SEARCH_PAGED_SIZE': 1000,
'AUTH_LDAP_HA_SYNC_IS_PERIODIC': False,
'AUTH_LDAP_HA_SYNC_INTERVAL': None,
'AUTH_LDAP_HA_SYNC_CRONTAB': None,
'AUTH_LDAP_HA_SYNC_ORG_IDS': [DEFAULT_ID],
'AUTH_LDAP_HA_SYNC_RECEIVERS': [],
'AUTH_LDAP_HA_USER_LOGIN_ONLY_IN_USERS': False,
'AUTH_LDAP_HA_OPTIONS_OPT_REFERRALS': -1,
# OpenID 配置参数 # OpenID 配置参数
# OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8) # OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
'AUTH_OPENID': False, 'AUTH_OPENID': False,

View File

@ -53,6 +53,44 @@ AUTH_LDAP_SYNC_ORG_IDS = CONFIG.AUTH_LDAP_SYNC_ORG_IDS
AUTH_LDAP_SYNC_RECEIVERS = CONFIG.AUTH_LDAP_SYNC_RECEIVERS AUTH_LDAP_SYNC_RECEIVERS = CONFIG.AUTH_LDAP_SYNC_RECEIVERS
AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
# Auth LDAP HA settings
AUTH_LDAP_HA = CONFIG.AUTH_LDAP_HA
AUTH_LDAP_HA_SERVER_URI = CONFIG.AUTH_LDAP_HA_SERVER_URI
AUTH_LDAP_HA_BIND_DN = CONFIG.AUTH_LDAP_HA_BIND_DN
AUTH_LDAP_HA_BIND_PASSWORD = CONFIG.AUTH_LDAP_HA_BIND_PASSWORD
AUTH_LDAP_HA_SEARCH_OU = CONFIG.AUTH_LDAP_HA_SEARCH_OU
AUTH_LDAP_HA_SEARCH_FILTER = CONFIG.AUTH_LDAP_HA_SEARCH_FILTER
AUTH_LDAP_HA_START_TLS = CONFIG.AUTH_LDAP_HA_START_TLS
AUTH_LDAP_HA_USER_ATTR_MAP = CONFIG.AUTH_LDAP_HA_USER_ATTR_MAP
AUTH_LDAP_HA_USER_QUERY_FIELD = 'username'
AUTH_LDAP_HA_GLOBAL_OPTIONS = {
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
ldap.OPT_REFERRALS: CONFIG.AUTH_LDAP_HA_OPTIONS_OPT_REFERRALS
}
LDAP_HA_CACERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ha_ca.pem")
if os.path.isfile(LDAP_HA_CACERT_FILE):
AUTH_LDAP_HA_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CACERT_FILE
LDAP_HA_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ha_cert.pem")
if os.path.isfile(LDAP_HA_CERT_FILE):
AUTH_LDAP_HA_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CERTFILE] = LDAP_HA_CERT_FILE
LDAP_HA_KEY_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ha_cert.key")
if os.path.isfile(LDAP_HA_KEY_FILE):
AUTH_LDAP_HA_GLOBAL_OPTIONS[ldap.OPT_X_TLS_KEYFILE] = LDAP_HA_KEY_FILE
AUTH_LDAP_HA_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: CONFIG.AUTH_LDAP_HA_CONNECT_TIMEOUT,
ldap.OPT_NETWORK_TIMEOUT: CONFIG.AUTH_LDAP_HA_CONNECT_TIMEOUT
}
AUTH_LDAP_HA_CACHE_TIMEOUT = CONFIG.AUTH_LDAP_HA_CACHE_TIMEOUT
AUTH_LDAP_HA_ALWAYS_UPDATE_USER = True
AUTH_LDAP_HA_SEARCH_PAGED_SIZE = CONFIG.AUTH_LDAP_HA_SEARCH_PAGED_SIZE
AUTH_LDAP_HA_SYNC_IS_PERIODIC = CONFIG.AUTH_LDAP_HA_SYNC_IS_PERIODIC
AUTH_LDAP_HA_SYNC_INTERVAL = CONFIG.AUTH_LDAP_HA_SYNC_INTERVAL
AUTH_LDAP_HA_SYNC_CRONTAB = CONFIG.AUTH_LDAP_HA_SYNC_CRONTAB
AUTH_LDAP_HA_SYNC_ORG_IDS = CONFIG.AUTH_LDAP_HA_SYNC_ORG_IDS
AUTH_LDAP_HA_SYNC_RECEIVERS = CONFIG.AUTH_LDAP_HA_SYNC_RECEIVERS
AUTH_LDAP_HA_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_HA_USER_LOGIN_ONLY_IN_USERS
# ============================================================================== # ==============================================================================
# 认证 OpenID 配置参数 # 认证 OpenID 配置参数
# 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html # 参考: https://django-oidc-rp.readthedocs.io/en/stable/settings.html
@ -212,6 +250,7 @@ RBAC_BACKEND = 'rbac.backends.RBACBackend'
AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend' AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend'
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend' AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend' AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
AUTH_BACKEND_LDAP_HA = 'authentication.backends.ldap.LDAPHAAuthorizationBackend'
AUTH_BACKEND_OIDC_PASSWORD = 'authentication.backends.oidc.OIDCAuthPasswordBackend' AUTH_BACKEND_OIDC_PASSWORD = 'authentication.backends.oidc.OIDCAuthPasswordBackend'
AUTH_BACKEND_OIDC_CODE = 'authentication.backends.oidc.OIDCAuthCodeBackend' AUTH_BACKEND_OIDC_CODE = 'authentication.backends.oidc.OIDCAuthCodeBackend'
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend' AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
@ -232,7 +271,7 @@ AUTHENTICATION_BACKENDS = [
# 只做权限校验 # 只做权限校验
RBAC_BACKEND, RBAC_BACKEND,
# 密码形式 # 密码形式
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS, AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_LDAP_HA, AUTH_BACKEND_RADIUS,
# 跳转形式 # 跳转形式
AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2, AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
AUTH_BACKEND_OAUTH2, AUTH_BACKEND_OAUTH2,

View File

@ -26,12 +26,14 @@ class LDAPUserListApi(generics.ListAPIView):
def get_queryset_from_cache(self): def get_queryset_from_cache(self):
search_value = self.request.query_params.get('search') search_value = self.request.query_params.get('search')
users = LDAPCacheUtil().search(search_value=search_value) category = self.request.query_params.get('category')
users = LDAPCacheUtil(category=category).search(search_value=search_value)
return users return users
def get_queryset_from_server(self): def get_queryset_from_server(self):
search_value = self.request.query_params.get('search') search_value = self.request.query_params.get('search')
users = LDAPServerUtil().search(search_value=search_value) category = self.request.query_params.get('category')
users = LDAPServerUtil(category=category).search(search_value=search_value)
return users return users
def get_queryset(self): def get_queryset(self):

View File

@ -36,6 +36,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'security_password': serializers.SecurityPasswordRuleSerializer, 'security_password': serializers.SecurityPasswordRuleSerializer,
'security_login_limit': serializers.SecurityLoginLimitSerializer, 'security_login_limit': serializers.SecurityLoginLimitSerializer,
'ldap': serializers.LDAPSettingSerializer, 'ldap': serializers.LDAPSettingSerializer,
'ldap_ha': serializers.LDAPHASettingSerializer,
'email': serializers.EmailSettingSerializer, 'email': serializers.EmailSettingSerializer,
'email_content': serializers.EmailContentSettingSerializer, 'email_content': serializers.EmailContentSettingSerializer,
'wecom': serializers.WeComSettingSerializer, 'wecom': serializers.WeComSettingSerializer,

View File

@ -4,6 +4,7 @@ from .dingtalk import *
from .feishu import * from .feishu import *
from .lark import * from .lark import *
from .ldap import * from .ldap import *
from .ldap_ha import *
from .oauth2 import * from .oauth2 import *
from .oidc import * from .oidc import *
from .passkey import * from .passkey import *

View File

@ -11,6 +11,7 @@ class AuthSettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('Authentication') PREFIX_TITLE = _('Authentication')
AUTH_LDAP = serializers.BooleanField(required=False, label=_('LDAP Auth')) AUTH_LDAP = serializers.BooleanField(required=False, label=_('LDAP Auth'))
AUTH_LDAP_HA = serializers.BooleanField(required=False, label=_('LDAP Auth HA'))
AUTH_CAS = serializers.BooleanField(required=False, label=_('CAS Auth')) AUTH_CAS = serializers.BooleanField(required=False, label=_('CAS Auth'))
AUTH_OPENID = serializers.BooleanField(required=False, label=_('OPENID Auth')) AUTH_OPENID = serializers.BooleanField(required=False, label=_('OPENID Auth'))
AUTH_SAML2 = serializers.BooleanField(default=False, label=_("SAML2 Auth")) AUTH_SAML2 = serializers.BooleanField(default=False, label=_("SAML2 Auth"))

View File

@ -0,0 +1,94 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.serializers.fields import EncryptedField
from .base import OrgListField
__all__ = ['LDAPHATestConfigSerializer', 'LDAPHASettingSerializer']
class LDAPHATestConfigSerializer(serializers.Serializer):
AUTH_LDAP_HA_SERVER_URI = serializers.CharField(max_length=1024)
AUTH_LDAP_HA_BIND_DN = serializers.CharField(max_length=1024, required=False, allow_blank=True)
AUTH_LDAP_HA_BIND_PASSWORD = EncryptedField(required=False, allow_blank=True)
AUTH_LDAP_HA_SEARCH_OU = serializers.CharField()
AUTH_LDAP_HA_SEARCH_FILTER = serializers.CharField()
AUTH_LDAP_HA_USER_ATTR_MAP = serializers.JSONField()
AUTH_LDAP_HA_START_TLS = serializers.BooleanField(required=False)
AUTH_LDAP_HA = serializers.BooleanField(required=False)
class LDAPHASettingSerializer(serializers.Serializer):
# encrypt_fields 现在使用 write_only 来判断了
PREFIX_TITLE = _('LDAP HA')
AUTH_LDAP_HA_SERVER_URI = serializers.CharField(
required=True, max_length=1024, label=_('Server'),
help_text=_('LDAP HA server URI')
)
AUTH_LDAP_HA_BIND_DN = serializers.CharField(
required=False, max_length=1024, label=_('Bind DN'),
help_text=_('Binding Distinguished Name')
)
AUTH_LDAP_HA_BIND_PASSWORD = EncryptedField(
max_length=1024, required=False, label=_('Password'),
help_text=_('Binding password')
)
AUTH_LDAP_HA_SEARCH_OU = serializers.CharField(
max_length=1024, allow_blank=True, required=False, label=_('Search OU'),
help_text=_(
'User Search Base, if there are multiple OUs, you can separate them with the `|` symbol'
)
)
AUTH_LDAP_HA_SEARCH_FILTER = serializers.CharField(
max_length=1024, required=True, label=_('Search filter'),
help_text=_('Selection could include (cn|uid|sAMAccountName=%(user)s)')
)
AUTH_LDAP_HA_USER_ATTR_MAP = serializers.JSONField(
required=True, label=_('User attribute'),
help_text=_(
'User attribute mapping, where the `key` is the JumpServer user attribute name and the '
'`value` is the LDAP service user attribute name'
)
)
AUTH_LDAP_HA_SYNC_IS_PERIODIC = serializers.BooleanField(
required=False, label=_('Periodic run')
)
AUTH_LDAP_HA_SYNC_CRONTAB = serializers.CharField(
required=False, max_length=128, allow_null=True, allow_blank=True,
label=_('Crontab')
)
AUTH_LDAP_HA_SYNC_INTERVAL = serializers.IntegerField(
required=False, default=24, allow_null=True, label=_('Interval')
)
AUTH_LDAP_HA_CONNECT_TIMEOUT = serializers.IntegerField(
min_value=1, max_value=300,
required=False, label=_('Connect timeout (s)'),
)
AUTH_LDAP_HA_CACHE_TIMEOUT = serializers.IntegerField(
min_value=0, max_value=3600 * 24 * 30 * 12,
default=3600 * 24 * 30,
required=False, label=_('User DN cache timeout (s)'),
help_text=_(
'Caching the User DN obtained during user login authentication can effectively'
'improve the speed of user authentication., 0 means no cache<br>'
'If the user OU structure has been adjusted, click Submit to clear the user DN cache'
)
)
AUTH_LDAP_HA_SEARCH_PAGED_SIZE = serializers.IntegerField(
required=False, label=_('Search paged size (piece)')
)
AUTH_LDAP_HA_SYNC_RECEIVERS = serializers.ListField(
required=False, label=_('Recipient'), max_length=36
)
AUTH_LDAP_HA = serializers.BooleanField(required=False, label=_('LDAP HA'))
AUTH_LDAP_HA_SYNC_ORG_IDS = OrgListField()
def post_save(self):
keys = ['AUTH_LDAP_HA_SYNC_IS_PERIODIC', 'AUTH_LDAP_HA_SYNC_INTERVAL', 'AUTH_LDAP_HA_SYNC_CRONTAB']
kwargs = {k: self.validated_data[k] for k in keys if k in self.validated_data}
if not kwargs:
return
from settings.tasks import import_ldap_ha_user_periodic
import_ldap_ha_user_periodic(**kwargs)

View File

@ -1,5 +1,4 @@
# coding: utf-8 # coding: utf-8
#
import time import time
from celery import shared_task from celery import shared_task
from django.conf import settings from django.conf import settings
@ -16,45 +15,44 @@ from settings.notifications import LDAPImportMessage
from users.models import User from users.models import User
from ..utils import LDAPSyncUtil, LDAPServerUtil, LDAPImportUtil from ..utils import LDAPSyncUtil, LDAPServerUtil, LDAPImportUtil
__all__ = ['sync_ldap_user', 'import_ldap_user_periodic', 'import_ldap_user'] __all__ = [
'sync_ldap_user', 'import_ldap_user_periodic', 'import_ldap_ha_user_periodic',
'import_ldap_user', 'import_ldap_ha_user'
]
logger = get_logger(__file__) logger = get_logger(__file__)
def sync_ldap_user(): def sync_ldap_user(category='ldap'):
LDAPSyncUtil().perform_sync() LDAPSyncUtil(category=category).perform_sync()
@shared_task( def perform_import(category, util_server):
verbose_name=_('Periodic import ldap user'),
description=_(
"""
When LDAP auto-sync is configured, this task will be invoked to synchronize users
"""
)
)
def import_ldap_user():
start_time = time.time() start_time = time.time()
time_start_display = local_now_display() time_start_display = local_now_display()
logger.info("Start import ldap user task") logger.info(f"Start import {category} ldap user task")
util_server = LDAPServerUtil()
util_import = LDAPImportUtil() util_import = LDAPImportUtil()
users = util_server.search() users = util_server.search()
if settings.XPACK_ENABLED: if settings.XPACK_ENABLED:
org_ids = settings.AUTH_LDAP_SYNC_ORG_IDS org_ids = getattr(settings, f"AUTH_{category.upper()}_SYNC_ORG_IDS")
default_org = None default_org = None
else: else:
# 社区版默认导入Default组织
org_ids = [Organization.DEFAULT_ID] org_ids = [Organization.DEFAULT_ID]
default_org = Organization.default() default_org = Organization.default()
orgs = list(set([Organization.get_instance(org_id, default=default_org) for org_id in org_ids])) orgs = list(set([Organization.get_instance(org_id, default=default_org) for org_id in org_ids]))
new_users, errors = util_import.perform_import(users, orgs) new_users, errors = util_import.perform_import(users, orgs)
if errors: if errors:
logger.error("Imported LDAP users errors: {}".format(errors)) logger.error(f"Imported {category} LDAP users errors: {errors}")
else: else:
logger.info('Imported {} users successfully'.format(len(users))) logger.info(f"Imported {len(users)} {category} users successfully")
if settings.AUTH_LDAP_SYNC_RECEIVERS:
user_ids = settings.AUTH_LDAP_SYNC_RECEIVERS receivers_setting = f"AUTH_{category.upper()}_SYNC_RECEIVERS"
if getattr(settings, receivers_setting, None):
user_ids = getattr(settings, receivers_setting)
recipient_list = User.objects.filter(id__in=list(user_ids)) recipient_list = User.objects.filter(id__in=list(user_ids))
end_time = time.time() end_time = time.time()
extra_kwargs = { extra_kwargs = {
@ -70,6 +68,54 @@ def import_ldap_user():
LDAPImportMessage(user, extra_kwargs).publish() LDAPImportMessage(user, extra_kwargs).publish()
@shared_task(
verbose_name=_('Periodic import ldap user'),
description=_(
"""
When LDAP auto-sync is configured, this task will be invoked to synchronize users
"""
)
)
def import_ldap_user():
perform_import('ldap', LDAPServerUtil())
@shared_task(
verbose_name=_('Periodic import ldap ha user'),
description=_(
"""
When LDAP auto-sync is configured, this task will be invoked to synchronize users
"""
)
)
def import_ldap_ha_user():
perform_import('ldap_ha', LDAPServerUtil(category='ldap_ha'))
def register_periodic_task(task_name, task_func, interval_key, enabled_key, crontab_key, **kwargs):
interval = kwargs.get(interval_key, settings.AUTH_LDAP_SYNC_INTERVAL)
enabled = kwargs.get(enabled_key, settings.AUTH_LDAP_SYNC_IS_PERIODIC)
crontab = kwargs.get(crontab_key, settings.AUTH_LDAP_SYNC_CRONTAB)
if isinstance(interval, int):
interval = interval * 3600
else:
interval = None
if crontab:
interval = None # 优先使用 crontab
tasks = {
task_name: {
'task': task_func.name,
'interval': interval,
'crontab': crontab,
'enabled': enabled
}
}
create_or_update_celery_periodic_tasks(tasks)
@shared_task( @shared_task(
verbose_name=_('Registration periodic import ldap user task'), verbose_name=_('Registration periodic import ldap user task'),
description=_( description=_(
@ -81,23 +127,26 @@ def import_ldap_user():
) )
@after_app_ready_start @after_app_ready_start
def import_ldap_user_periodic(**kwargs): def import_ldap_user_periodic(**kwargs):
task_name = 'import_ldap_user_periodic' register_periodic_task(
interval = kwargs.get('AUTH_LDAP_SYNC_INTERVAL', settings.AUTH_LDAP_SYNC_INTERVAL) 'import_ldap_user_periodic', import_ldap_user,
enabled = kwargs.get('AUTH_LDAP_SYNC_IS_PERIODIC', settings.AUTH_LDAP_SYNC_IS_PERIODIC) 'AUTH_LDAP_SYNC_INTERVAL', 'AUTH_LDAP_SYNC_IS_PERIODIC',
crontab = kwargs.get('AUTH_LDAP_SYNC_CRONTAB', settings.AUTH_LDAP_SYNC_CRONTAB) 'AUTH_LDAP_SYNC_CRONTAB', **kwargs
if isinstance(interval, int): )
interval = interval * 3600
else:
interval = None @shared_task(
if crontab: verbose_name=_('Registration periodic import ldap ha user task'),
# 优先使用 crontab description=_(
interval = None """
tasks = { When LDAP HA auto-sync parameters change, such as Crontab parameters, the LDAP HA sync task
task_name: { will be re-registered or updated, and this task will be invoked
'task': import_ldap_user.name, """
'interval': interval, )
'crontab': crontab, )
'enabled': enabled @after_app_ready_start
} def import_ldap_ha_user_periodic(**kwargs):
} register_periodic_task(
create_or_update_celery_periodic_tasks(tasks) 'import_ldap_ha_user_periodic', import_ldap_ha_user,
'AUTH_LDAP_HA_SYNC_INTERVAL', 'AUTH_LDAP_HA_SYNC_IS_PERIODIC',
'AUTH_LDAP_HA_SYNC_CRONTAB', **kwargs
)

View File

@ -24,7 +24,8 @@ from ldap3.core.exceptions import (
LDAPAttributeError, LDAPAttributeError,
) )
from authentication.backends.ldap import LDAPAuthorizationBackend, LDAPUser from authentication.backends.ldap import LDAPAuthorizationBackend, LDAPUser, \
LDAPHAAuthorizationBackend
from common.const import LDAP_AD_ACCOUNT_DISABLE from common.const import LDAP_AD_ACCOUNT_DISABLE
from common.db.utils import close_old_connections from common.db.utils import close_old_connections
from common.utils import timeit, get_logger from common.utils import timeit, get_logger
@ -46,7 +47,7 @@ LDAP_USE_CACHE_FLAGS = [1, '1', 'true', 'True', True]
class LDAPConfig(object): class LDAPConfig(object):
def __init__(self, config=None): def __init__(self, config=None, category='ldap'):
self.server_uri = None self.server_uri = None
self.bind_dn = None self.bind_dn = None
self.password = None self.password = None
@ -55,6 +56,7 @@ class LDAPConfig(object):
self.search_filter = None self.search_filter = None
self.attr_map = None self.attr_map = None
self.auth_ldap = None self.auth_ldap = None
self.category = category
if isinstance(config, dict): if isinstance(config, dict):
self.load_from_config(config) self.load_from_config(config)
else: else:
@ -71,25 +73,26 @@ class LDAPConfig(object):
self.auth_ldap = config.get('auth_ldap') self.auth_ldap = config.get('auth_ldap')
def load_from_settings(self): def load_from_settings(self):
self.server_uri = settings.AUTH_LDAP_SERVER_URI prefix = 'AUTH_LDAP' if self.category == 'ldap' else 'AUTH_LDAP_HA'
self.bind_dn = settings.AUTH_LDAP_BIND_DN self.server_uri = getattr(settings, f"{prefix}_SERVER_URI")
self.password = settings.AUTH_LDAP_BIND_PASSWORD self.bind_dn = getattr(settings, f"{prefix}_BIND_DN")
self.use_ssl = settings.AUTH_LDAP_START_TLS self.password = getattr(settings, f"{prefix}_BIND_PASSWORD")
self.search_ou = settings.AUTH_LDAP_SEARCH_OU self.use_ssl = getattr(settings, f"{prefix}_START_TLS")
self.search_filter = settings.AUTH_LDAP_SEARCH_FILTER self.search_ou = getattr(settings, f"{prefix})_SEARCH_OU")
self.attr_map = settings.AUTH_LDAP_USER_ATTR_MAP self.search_filter = getattr(settings, f"{prefix}_SEARCH_FILTER")
self.auth_ldap = settings.AUTH_LDAP self.attr_map = getattr(settings, f"{prefix}_USER_ATTR_MAP")
self.auth_ldap = getattr(settings, prefix)
class LDAPServerUtil(object): class LDAPServerUtil(object):
def __init__(self, config=None): def __init__(self, config=None, category='ldap'):
if isinstance(config, dict): if isinstance(config, dict):
self.config = LDAPConfig(config=config) self.config = LDAPConfig(config=config)
elif isinstance(config, LDAPConfig): elif isinstance(config, LDAPConfig):
self.config = config self.config = config
else: else:
self.config = LDAPConfig() self.config = LDAPConfig(category=category)
self._conn = None self._conn = None
self._paged_size = self.get_paged_size() self._paged_size = self.get_paged_size()
self.search_users = None self.search_users = None
@ -230,25 +233,29 @@ class LDAPServerUtil(object):
class LDAPCacheUtil(object): class LDAPCacheUtil(object):
CACHE_KEY_USERS = 'CACHE_KEY_LDAP_USERS'
def __init__(self): def __init__(self, category='ldap'):
self.search_users = None self.search_users = None
self.search_value = None self.search_value = None
self.category = category
if self.category == 'ldap':
self.cache_key_users = 'CACHE_KEY_LDAP_USERS'
else:
self.cache_key_users = 'CACHE_KEY_LDAP_HA_USERS'
def set_users(self, users): def set_users(self, users):
logger.info('Set ldap users to cache, count: {}'.format(len(users))) logger.info('Set ldap users to cache, count: {}'.format(len(users)))
cache.set(self.CACHE_KEY_USERS, users, None) cache.set(self.cache_key_users, users, None)
def get_users(self): def get_users(self):
users = cache.get(self.CACHE_KEY_USERS) users = cache.get(self.cache_key_users)
count = users if users is None else len(users) count = users if users is None else len(users)
logger.info('Get ldap users from cache, count: {}'.format(count)) logger.info('Get ldap users from cache, count: {}'.format(count))
return users return users
def delete_users(self): def delete_users(self):
logger.info('Delete ldap users from cache') logger.info('Delete ldap users from cache')
cache.delete(self.CACHE_KEY_USERS) cache.delete(self.cache_key_users)
def filter_users(self, users): def filter_users(self, users):
if users is None: if users is None:
@ -288,10 +295,11 @@ class LDAPSyncUtil(object):
TASK_STATUS_IS_RUNNING = 'RUNNING' TASK_STATUS_IS_RUNNING = 'RUNNING'
TASK_STATUS_IS_OVER = 'OVER' TASK_STATUS_IS_OVER = 'OVER'
def __init__(self): def __init__(self, category='ldap'):
self.server_util = LDAPServerUtil() self.server_util = LDAPServerUtil(category=category)
self.cache_util = LDAPCacheUtil() self.cache_util = LDAPCacheUtil(category=category)
self.task_error_msg = None self.task_error_msg = None
self.category = category
def clear_cache(self): def clear_cache(self):
logger.info('Clear ldap sync cache') logger.info('Clear ldap sync cache')
@ -347,7 +355,7 @@ class LDAPSyncUtil(object):
def perform_sync(self): def perform_sync(self):
logger.info('Start perform sync ldap users from server to cache') logger.info('Start perform sync ldap users from server to cache')
try: try:
ok, msg = LDAPTestUtil().test_config() ok, msg = LDAPTestUtil(category=self.category).test_config()
if not ok: if not ok:
raise self.LDAPSyncUtilException(msg) raise self.LDAPSyncUtilException(msg)
self.sync() self.sync()
@ -377,6 +385,7 @@ class LDAPImportUtil(object):
user['email'] = self.get_user_email(user) user['email'] = self.get_user_email(user)
if user['username'] not in ['admin']: if user['username'] not in ['admin']:
user['source'] = User.Source.ldap.value user['source'] = User.Source.ldap.value
user.pop('status', None)
obj, created = User.objects.update_or_create( obj, created = User.objects.update_or_create(
username=user['username'], defaults=user username=user['username'], defaults=user
) )
@ -476,9 +485,13 @@ class LDAPTestUtil(object):
class LDAPBeforeLoginCheckError(LDAPExceptionError): class LDAPBeforeLoginCheckError(LDAPExceptionError):
pass pass
def __init__(self, config=None): def __init__(self, config=None, category='ldap'):
self.config = LDAPConfig(config) self.config = LDAPConfig(config, category)
self.user_entries = [] self.user_entries = []
if category == 'ldap':
self.backend = LDAPAuthorizationBackend()
else:
self.backend = LDAPHAAuthorizationBackend()
def _test_connection_bind(self, authentication=None, user=None, password=None): def _test_connection_bind(self, authentication=None, user=None, password=None):
server = Server(self.config.server_uri) server = Server(self.config.server_uri)
@ -656,15 +669,12 @@ class LDAPTestUtil(object):
if not cache.get(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS): if not cache.get(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS):
self.test_config() self.test_config()
backend = LDAPAuthorizationBackend() ok, msg = self.backend.pre_check(username, password)
ok, msg = backend.pre_check(username, password)
if not ok: if not ok:
raise self.LDAPBeforeLoginCheckError(msg) raise self.LDAPBeforeLoginCheckError(msg)
@staticmethod def _test_login_auth(self, username, password):
def _test_login_auth(username, password): ldap_user = LDAPUser(self.backend, username=username.strip())
backend = LDAPAuthorizationBackend()
ldap_user = LDAPUser(backend, username=username.strip())
ldap_user._authenticate_user_dn(password) ldap_user._authenticate_user_dn(password)
def _test_login(self, username, password): def _test_login(self, username, password):

View File

@ -3,16 +3,17 @@
import json import json
import asyncio import asyncio
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.core.cache import cache from django.core.cache import cache
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _, activate from django.utils.translation import gettext_lazy as _, activate
from django.utils import translation from django.utils import translation
from urllib.parse import parse_qs
from common.db.utils import close_old_connections from common.db.utils import close_old_connections
from common.utils import get_logger from common.utils import get_logger
from settings.serializers import ( from settings.serializers import (
LDAPHATestConfigSerializer,
LDAPTestConfigSerializer, LDAPTestConfigSerializer,
LDAPTestLoginSerializer LDAPTestLoginSerializer
) )
@ -101,8 +102,12 @@ class ToolsWebsocket(AsyncJsonWebsocketConsumer):
class LdapWebsocket(AsyncJsonWebsocketConsumer): class LdapWebsocket(AsyncJsonWebsocketConsumer):
category: str
async def connect(self): async def connect(self):
user = self.scope["user"] user = self.scope["user"]
query = parse_qs(self.scope['query_string'].decode())
self.category = query.get('category', ['ldap'])[0]
if user.is_authenticated: if user.is_authenticated:
await self.accept() await self.accept()
else: else:
@ -125,30 +130,21 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
await self.close() await self.close()
close_old_connections() close_old_connections()
@staticmethod def get_ldap_config(self, serializer):
def get_ldap_config(serializer): prefix = 'AUTH_LDAP_' if self.category == 'ldap' else 'AUTH_LDAP_HA_'
server_uri = serializer.validated_data["AUTH_LDAP_SERVER_URI"]
bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"]
password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"]
use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False)
search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
auth_ldap = serializer.validated_data.get('AUTH_LDAP', False)
if not password:
password = settings.AUTH_LDAP_BIND_PASSWORD
config = { config = {
'server_uri': server_uri, 'server_uri': serializer.validated_data.get(f"{prefix}SERVER_URI"),
'bind_dn': bind_dn, 'bind_dn': serializer.validated_data.get(f"{prefix}BIND_DN"),
'password': password, 'password': (serializer.validated_data.get(f"{prefix}BIND_PASSWORD") or
'use_ssl': use_ssl, getattr(settings, f"{prefix}BIND_PASSWORD")),
'search_ou': search_ou, 'use_ssl': serializer.validated_data.get(f"{prefix}START_TLS", False),
'search_filter': search_filter, 'search_ou': serializer.validated_data.get(f"{prefix}SEARCH_OU"),
'attr_map': attr_map, 'search_filter': serializer.validated_data.get(f"{prefix}SEARCH_FILTER"),
'auth_ldap': auth_ldap 'attr_map': serializer.validated_data.get(f"{prefix}USER_ATTR_MAP"),
'auth_ldap': serializer.validated_data.get(f"{prefix.rstrip('_')}", False)
} }
return config return config
@staticmethod @staticmethod
@ -160,7 +156,10 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
cache.set(task_key, TASK_STATUS_IS_OVER, ttl) cache.set(task_key, TASK_STATUS_IS_OVER, ttl)
def run_testing_config(self, data): def run_testing_config(self, data):
serializer = LDAPTestConfigSerializer(data=data) if self.category == 'ldap':
serializer = LDAPTestConfigSerializer(data=data)
else:
serializer = LDAPHATestConfigSerializer(data=data)
if not serializer.is_valid(): if not serializer.is_valid():
self.send_msg(msg=f'error: {str(serializer.errors)}') self.send_msg(msg=f'error: {str(serializer.errors)}')
config = self.get_ldap_config(serializer) config = self.get_ldap_config(serializer)
@ -175,14 +174,13 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
self.send_msg(msg=f'error: {str(serializer.errors)}') self.send_msg(msg=f'error: {str(serializer.errors)}')
username = serializer.validated_data['username'] username = serializer.validated_data['username']
password = serializer.validated_data['password'] password = serializer.validated_data['password']
ok, msg = LDAPTestUtil().test_login(username, password) ok, msg = LDAPTestUtil(category=self.category).test_login(username, password)
return ok, msg return ok, msg
@staticmethod def run_sync_user(self, data):
def run_sync_user(data): sync_util = LDAPSyncUtil(category=self.category)
sync_util = LDAPSyncUtil()
sync_util.clear_cache() sync_util.clear_cache()
sync_ldap_user() sync_ldap_user(category=self.category)
msg = sync_util.get_task_error_msg() msg = sync_util.get_task_error_msg()
ok = False if msg else True ok = False if msg else True
return ok, msg return ok, msg
@ -215,7 +213,7 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
return ok, msg return ok, msg
def set_users_status(self, import_users, errors): def set_users_status(self, import_users, errors):
util = LDAPCacheUtil() util = LDAPCacheUtil(category=self.category)
all_users = util.get_users() all_users = util.get_users()
import_usernames = [u['username'] for u in import_users] import_usernames = [u['username'] for u in import_users]
errors_mapper = {k: v for err in errors for k, v in err.items()} errors_mapper = {k: v for err in errors for k, v in err.items()}
@ -225,7 +223,7 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
user['status'] = {'error': errors_mapper[username]} user['status'] = {'error': errors_mapper[username]}
elif username in import_usernames: elif username in import_usernames:
user['status'] = ImportStatus.ok user['status'] = ImportStatus.ok
LDAPCacheUtil().set_users(all_users) LDAPCacheUtil(category=self.category).set_users(all_users)
@staticmethod @staticmethod
def get_orgs(org_ids): def get_orgs(org_ids):
@ -235,12 +233,11 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
orgs = [current_org] orgs = [current_org]
return orgs return orgs
@staticmethod def get_ldap_users(self, username_list, cache_police):
def get_ldap_users(username_list, cache_police):
if '*' in username_list: if '*' in username_list:
users = LDAPServerUtil().search() users = LDAPServerUtil(category=self.category).search()
elif cache_police in LDAP_USE_CACHE_FLAGS: elif cache_police in LDAP_USE_CACHE_FLAGS:
users = LDAPCacheUtil().search(search_users=username_list) users = LDAPCacheUtil(category=self.category).search(search_users=username_list)
else: else:
users = LDAPServerUtil().search(search_users=username_list) users = LDAPServerUtil(category=self.category).search(search_users=username_list)
return users return users

View File

@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
class Source(models.TextChoices): class Source(models.TextChoices):
local = "local", _("Local") local = "local", _("Local")
ldap = "ldap", "LDAP/AD" ldap = "ldap", "LDAP/AD"
ldap_ha = "ldap_ha", "LDAP/AD (HA)"
openid = "openid", "OpenID" openid = "openid", "OpenID"
radius = "radius", "Radius" radius = "radius", "Radius"
cas = "cas", "CAS" cas = "cas", "CAS"
@ -55,6 +56,7 @@ class SourceMixin:
mapper = { mapper = {
cls.Source.local: True, cls.Source.local: True,
cls.Source.ldap: settings.AUTH_LDAP, cls.Source.ldap: settings.AUTH_LDAP,
cls.Source.ldap_ha: settings.AUTH_LDAP_HA,
cls.Source.openid: settings.AUTH_OPENID, cls.Source.openid: settings.AUTH_OPENID,
cls.Source.radius: settings.AUTH_RADIUS, cls.Source.radius: settings.AUTH_RADIUS,
cls.Source.cas: settings.AUTH_CAS, cls.Source.cas: settings.AUTH_CAS,