diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py
index c99c52bc9..930a1f03d 100644
--- a/apps/audits/signals_handler.py
+++ b/apps/audits/signals_handler.py
@@ -57,6 +57,8 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
+ backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
+ backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
return backend_label_mapping
def _setup(self):
diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py
index 12b83421f..6ef54b09b 100644
--- a/apps/authentication/api/__init__.py
+++ b/apps/authentication/api/__init__.py
@@ -7,3 +7,6 @@ from .mfa import *
from .access_key import *
from .login_confirm import *
from .sso import *
+from .wecom import *
+from .dingtalk import *
+from .password import *
diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py
new file mode 100644
index 000000000..e4b2ea85b
--- /dev/null
+++ b/apps/authentication/api/dingtalk.py
@@ -0,0 +1,35 @@
+from rest_framework.views import APIView
+from rest_framework.request import Request
+from rest_framework.response import Response
+
+from users.permissions import IsAuthPasswdTimeValid
+from users.models import User
+from common.utils import get_logger
+from common.permissions import IsOrgAdmin
+from common.mixins.api import RoleUserMixin, RoleAdminMixin
+from authentication import errors
+
+logger = get_logger(__file__)
+
+
+class DingTalkQRUnBindBase(APIView):
+ user: User
+
+ def post(self, request: Request, **kwargs):
+ user = self.user
+
+ if not user.dingtalk_id:
+ raise errors.DingTalkNotBound
+
+ user.dingtalk_id = ''
+ user.save()
+ return Response()
+
+
+class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
+ permission_classes = (IsAuthPasswdTimeValid,)
+
+
+class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
+ user_id_url_kwarg = 'user_id'
+ permission_classes = (IsOrgAdmin,)
diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py
new file mode 100644
index 000000000..af8b41358
--- /dev/null
+++ b/apps/authentication/api/password.py
@@ -0,0 +1,26 @@
+from rest_framework.generics import CreateAPIView
+from rest_framework.response import Response
+
+from authentication.serializers import PasswordVerifySerializer
+from common.permissions import IsValidUser
+from authentication.mixins import authenticate
+from authentication.errors import PasswdInvalid
+from authentication.mixins import AuthMixin
+
+
+class UserPasswordVerifyApi(AuthMixin, CreateAPIView):
+ permission_classes = (IsValidUser,)
+ serializer_class = PasswordVerifySerializer
+
+ def create(self, request, *args, **kwargs):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ password = serializer.validated_data['password']
+ user = self.request.user
+
+ user = authenticate(request=request, username=user.username, password=password)
+ if not user:
+ raise PasswdInvalid
+
+ self.set_passwd_verify_on_session(user)
+ return Response()
diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py
new file mode 100644
index 000000000..1ab5ff725
--- /dev/null
+++ b/apps/authentication/api/wecom.py
@@ -0,0 +1,35 @@
+from rest_framework.views import APIView
+from rest_framework.request import Request
+from rest_framework.response import Response
+
+from users.permissions import IsAuthPasswdTimeValid
+from users.models import User
+from common.utils import get_logger
+from common.permissions import IsOrgAdmin
+from common.mixins.api import RoleUserMixin, RoleAdminMixin
+from authentication import errors
+
+logger = get_logger(__file__)
+
+
+class WeComQRUnBindBase(APIView):
+ user: User
+
+ def post(self, request: Request, **kwargs):
+ user = self.user
+
+ if not user.wecom_id:
+ raise errors.WeComNotBound
+
+ user.wecom_id = ''
+ user.save()
+ return Response()
+
+
+class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
+ permission_classes = (IsAuthPasswdTimeValid,)
+
+
+class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
+ user_id_url_kwarg = 'user_id'
+ permission_classes = (IsOrgAdmin,)
diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py
index 1fd315abb..63356eff6 100644
--- a/apps/authentication/backends/api.py
+++ b/apps/authentication/backends/api.py
@@ -205,3 +205,21 @@ class SSOAuthentication(ModelBackend):
def authenticate(self, request, sso_token=None, **kwargs):
pass
+
+
+class WeComAuthentication(ModelBackend):
+ """
+ 什么也不做呀😺
+ """
+
+ def authenticate(self, request, **kwargs):
+ pass
+
+
+class DingTalkAuthentication(ModelBackend):
+ """
+ 什么也不做呀😺
+ """
+
+ def authenticate(self, request, **kwargs):
+ pass
diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py
index 03368baa8..bcd83c97d 100644
--- a/apps/authentication/errors.py
+++ b/apps/authentication/errors.py
@@ -19,6 +19,7 @@ reason_user_inactive = 'user_inactive'
reason_user_expired = 'user_expired'
reason_backend_not_match = 'backend_not_match'
reason_acl_not_allow = 'acl_not_allow'
+only_local_users_are_allowed = 'only_local_users_are_allowed'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
@@ -32,6 +33,7 @@ reason_choices = {
reason_user_expired: _("This account is expired"),
reason_backend_not_match: _("Auth backend not match"),
reason_acl_not_allow: _("ACL is not allowed"),
+ only_local_users_are_allowed: _("Only local users are allowed")
}
old_reason_choices = {
'0': '-',
@@ -291,3 +293,28 @@ class PasswordRequireResetError(JMSException):
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.url = url
+
+
+class WeComCodeInvalid(JMSException):
+ default_code = 'wecom_code_invalid'
+ default_detail = 'Code invalid, can not get user info'
+
+
+class WeComBindAlready(JMSException):
+ default_code = 'wecom_bind_already'
+ default_detail = 'WeCom already binded'
+
+
+class WeComNotBound(JMSException):
+ default_code = 'wecom_not_bound'
+ default_detail = 'WeCom is not bound'
+
+
+class DingTalkNotBound(JMSException):
+ default_code = 'dingtalk_not_bound'
+ default_detail = 'DingTalk is not bound'
+
+
+class PasswdInvalid(JMSException):
+ default_code = 'passwd_invalid'
+ default_detail = _('Your password is invalid')
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 9b81d17fe..5eeceb7c3 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -5,6 +5,7 @@ from urllib.parse import urlencode
from functools import partial
import time
+from django.core.cache import cache
from django.conf import settings
from django.contrib import auth
from django.utils.translation import ugettext as _
@@ -12,7 +13,7 @@ from django.contrib.auth import (
BACKEND_SESSION_KEY, _get_backends,
PermissionDenied, user_login_failed, _clean_credentials
)
-from django.shortcuts import reverse
+from django.shortcuts import reverse, redirect
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
from users.models import User
@@ -82,6 +83,8 @@ class AuthMixin:
request = None
partial_credential_error = None
+ key_prefix_captcha = "_LOGIN_INVALID_{}"
+
def get_user_from_session(self):
if self.request.session.is_empty():
raise errors.SessionEmptyError()
@@ -110,11 +113,7 @@ class AuthMixin:
ip = ip or get_request_ip(self.request)
return ip
- def check_is_block(self, raise_exception=True):
- if hasattr(self.request, 'data'):
- username = self.request.data.get("username")
- else:
- username = self.request.POST.get("username")
+ def _check_is_block(self, username, raise_exception=True):
ip = self.get_request_ip()
if LoginBlockUtil(username, ip).is_block():
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
@@ -124,6 +123,13 @@ class AuthMixin:
else:
return exception
+ def check_is_block(self, raise_exception=True):
+ if hasattr(self.request, 'data'):
+ username = self.request.data.get("username")
+ else:
+ username = self.request.POST.get("username")
+ self._check_is_block(username, raise_exception)
+
def decrypt_passwd(self, raw_passwd):
# 获取解密密钥,对密码进行解密
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
@@ -140,6 +146,9 @@ class AuthMixin:
def raise_credential_error(self, error):
raise self.partial_credential_error(error=error)
+ def _set_partial_credential_error(self, username, ip, request):
+ self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
+
def get_auth_data(self, decrypt_passwd=False):
request = self.request
if hasattr(request, 'data'):
@@ -151,7 +160,7 @@ class AuthMixin:
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
password = password + challenge.strip()
ip = self.get_request_ip()
- self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
+ self._set_partial_credential_error(username=username, ip=ip, request=request)
if decrypt_passwd:
password = self.decrypt_passwd(password)
@@ -184,6 +193,21 @@ class AuthMixin:
if not is_allowed:
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
+ def set_login_failed_mark(self):
+ ip = self.get_request_ip()
+ cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
+
+ def set_passwd_verify_on_session(self, user: User):
+ self.request.session['user_id'] = str(user.id)
+ self.request.session['auth_password'] = 1
+ self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
+
+ def check_is_need_captcha(self):
+ # 最近有登录失败时需要填写验证码
+ ip = get_request_ip(self.request)
+ need = cache.get(self.key_prefix_captcha.format(ip))
+ return need
+
def check_user_auth(self, decrypt_passwd=False):
self.check_is_block()
request = self.request
@@ -204,6 +228,27 @@ class AuthMixin:
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
return user
+ def _check_is_local_user(self, user: User):
+ if user.source != User.Source.local:
+ raise self.raise_credential_error(error=errors.only_local_users_are_allowed)
+
+ def check_oauth2_auth(self, user: User, auth_backend):
+ ip = self.get_request_ip()
+ request = self.request
+
+ self._set_partial_credential_error(user.username, ip, request)
+ self._check_is_local_user(user)
+ self._check_is_block(user.username)
+ self._check_login_acl(user, ip)
+
+ LoginBlockUtil(user.username, ip).clean_failed_count()
+ MFABlockUtils(user.username, ip).clean_failed_count()
+
+ request.session['auth_password'] = 1
+ request.session['user_id'] = str(user.id)
+ request.session['auth_backend'] = auth_backend
+ return user
+
@classmethod
def generate_reset_password_url_with_flash_msg(cls, user, message):
reset_passwd_url = reverse('authentication:reset-password')
@@ -354,3 +399,10 @@ class AuthMixin:
sender=self.__class__, username=username,
request=self.request, reason=reason
)
+
+ def redirect_to_guard_view(self):
+ guard_url = reverse('authentication:login-guard')
+ args = self.request.META.get('QUERY_STRING', '')
+ if args:
+ guard_url = "%s?%s" % (guard_url, args)
+ return redirect(guard_url)
diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py
index 5f2bc231b..72b54e3ee 100644
--- a/apps/authentication/serializers.py
+++ b/apps/authentication/serializers.py
@@ -10,13 +10,14 @@ from applications.models import Application
from users.serializers import UserProfileSerializer
from assets.serializers import ProtocolsField
from perms.serializers.asset.permission import ActionsField
-from .models import AccessKey, LoginConfirmSetting, SSOToken
+from .models import AccessKey, LoginConfirmSetting
__all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
- 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer'
+ 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer',
+ 'PasswordVerifySerializer',
]
@@ -31,6 +32,10 @@ class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6)
+class PasswordVerifySerializer(serializers.Serializer):
+ password = serializers.CharField()
+
+
class BearerTokenSerializer(serializers.Serializer):
username = serializers.CharField(allow_null=True, required=False, write_only=True)
password = serializers.CharField(write_only=True, allow_null=True,
diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html
index 722ef5d16..0104d0e44 100644
--- a/apps/authentication/templates/authentication/login.html
+++ b/apps/authentication/templates/authentication/login.html
@@ -117,6 +117,15 @@
float: right;
margin: 10px 10px 0 0;
}
+ .more-login-item {
+ border-right: 1px dashed #dedede;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ .more-login-item:last-child {
+ border: none;
+ }
@@ -182,10 +191,10 @@
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运"
"div>"
+#: notifications/models.py:17 users/forms/profile.py:101
+#: users/models/user.py:557
+msgid "Email"
+msgstr "邮件"
+
+#: notifications/models.py:67 templates/_nav.html:110 terminal/apps.py:9
+#: terminal/serializers/session.py:40
+msgid "Terminal"
+msgstr "终端"
+
+#: notifications/models.py:68 ops/apps.py:9
+msgid "Operations"
+msgstr "运维"
+
#: ops/api/celery.py:61 ops/api/celery.py:76
msgid "Waiting task start"
msgstr "等待任务开始"
@@ -1811,7 +1904,7 @@ msgid "Time"
msgstr "时间"
#: ops/models/adhoc.py:246 ops/models/command.py:28
-#: terminal/serializers/session.py:38
+#: terminal/serializers/session.py:41
msgid "Is finished"
msgstr "是否完成"
@@ -1843,10 +1936,18 @@ msgstr "任务开始"
msgid "Command `{}` is forbidden ........"
msgstr "命令 `{}` 不允许被执行 ......."
-#: ops/models/command.py:113
+#: ops/models/command.py:115
msgid "Task end"
msgstr "任务结束"
+#: ops/notifications.py:10
+msgid "Server performance"
+msgstr "服务器性能"
+
+#: ops/notifications.py:17
+msgid "Disk used more than 80%: {} => {}"
+msgstr "磁盘使用率超过 80%: {} => {}"
+
#: ops/tasks.py:71
msgid "Clean task history period"
msgstr "定期清除任务历史"
@@ -1863,10 +1964,6 @@ msgstr "任务列表"
msgid "Update task content: {}"
msgstr "更新任务内容: {}"
-#: ops/utils.py:74
-msgid "Disk used more than 80%: {} => {}"
-msgstr "磁盘使用率超过 80%: {} => {}"
-
#: orgs/api.py:76
#, python-brace-format
msgid "Have `{model._meta.verbose_name}` exists, Please delete"
@@ -1877,8 +1974,8 @@ msgid "The current organization cannot be deleted"
msgstr "当前组织不能被删除"
#: orgs/mixins/models.py:45 orgs/mixins/serializers.py:25 orgs/models.py:36
-#: orgs/models.py:417 orgs/serializers.py:101
-#: tickets/serializers/ticket/ticket.py:81
+#: orgs/models.py:417 orgs/serializers.py:108
+#: tickets/serializers/ticket/ticket.py:83
msgid "Organization"
msgstr "组织"
@@ -1948,7 +2045,7 @@ msgid "Clipboard copy paste"
msgstr "剪贴板复制粘贴"
#: perms/models/asset_permission.py:102
-#: perms/serializers/asset/permission.py:69
+#: perms/serializers/asset/permission.py:71
msgid "Actions"
msgstr "动作"
@@ -1989,49 +2086,41 @@ msgid ""
"permission type. ({})"
msgstr "应用列表中包含与授权类型不同的应用。({})"
-#: perms/serializers/asset/permission.py:43
-#: perms/serializers/asset/permission.py:67 users/serializers/user.py:34
-#: users/serializers/user.py:70
+#: perms/serializers/asset/permission.py:45
+#: perms/serializers/asset/permission.py:69 users/serializers/user.py:34
+#: users/serializers/user.py:82
msgid "Is expired"
msgstr "是否过期"
-#: perms/serializers/asset/permission.py:44
-#, fuzzy
-#| msgid "Username"
+#: perms/serializers/asset/permission.py:46
msgid "Users name"
msgstr "用户名"
-#: perms/serializers/asset/permission.py:45
-#, fuzzy
-#| msgid "User groups amount"
+#: perms/serializers/asset/permission.py:47
msgid "User groups name"
msgstr "用户组数量"
-#: perms/serializers/asset/permission.py:46
-#, fuzzy
-#| msgid "Asset num"
-msgid "Assets name"
-msgstr "资产数量"
-
#: perms/serializers/asset/permission.py:48
-#, fuzzy
-#| msgid "System users amount"
-msgid "System users name"
-msgstr "系统用户数量"
+msgid "Assets name"
+msgstr "资产名字"
-#: perms/serializers/asset/permission.py:68 users/serializers/user.py:69
+#: perms/serializers/asset/permission.py:50
+msgid "System users name"
+msgstr "系统用户名字"
+
+#: perms/serializers/asset/permission.py:70 users/serializers/user.py:81
msgid "Is valid"
msgstr "账户是否有效"
-#: perms/serializers/asset/permission.py:70 users/serializers/group.py:36
+#: perms/serializers/asset/permission.py:72 users/serializers/group.py:36
msgid "Users amount"
msgstr "用户数量"
-#: perms/serializers/asset/permission.py:71
+#: perms/serializers/asset/permission.py:73
msgid "User groups amount"
msgstr "用户组数量"
-#: perms/serializers/asset/permission.py:74
+#: perms/serializers/asset/permission.py:76
msgid "System users amount"
msgstr "系统用户数量"
@@ -2044,6 +2133,10 @@ msgstr "邮件已经发送{}, 请检查"
msgid "Welcome to the JumpServer open source Bastion Host"
msgstr "欢迎使用JumpServer开源堡垒机"
+#: settings/api/dingtalk.py:36 settings/api/wecom.py:36
+msgid "OK"
+msgstr ""
+
#: settings/api/ldap.py:189
msgid "Get ldap users is None"
msgstr "获取 LDAP 用户为 None"
@@ -2379,6 +2472,38 @@ msgstr "邮件收件人"
msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割"
+#: settings/serializers/settings.py:193
+msgid "Corporation ID"
+msgstr "企业 ID(CorpId)"
+
+#: settings/serializers/settings.py:194
+msgid "Agent ID"
+msgstr "应用 ID(AgentId)"
+
+#: settings/serializers/settings.py:195
+msgid "Corporation Secret"
+msgstr "凭证密钥(Secret)"
+
+#: settings/serializers/settings.py:196
+msgid "Enable WeCom Auth"
+msgstr "启用企业微信认证"
+
+#: settings/serializers/settings.py:200
+msgid "AgentId"
+msgstr "应用 ID(AgentId)"
+
+#: settings/serializers/settings.py:201
+msgid "AppKey"
+msgstr "应用 Key(AppKey)"
+
+#: settings/serializers/settings.py:202
+msgid "AppSecret"
+msgstr "应用密文(AppSecret)"
+
+#: settings/serializers/settings.py:203
+msgid "Enable DingTalk Auth"
+msgstr "启用钉钉认证"
+
#: settings/utils/ldap.py:411
msgid "Host or port is disconnected: {}"
msgstr "主机或端口不可连接: {}"
@@ -2685,10 +2810,6 @@ msgstr "Web终端"
msgid "File manager"
msgstr "文件管理"
-#: templates/_nav.html:110 terminal/serializers/session.py:37
-msgid "Terminal"
-msgstr "终端"
-
#: templates/_nav.html:121
msgid "Job Center"
msgstr "作业中心"
@@ -2774,14 +2895,6 @@ msgstr "确认删除"
msgid "Are you sure delete"
msgstr "您确定删除吗?"
-#: templates/flash_message_standalone.html:28
-msgid "Cancel"
-msgstr "取消"
-
-#: templates/flash_message_standalone.html:37
-msgid "Go"
-msgstr "跳转"
-
#: templates/index.html:11
msgid "Total users"
msgstr "用户总数"
@@ -3111,27 +3224,110 @@ msgstr "命令存储"
msgid "Replay storage"
msgstr "录像存储"
-#: terminal/serializers/session.py:30
+#: terminal/notifications.py:29
+msgid "Terminal command alert"
+msgstr "终端命令告警"
+
+#: terminal/notifications.py:38
+#, python-format
+msgid ""
+"\n"
+" Command: %(command)s\n"
+"
\n"
+" Asset: %(host_name)s (%(host_ip)s)\n"
+"
\n"
+" User: %(user)s\n"
+"
\n"
+" Level: %(risk_level)s\n"
+"
\n"
+" Session:
session "
+"detail\n"
+"
\n"
+" "
+msgstr ""
+"\n"
+" 命令: %(command)s\n"
+"
\n"
+" 资产: %(host_name)s (%(host_ip)s)\n"
+"
\n"
+" 用户: %(user)s\n"
+"
\n"
+" 等级: %(risk_level)s\n"
+"
\n"
+" 会话:
会话详情\n"
+"
\n"
+" "
+
+#: terminal/notifications.py:73
+#, python-format
+msgid ""
+"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $"
+"%(command)s"
+msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s"
+
+#: terminal/notifications.py:90
+msgid "Batch command alert"
+msgstr "批量命令告警"
+
+#: terminal/notifications.py:101
+#, python-format
+msgid ""
+"\n"
+"
\n"
+" Assets: %(assets)s\n"
+"
\n"
+" User: %(user)s\n"
+"
\n"
+" Level: %(risk_level)s\n"
+"
\n"
+"\n"
+" ----------------- Commands ---------------- "
+"
\n"
+" %(command)s
\n"
+" ----------------- Commands ---------------- "
+"
\n"
+" "
+msgstr ""
+"\n"
+"
\n"
+" 资产: %(assets)s\n"
+"
\n"
+" 用户: %(user)s\n"
+"
\n"
+" 等级: %(risk_level)s\n"
+"
\n"
+"\n"
+" ----------------- 命令 ----------------
\n"
+" %(command)s
\n"
+" ----------------- 命令 ----------------
\n"
+" "
+
+#: terminal/notifications.py:127
+#, python-format
+msgid "Insecure Web Command Execution Alert: [%(name)s]"
+msgstr "Web页面-> 命令执行 告警: [%(name)s]"
+
+#: terminal/serializers/session.py:33
msgid "User ID"
msgstr "用户 ID"
-#: terminal/serializers/session.py:31
+#: terminal/serializers/session.py:34
msgid "Asset ID"
msgstr "资产 ID"
-#: terminal/serializers/session.py:32
+#: terminal/serializers/session.py:35
msgid "System user ID"
msgstr "系统用户 ID"
-#: terminal/serializers/session.py:33
+#: terminal/serializers/session.py:36
msgid "Login from for display"
msgstr "登录来源(显示名称)"
-#: terminal/serializers/session.py:35
+#: terminal/serializers/session.py:38
msgid "Can replay"
msgstr "是否可重放"
-#: terminal/serializers/session.py:36
+#: terminal/serializers/session.py:39
msgid "Can join"
msgstr "是否可加入"
@@ -3200,82 +3396,10 @@ msgstr "文档类型"
msgid "Ignore Certificate Verification"
msgstr "忽略证书认证"
-#: terminal/serializers/terminal.py:66 terminal/serializers/terminal.py:74
+#: terminal/serializers/terminal.py:73 terminal/serializers/terminal.py:81
msgid "Not found"
msgstr "没有发现"
-#: terminal/utils.py:78
-#, python-format
-msgid ""
-"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $"
-"%(command)s"
-msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s"
-
-#: terminal/utils.py:86
-#, python-format
-msgid ""
-"\n"
-" Command: %(command)s\n"
-"
\n"
-" Asset: %(host_name)s (%(host_ip)s)\n"
-"
\n"
-" User: %(user)s\n"
-"
\n"
-" Level: %(risk_level)s\n"
-"
\n"
-" Session:
session detail\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" 命令: %(command)s\n"
-"
\n"
-" 资产: %(host_name)s (%(host_ip)s)\n"
-"
\n"
-" 用户: %(user)s\n"
-"
\n"
-" 等级: %(risk_level)s\n"
-"
\n"
-" 会话:
会话详情\n"
-"
\n"
-" "
-
-#: terminal/utils.py:113
-#, python-format
-msgid "Insecure Web Command Execution Alert: [%(name)s]"
-msgstr "Web页面-> 命令执行 告警: [%(name)s]"
-
-#: terminal/utils.py:121
-#, python-format
-msgid ""
-"\n"
-"
\n"
-" Assets: %(assets)s\n"
-"
\n"
-" User: %(user)s\n"
-"
\n"
-" Level: %(risk_level)s\n"
-"
\n"
-"\n"
-" ----------------- Commands ----------------
\n"
-" %(command)s
\n"
-" ----------------- Commands ----------------
\n"
-" "
-msgstr ""
-"\n"
-"
\n"
-" 资产: %(assets)s\n"
-"
\n"
-" 用户: %(user)s\n"
-"
\n"
-" 等级: %(risk_level)s\n"
-"
\n"
-"\n"
-" ----------------- 命令 ----------------
\n"
-" %(command)s
\n"
-" ----------------- 命令 ----------------
\n"
-" "
-
#: tickets/const.py:8
msgid "General"
msgstr "一般"
@@ -3636,13 +3760,13 @@ msgstr "动作 (显示名称)"
msgid "Status display"
msgstr "状态(显示名称)"
-#: tickets/serializers/ticket/ticket.py:99
+#: tickets/serializers/ticket/ticket.py:101
msgid ""
"The `type` in the submission data (`{}`) is different from the type in the "
"request url (`{}`)"
msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致"
-#: tickets/serializers/ticket/ticket.py:120
+#: tickets/serializers/ticket/ticket.py:122
msgid "None of the assignees belong to Organization `{}` admins"
msgstr "所有受理人都不属于组织 `{}` 下的管理员"
@@ -3712,10 +3836,6 @@ msgstr "确认密码"
msgid "Password does not match"
msgstr "密码不一致"
-#: users/forms/profile.py:101 users/models/user.py:557
-msgid "Email"
-msgstr "邮件"
-
#: users/forms/profile.py:108
msgid "Old password"
msgstr "原来密码"
@@ -3781,11 +3901,11 @@ msgstr "用户来源"
msgid "Date password last updated"
msgstr "最后更新密码日期"
-#: users/models/user.py:741
+#: users/models/user.py:751
msgid "Administrator"
msgstr "管理员"
-#: users/models/user.py:744
+#: users/models/user.py:754
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
@@ -3793,7 +3913,7 @@ msgstr "Administrator是初始的超级管理员"
msgid "The old password is incorrect"
msgstr "旧密码错误"
-#: users/serializers/profile.py:36 users/serializers/user.py:113
+#: users/serializers/profile.py:36 users/serializers/user.py:125
msgid "Password does not match security rules"
msgstr "密码不满足安全规则"
@@ -3805,7 +3925,7 @@ msgstr "新密码不能是最近 {} 次的密码"
msgid "The newly set password is inconsistent"
msgstr "两次密码不一致"
-#: users/serializers/profile.py:121 users/serializers/user.py:68
+#: users/serializers/profile.py:121 users/serializers/user.py:80
msgid "Is first login"
msgstr "首次登录"
@@ -3846,35 +3966,35 @@ msgstr "是否可更新"
msgid "Can delete"
msgstr "是否可删除"
-#: users/serializers/user.py:39 users/serializers/user.py:75
+#: users/serializers/user.py:39 users/serializers/user.py:87
msgid "Organization role name"
msgstr "组织角色名称"
-#: users/serializers/user.py:71
+#: users/serializers/user.py:83
msgid "Avatar url"
msgstr "头像路径"
-#: users/serializers/user.py:73
+#: users/serializers/user.py:85
msgid "Groups name"
msgstr "用户组名"
-#: users/serializers/user.py:74
+#: users/serializers/user.py:86
msgid "Source name"
msgstr "用户来源名"
-#: users/serializers/user.py:76
+#: users/serializers/user.py:88
msgid "Super role name"
msgstr "超级角色名称"
-#: users/serializers/user.py:77
+#: users/serializers/user.py:89
msgid "Total role name"
msgstr "汇总角色名称"
-#: users/serializers/user.py:101
+#: users/serializers/user.py:113
msgid "Role limit to {}"
msgstr "角色只能为 {}"
-#: users/serializers/user.py:198
+#: users/serializers/user.py:210
msgid "name not unique"
msgstr "名称重复"
@@ -4925,12 +5045,85 @@ msgstr "旗舰版"
msgid "Community edition"
msgstr "社区版"
-#~ msgid "The administrator require you to change your password this time"
-#~ msgstr "管理员要求您本次修改密码"
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ " Command: %(command)s\n"
+#~ "
\n"
+#~ " Asset: %(host_name)s (%(host_ip)s)\n"
+#~ "
\n"
+#~ " User: %(user)s\n"
+#~ "
\n"
+#~ " Level: %(risk_level)s\n"
+#~ "
\n"
+#~ " Session:
session detail\n"
+#~ "
\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ " 命令: %(command)s\n"
+#~ "
\n"
+#~ " 资产: %(host_name)s (%(host_ip)s)\n"
+#~ "
\n"
+#~ " 用户: %(user)s\n"
+#~ "
\n"
+#~ " 等级: %(risk_level)s\n"
+#~ "
\n"
+#~ " 会话:
会话详情\n"
+#~ "
\n"
+#~ " "
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ "
\n"
+#~ " Assets: %(assets)s\n"
+#~ "
\n"
+#~ " User: %(user)s\n"
+#~ "
\n"
+#~ " Level: %(risk_level)s\n"
+#~ "
\n"
+#~ "\n"
+#~ " ----------------- Commands ----------------
\n"
+#~ " %(command)s
\n"
+#~ " ----------------- Commands ----------------
\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ "
\n"
+#~ " 资产: %(assets)s\n"
+#~ "
\n"
+#~ " 用户: %(user)s\n"
+#~ "
\n"
+#~ " 等级: %(risk_level)s\n"
+#~ "
\n"
+#~ "\n"
+#~ " ----------------- 命令 ----------------
\n"
+#~ " %(command)s
\n"
+#~ " ----------------- 命令 ----------------
\n"
+#~ " "
+
+#~ msgid "Ops"
+#~ msgstr "选项"
+
+#~ msgid "Command Alert"
+#~ msgstr "命令告警"
+
+#~ msgid "Agent Secret"
+#~ msgstr "凭证密钥(secret)"
+
+#~ msgid "APP key"
+#~ msgstr "APPKEY"
+
+#~ msgid "APP secret"
+#~ msgstr "LDAP 地址"
#~ msgid "Auth"
#~ msgstr "认证"
+#~ msgid "The administrator require you to change your password this time"
+#~ msgstr "管理员要求您本次修改密码"
+
#~ msgid "Security and Role"
#~ msgstr "角色安全"
@@ -4985,9 +5178,6 @@ msgstr "社区版"
#~ msgid "Join"
#~ msgstr "加入"
-#~ msgid "Update successfully!"
-#~ msgstr "更新成功"
-
#~ msgid "Goto profile page enable MFA"
#~ msgstr "请去个人信息页面启用自己的多因子认证"
@@ -5000,6 +5190,9 @@ msgstr "社区版"
#~ msgid "This will reset the user password and send a reset mail"
#~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱"
+#~ msgid "Cancel"
+#~ msgstr "取消"
+
#~ msgid ""
#~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform "
#~ "the user to update his new ssh public key."
diff --git a/apps/ops/apps.py b/apps/ops/apps.py
index 01dfd05fa..8bdc04ce8 100644
--- a/apps/ops/apps.py
+++ b/apps/ops/apps.py
@@ -1,10 +1,12 @@
from __future__ import unicode_literals
+from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class OpsConfig(AppConfig):
name = 'ops'
+ verbose_name = _('Operations')
def ready(self):
from orgs.models import Organization
diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py
index f59c4cb47..6beecd5cb 100644
--- a/apps/orgs/signals_handler/common.py
+++ b/apps/orgs/signals_handler/common.py
@@ -8,7 +8,6 @@ from django.dispatch import receiver
from django.utils.functional import LazyObject
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save, post_delete, pre_delete
-from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org
from orgs.models import Organization, OrganizationMember
@@ -19,7 +18,6 @@ from common.const.signals import PRE_REMOVE, POST_REMOVE
from common.signals import django_ready
from common.utils import get_logger
from common.utils.connection import RedisPubSub
-from common.exceptions import JMSException
logger = get_logger(__file__)
diff --git a/apps/perms/api/asset/user_permission/mixin.py b/apps/perms/api/asset/user_permission/mixin.py
index 3b8733b3b..c0f25b8a4 100644
--- a/apps/perms/api/asset/user_permission/mixin.py
+++ b/apps/perms/api/asset/user_permission/mixin.py
@@ -3,8 +3,9 @@
from rest_framework.request import Request
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
-from common.utils import lazyproperty
from common.http import is_true
+from common.mixins.api import RoleAdminMixin as _RoleAdminMixin
+from common.mixins.api import RoleUserMixin as _RoleUserMixin
from orgs.utils import tmp_to_root_org
from users.models import User
from perms.utils.asset.user_permission import UserGrantedTreeRefreshController
@@ -20,24 +21,13 @@ class PermBaseMixin:
return super().get(request, *args, **kwargs)
-class RoleAdminMixin(PermBaseMixin):
+class RoleAdminMixin(PermBaseMixin, _RoleAdminMixin):
permission_classes = (IsOrgAdminOrAppUser,)
- kwargs: dict
-
- @lazyproperty
- def user(self):
- user_id = self.kwargs.get('pk')
- return User.objects.get(id=user_id)
-class RoleUserMixin(PermBaseMixin):
+class RoleUserMixin(PermBaseMixin, _RoleUserMixin):
permission_classes = (IsValidUser,)
- request: Request
def get(self, request, *args, **kwargs):
with tmp_to_root_org():
return super().get(request, *args, **kwargs)
-
- @lazyproperty
- def user(self):
- return self.request.user
diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py
index 151617be5..39e009ed5 100644
--- a/apps/settings/api/__init__.py
+++ b/apps/settings/api/__init__.py
@@ -1,2 +1,4 @@
from .common import *
from .ldap import *
+from .wecom import *
+from .dingtalk import *
diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py
index 1fa578c50..bb3107dfd 100644
--- a/apps/settings/api/common.py
+++ b/apps/settings/api/common.py
@@ -125,7 +125,9 @@ class PublicSettingApi(generics.RetrieveAPIView):
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
- }
+ },
+ "AUTH_WECOM": settings.AUTH_WECOM,
+ "AUTH_DINGTALK": settings.AUTH_DINGTALK,
}
}
return instance
@@ -141,6 +143,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'ldap': serializers.LDAPSettingSerializer,
'email': serializers.EmailSettingSerializer,
'email_content': serializers.EmailContentSettingSerializer,
+ 'wecom': serializers.WeComSettingSerializer,
+ 'dingtalk': serializers.DingTalkSettingSerializer,
}
def get_serializer_class(self):
@@ -163,6 +167,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
category = self.request.query_params.get('category', '')
for name, value in serializer.validated_data.items():
encrypted = name in encrypted_items
+ if encrypted and value in ['', None]:
+ continue
data.append({
'name': name, 'value': value,
'encrypted': encrypted, 'category': category
diff --git a/apps/settings/api/dingtalk.py b/apps/settings/api/dingtalk.py
new file mode 100644
index 000000000..e560f8626
--- /dev/null
+++ b/apps/settings/api/dingtalk.py
@@ -0,0 +1,38 @@
+import requests
+
+from rest_framework.views import Response
+from rest_framework.generics import GenericAPIView
+from django.utils.translation import gettext_lazy as _
+
+from common.permissions import IsSuperUser
+from common.message.backends.dingtalk import URL
+
+from .. import serializers
+
+
+class DingTalkTestingAPI(GenericAPIView):
+ permission_classes = (IsSuperUser,)
+ serializer_class = serializers.DingTalkSettingSerializer
+
+ def post(self, request):
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY']
+ dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID']
+ dingtalk_appsecret = serializer.validated_data['DINGTALK_APPSECRET']
+
+ try:
+ params = {'appkey': dingtalk_appkey, 'appsecret': dingtalk_appsecret}
+ resp = requests.get(url=URL.GET_TOKEN, params=params)
+ if resp.status_code != 200:
+ return Response(status=400, data={'error': resp.json()})
+
+ data = resp.json()
+ errcode = data['errcode']
+ if errcode != 0:
+ return Response(status=400, data={'error': data['errmsg']})
+
+ return Response(status=200, data={'msg': _('OK')})
+ except Exception as e:
+ return Response(status=400, data={'error': str(e)})
diff --git a/apps/settings/api/wecom.py b/apps/settings/api/wecom.py
new file mode 100644
index 000000000..0fda33c61
--- /dev/null
+++ b/apps/settings/api/wecom.py
@@ -0,0 +1,38 @@
+import requests
+
+from rest_framework.views import Response
+from rest_framework.generics import GenericAPIView
+from django.utils.translation import gettext_lazy as _
+
+from common.permissions import IsSuperUser
+from common.message.backends.wecom import URL
+
+from .. import serializers
+
+
+class WeComTestingAPI(GenericAPIView):
+ permission_classes = (IsSuperUser,)
+ serializer_class = serializers.WeComSettingSerializer
+
+ def post(self, request):
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ wecom_corpid = serializer.validated_data['WECOM_CORPID']
+ wecom_agentid = serializer.validated_data['WECOM_AGENTID']
+ wecom_corpsecret = serializer.validated_data['WECOM_CORPSECRET']
+
+ try:
+ params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret}
+ resp = requests.get(url=URL.GET_TOKEN, params=params)
+ if resp.status_code != 200:
+ return Response(status=400, data={'error': resp.json()})
+
+ data = resp.json()
+ errcode = data['errcode']
+ if errcode != 0:
+ return Response(status=400, data={'error': data['errmsg']})
+
+ return Response(status=200, data={'msg': _('OK')})
+ except Exception as e:
+ return Response(status=400, data={'error': str(e)})
diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py
index b64b95cb6..5d33a1d83 100644
--- a/apps/settings/serializers/settings.py
+++ b/apps/settings/serializers/settings.py
@@ -6,7 +6,7 @@ from rest_framework import serializers
__all__ = [
'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer',
'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer',
- 'SettingsSerializer'
+ 'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer',
]
@@ -189,13 +189,29 @@ class SecuritySettingSerializer(serializers.Serializer):
)
+class WeComSettingSerializer(serializers.Serializer):
+ WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID'))
+ WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID"))
+ WECOM_CORPSECRET = serializers.CharField(max_length=256, required=False, label=_("Corporation Secret"), write_only=True)
+ AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth'))
+
+
+class DingTalkSettingSerializer(serializers.Serializer):
+ DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label=_("AgentId"))
+ DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label=_("AppKey"))
+ DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label=_("AppSecret"), write_only=True)
+ AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth'))
+
+
class SettingsSerializer(
BasicSettingSerializer,
EmailSettingSerializer,
EmailContentSettingSerializer,
LDAPSettingSerializer,
TerminalSettingSerializer,
- SecuritySettingSerializer
+ SecuritySettingSerializer,
+ WeComSettingSerializer,
+ DingTalkSettingSerializer,
):
# encrypt_fields 现在使用 write_only 来判断了
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index 0db9c7c54..86dfc6847 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -13,6 +13,8 @@ urlpatterns = [
path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'),
path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'),
path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'),
+ path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
+ path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
diff --git a/apps/static/img/login_dingtalk_log.png b/apps/static/img/login_dingtalk_log.png
new file mode 100644
index 000000000..998f730ad
Binary files /dev/null and b/apps/static/img/login_dingtalk_log.png differ
diff --git a/apps/static/img/login_wecom_log.png b/apps/static/img/login_wecom_log.png
new file mode 100644
index 000000000..d5a58d0ba
Binary files /dev/null and b/apps/static/img/login_wecom_log.png differ
diff --git a/apps/terminal/apps.py b/apps/terminal/apps.py
index 5341369c7..f0cb05bf2 100644
--- a/apps/terminal/apps.py
+++ b/apps/terminal/apps.py
@@ -1,10 +1,12 @@
from __future__ import unicode_literals
+from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class TerminalConfig(AppConfig):
name = 'terminal'
+ verbose_name = _('Terminal')
def ready(self):
from . import signals_handler
diff --git a/apps/users/migrations/0034_auto_20210506_1448.py b/apps/users/migrations/0034_auto_20210506_1448.py
new file mode 100644
index 000000000..df6257064
--- /dev/null
+++ b/apps/users/migrations/0034_auto_20210506_1448.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1 on 2021-05-06 06:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0033_user_need_update_password'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='dingtalk_id',
+ field=models.CharField(default=None, max_length=128, null=True, unique=True),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='wecom_id',
+ field=models.CharField(default=None, max_length=128, null=True, unique=True),
+ ),
+ ]
diff --git a/apps/users/models/user.py b/apps/users/models/user.py
index 344e2b6e6..715f02b9d 100644
--- a/apps/users/models/user.py
+++ b/apps/users/models/user.py
@@ -541,7 +541,10 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
cas = 'cas', 'CAS'
SOURCE_BACKEND_MAPPING = {
- Source.local: [settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY],
+ Source.local: [
+ settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY,
+ settings.AUTH_BACKEND_WECOM, settings.AUTH_BACKEND_DINGTALK,
+ ],
Source.ldap: [settings.AUTH_BACKEND_LDAP],
Source.openid: [settings.AUTH_BACKEND_OIDC_PASSWORD, settings.AUTH_BACKEND_OIDC_CODE],
Source.radius: [settings.AUTH_BACKEND_RADIUS],
@@ -605,10 +608,20 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
verbose_name=_('Date password last updated')
)
need_update_password = models.BooleanField(default=False)
+ wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128)
+ dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128)
def __str__(self):
return '{0.name}({0.username})'.format(self)
+ @property
+ def is_wecom_bound(self):
+ return bool(self.wecom_id)
+
+ @property
+ def is_dingtalk_bound(self):
+ return bool(self.dingtalk_id)
+
def get_absolute_url(self):
return reverse('users:user-detail', args=(self.id,))
diff --git a/apps/users/permissions.py b/apps/users/permissions.py
new file mode 100644
index 000000000..03534d211
--- /dev/null
+++ b/apps/users/permissions.py
@@ -0,0 +1,10 @@
+from rest_framework import permissions
+
+from .utils import is_auth_password_time_valid
+
+
+class IsAuthPasswdTimeValid(permissions.IsAuthenticated):
+
+ def has_permission(self, request, view):
+ return super().has_permission(request, view) \
+ and is_auth_password_time_valid(request.session)
diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py
index 5b2c19d10..d7591360b 100644
--- a/apps/users/serializers/user.py
+++ b/apps/users/serializers/user.py
@@ -55,6 +55,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
'mfa_enabled', 'is_valid', 'is_expired', 'is_active', # 布尔字段
'date_expired', 'date_joined', 'last_login', # 日期字段
'created_by', 'comment', # 通用字段
+ 'is_wecom_bound', 'is_dingtalk_bound',
]
# 包含不太常用的字段,可以没有
fields_verbose = fields_small + [