Browse Source

feat: LDAP HA

pull/14125/head
wangruidong 3 months ago committed by Bryan
parent
commit
c2784c44ad
  1. 126
      apps/authentication/backends/ldap.py
  2. 274
      apps/i18n/core/zh/LC_MESSAGES/django.po
  3. 20
      apps/jumpserver/conf.py
  4. 41
      apps/jumpserver/settings/auth.py
  5. 6
      apps/settings/api/ldap.py
  6. 1
      apps/settings/api/settings.py
  7. 1
      apps/settings/serializers/auth/__init__.py
  8. 1
      apps/settings/serializers/auth/base.py
  9. 94
      apps/settings/serializers/auth/ldap_ha.py
  10. 115
      apps/settings/tasks/ldap.py
  11. 68
      apps/settings/utils/ldap.py
  12. 67
      apps/settings/ws.py
  13. 2
      apps/users/models/user/_source.py

126
apps/authentication/backends/ldap.py

@ -1,6 +1,6 @@
# coding:utf-8
#
import abc
import ldap
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
@ -15,13 +15,16 @@ from .base import JMSBaseAuthBackend
logger = _LDAPConfig.get_logger()
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
@staticmethod
def is_enabled():
return settings.AUTH_LDAP
class LDAPBaseBackend(LDAPBackend):
@abc.abstractmethod
def is_enabled(self):
raise NotImplementedError('is_enabled')
@property
@abc.abstractmethod
def is_user_login_only_in_users(self):
raise NotImplementedError('is_authenticated')
def get_or_build_user(self, username, ldap_user):
"""
@ -56,38 +59,6 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
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):
user = None
try:
@ -111,6 +82,67 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
user = ldap_user.populate_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):
@ -126,13 +158,18 @@ class LDAPUser(_LDAPUser):
configuration in the settings.py file
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 = [
LDAPSearch(
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)
@ -169,7 +206,8 @@ class LDAPUser(_LDAPUser):
else:
value = is_true(value)
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:
if not hasattr(self._user, field):
continue

274
apps/i18n/core/zh/LC_MESSAGES/django.po

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

20
apps/jumpserver/conf.py

@ -288,6 +288,26 @@ class Config(dict):
'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False,
'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 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
'AUTH_OPENID': False,

41
apps/jumpserver/settings/auth.py

@ -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_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 配置参数
# 参考: 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_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
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_CODE = 'authentication.backends.oidc.OIDCAuthCodeBackend'
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
@ -232,7 +271,7 @@ AUTHENTICATION_BACKENDS = [
# 只做权限校验
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_OAUTH2,

6
apps/settings/api/ldap.py

@ -26,12 +26,14 @@ class LDAPUserListApi(generics.ListAPIView):
def get_queryset_from_cache(self):
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
def get_queryset_from_server(self):
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
def get_queryset(self):

1
apps/settings/api/settings.py

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

1
apps/settings/serializers/auth/__init__.py

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

1
apps/settings/serializers/auth/base.py

@ -11,6 +11,7 @@ class AuthSettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('Authentication')
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_OPENID = serializers.BooleanField(required=False, label=_('OPENID Auth'))
AUTH_SAML2 = serializers.BooleanField(default=False, label=_("SAML2 Auth"))

94
apps/settings/serializers/auth/ldap_ha.py

@ -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)

115
apps/settings/tasks/ldap.py

@ -1,5 +1,4 @@
# coding: utf-8
#
import time
from celery import shared_task
from django.conf import settings
@ -16,45 +15,44 @@ from settings.notifications import LDAPImportMessage
from users.models import User
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__)
def sync_ldap_user():
LDAPSyncUtil().perform_sync()
def sync_ldap_user(category='ldap'):
LDAPSyncUtil(category=category).perform_sync()
@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():
def perform_import(category, util_server):
start_time = time.time()
time_start_display = local_now_display()
logger.info("Start import ldap user task")
util_server = LDAPServerUtil()
logger.info(f"Start import {category} ldap user task")
util_import = LDAPImportUtil()
users = util_server.search()
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
else:
# 社区版默认导入Default组织
org_ids = [Organization.DEFAULT_ID]
default_org = Organization.default()
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)
if errors:
logger.error("Imported LDAP users errors: {}".format(errors))
logger.error(f"Imported {category} LDAP users errors: {errors}")
else:
logger.info('Imported {} users successfully'.format(len(users)))
if settings.AUTH_LDAP_SYNC_RECEIVERS:
user_ids = settings.AUTH_LDAP_SYNC_RECEIVERS
logger.info(f"Imported {len(users)} {category} users successfully")
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))
end_time = time.time()
extra_kwargs = {
@ -71,33 +69,84 @@ def import_ldap_user():
@shared_task(
verbose_name=_('Registration periodic import ldap user task'),
verbose_name=_('Periodic import ldap user'),
description=_(
"""
When LDAP auto-sync parameters change, such as Crontab parameters, the LDAP sync task
will be re-registered or updated, and this task will be invoked
When LDAP auto-sync is configured, this task will be invoked to synchronize users
"""
)
)
@after_app_ready_start
def import_ldap_user_periodic(**kwargs):
task_name = 'import_ldap_user_periodic'
interval = kwargs.get('AUTH_LDAP_SYNC_INTERVAL', settings.AUTH_LDAP_SYNC_INTERVAL)
enabled = kwargs.get('AUTH_LDAP_SYNC_IS_PERIODIC', settings.AUTH_LDAP_SYNC_IS_PERIODIC)
crontab = kwargs.get('AUTH_LDAP_SYNC_CRONTAB', settings.AUTH_LDAP_SYNC_CRONTAB)
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:
# 优先使用 crontab
interval = None
interval = None # 优先使用 crontab
tasks = {
task_name: {
'task': import_ldap_user.name,
'task': task_func.name,
'interval': interval,
'crontab': crontab,
'enabled': enabled
}
}
create_or_update_celery_periodic_tasks(tasks)
@shared_task(
verbose_name=_('Registration periodic import ldap user task'),
description=_(
"""
When LDAP auto-sync parameters change, such as Crontab parameters, the LDAP sync task
will be re-registered or updated, and this task will be invoked
"""
)
)
@after_app_ready_start
def import_ldap_user_periodic(**kwargs):
register_periodic_task(
'import_ldap_user_periodic', import_ldap_user,
'AUTH_LDAP_SYNC_INTERVAL', 'AUTH_LDAP_SYNC_IS_PERIODIC',
'AUTH_LDAP_SYNC_CRONTAB', **kwargs
)
@shared_task(
verbose_name=_('Registration periodic import ldap ha user task'),
description=_(
"""
When LDAP HA auto-sync parameters change, such as Crontab parameters, the LDAP HA sync task
will be re-registered or updated, and this task will be invoked
"""
)
)
@after_app_ready_start
def import_ldap_ha_user_periodic(**kwargs):
register_periodic_task(
'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
)

68
apps/settings/utils/ldap.py

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

67
apps/settings/ws.py

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

2
apps/users/models/user/_source.py

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

Loading…
Cancel
Save