Merge pull request #6880 from jumpserver/dev

v2.14.0 rc3
pull/6892/head
Jiangjie.Bai 2021-09-15 21:05:26 +08:00 committed by GitHub
commit 9b60d86ddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 446 additions and 252 deletions

9
SECURITY.md Normal file
View File

@ -0,0 +1,9 @@
# 安全说明
JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装.
如果你发现安全问题,请直接联系我们,我们携手让世界更好:
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755

View File

@ -9,12 +9,11 @@ from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..models import LoginAssetACL from ..models import LoginAssetACL
from .. import serializers from .. import serializers
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI'] __all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
class LoginAssetCheckAPI(CreateAPIView): class LoginAssetCheckAPI(CreateAPIView):
permission_classes = (IsAppUser, ) permission_classes = (IsAppUser,)
serializer_class = serializers.LoginAssetCheckSerializer serializer_class = serializers.LoginAssetCheckSerializer
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -57,11 +56,12 @@ class LoginAssetCheckAPI(CreateAPIView):
external=True, api_to_ui=True external=True, api_to_ui=True
) )
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
data = { data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
'ticket_detail_url': ticket_detail_url, 'ticket_detail_url': ticket_detail_url,
'reviewers': [str(user) for user in ticket.current_node.first().ticket_assignees.all()], 'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees]
} }
return data return data
@ -74,4 +74,3 @@ class LoginAssetCheckAPI(CreateAPIView):
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI): class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass pass

View File

@ -13,7 +13,6 @@ from ..hands import IsOrgAdmin, IsAppUser
from ..models import CommandFilter, CommandFilterRule from ..models import CommandFilter, CommandFilterRule
from .. import serializers from .. import serializers
__all__ = [ __all__ = [
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI', 'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
'CommandConfirmStatusAPI' 'CommandConfirmStatusAPI'
@ -44,7 +43,7 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
class CommandConfirmAPI(CreateAPIView): class CommandConfirmAPI(CreateAPIView):
permission_classes = (IsAppUser, ) permission_classes = (IsAppUser,)
serializer_class = serializers.CommandConfirmSerializer serializer_class = serializers.CommandConfirmSerializer
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -73,11 +72,12 @@ class CommandConfirmAPI(CreateAPIView):
external=True, api_to_ui=True external=True, api_to_ui=True
) )
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
return { return {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
'ticket_detail_url': ticket_detail_url, 'ticket_detail_url': ticket_detail_url,
'reviewers': [str(user) for user in ticket.current_node.first().ticket_assignees.all()] 'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees]
} }
@lazyproperty @lazyproperty
@ -89,4 +89,3 @@ class CommandConfirmAPI(CreateAPIView):
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI): class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass pass

View File

@ -18,7 +18,7 @@ def migrate_old_authbook_to_history(apps, schema_editor):
print() print()
while True: while True:
authbooks = authbook_model.objects.using(db_alias).filter(is_latest=False)[:20] authbooks = authbook_model.objects.using(db_alias).filter(is_latest=False)[:1000]
if not authbooks: if not authbooks:
break break
historys = [] historys = []

View File

@ -5,9 +5,12 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from common.utils import lazyproperty from common.utils import lazyproperty, get_logger
from .base import BaseUser, AbsConnectivity from .base import BaseUser, AbsConnectivity
logger = get_logger(__name__)
__all__ = ['AuthBook'] __all__ = ['AuthBook']
@ -93,6 +96,24 @@ class AuthBook(BaseUser, AbsConnectivity):
i.comment = 'Update triggered by account {}'.format(self.id) i.comment = 'Update triggered by account {}'.format(self.id)
i.save(update_fields=['password', 'private_key', 'public_key']) i.save(update_fields=['password', 'private_key', 'public_key'])
def remove_asset_admin_user_if_need(self):
if not self.asset or not self.asset.admin_user:
return
if not self.systemuser.is_admin_user:
return
logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser))
self.asset.admin_user = None
self.asset.save()
def update_asset_admin_user_if_need(self):
if not self.systemuser or not self.systemuser.is_admin_user:
return
if not self.asset or self.asset.admin_user == self.systemuser:
return
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
self.asset.admin_user = self.systemuser
self.asset.save()
def __str__(self): def __str__(self):
return self.smart_name return self.smart_name

View File

@ -75,10 +75,13 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols'] fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols']
fields_small = fields_mini + [ fields_small = fields_mini + [
'protocol', 'port', 'protocols', 'is_active', 'public_ip', 'protocol', 'port', 'protocols', 'is_active', 'public_ip',
'comment',
]
hardware_fields = [
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw', 'comment', 'os', 'os_version', 'os_arch', 'hostname_raw', 'hardware_info',
'hardware_info', 'connectivity', 'date_verified' 'connectivity', 'date_verified'
] ]
fields_fk = [ fields_fk = [
'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display' 'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display'
@ -89,15 +92,16 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
read_only_fields = [ read_only_fields = [
'created_by', 'date_created', 'created_by', 'date_created',
] ]
fields = fields_small + fields_fk + fields_m2m + read_only_fields fields = fields_small + hardware_fields + fields_fk + fields_m2m + read_only_fields
extra_kwargs = { extra_kwargs = {k: {'read_only': True} for k in hardware_fields}
extra_kwargs.update({
'protocol': {'write_only': True}, 'protocol': {'write_only': True},
'port': {'write_only': True}, 'port': {'write_only': True},
'hardware_info': {'label': _('Hardware info')}, 'hardware_info': {'label': _('Hardware info'), 'read_only': True},
'org_name': {'label': _('Org name')}, 'org_name': {'label': _('Org name'), 'read_only': True},
'admin_user_display': {'label': _('Admin user display')} 'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
} })
def get_fields(self): def get_fields(self):
fields = super().get_fields() fields = super().get_fields()

View File

@ -38,7 +38,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'username_same_with_user', 'auto_push', 'auto_generate_key', 'username_same_with_user', 'auto_push', 'auto_generate_key',
'date_created', 'date_updated', 'comment', 'created_by', 'date_created', 'date_updated', 'comment', 'created_by',
] ]
fields_m2m = ['cmd_filters', 'assets_amount'] fields_m2m = ['cmd_filters', 'assets_amount', 'nodes']
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
extra_kwargs = { extra_kwargs = {
'password': { 'password': {

View File

@ -1,7 +1,7 @@
from django.dispatch import receiver from django.dispatch import receiver
from django.apps import apps from django.apps import apps
from simple_history.signals import pre_create_historical_record from simple_history.signals import pre_create_historical_record
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save, post_delete
from common.utils import get_logger from common.utils import get_logger
from ..models import AuthBook, SystemUser from ..models import AuthBook, SystemUser
@ -28,10 +28,15 @@ def pre_create_historical_record_callback(sender, history_instance=None, **kwarg
setattr(history_instance, attr, system_user_attr_value) setattr(history_instance, attr, system_user_attr_value)
@receiver(post_delete, sender=AuthBook)
def on_authbook_post_delete(sender, instance, **kwargs):
instance.remove_asset_admin_user_if_need()
@receiver(post_save, sender=AuthBook) @receiver(post_save, sender=AuthBook)
def on_authbook_post_create(sender, instance, **kwargs): def on_authbook_post_create(sender, instance, **kwargs):
if not instance.systemuser: instance.sync_to_system_user_account()
instance.sync_to_system_user_account() instance.update_asset_admin_user_if_need()
@receiver(pre_save, sender=AuthBook) @receiver(pre_save, sender=AuthBook)

View File

@ -23,6 +23,7 @@ from common.drf.api import SerializerMixin
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true from common.http import is_true
from assets.models import SystemUser
from ..serializers import ( from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@ -88,6 +89,9 @@ class ClientProtocolMixin:
drives_redirect = is_true(self.request.query_params.get('drives_redirect')) drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token = self.create_token(user, asset, application, system_user) token = self.create_token(user, asset, application, system_user)
if system_user.login_mode == SystemUser.LOGIN_MANUAL:
options['prompt for credentials on client:i'] = '1'
if drives_redirect: if drives_redirect:
options['drivestoredirect:s'] = '*' options['drivestoredirect:s'] = '*'
options['screen mode id:i'] = '2' if full_screen else '1' options['screen mode id:i'] = '2' if full_screen else '1'

View File

@ -30,8 +30,8 @@ logger = get_logger(__name__)
def check_backend_can_auth(username, backend_path, allowed_auth_backends): def check_backend_can_auth(username, backend_path, allowed_auth_backends):
if allowed_auth_backends is not None and backend_path not in allowed_auth_backends: if allowed_auth_backends is not None and backend_path not in allowed_auth_backends:
logger.debug('Skip user auth backend: {}, {} not in'.format( logger.debug('Skip user auth backend: {}, {} not in'.format(
username, backend_path, ','.join(allowed_auth_backends) username, backend_path, ','.join(allowed_auth_backends)
) )
) )
return False return False
return True return True
@ -201,7 +201,7 @@ class AuthMixin(PasswordEncryptionViewMixin):
data = request.POST data = request.POST
items = ['username', 'password', 'challenge', 'public_key', 'auto_login'] items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='') username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
ip = self.get_request_ip() ip = self.get_request_ip()
self._set_partial_credential_error(username=username, ip=ip, request=request) self._set_partial_credential_error(username=username, ip=ip, request=request)
@ -285,7 +285,6 @@ class AuthMixin(PasswordEncryptionViewMixin):
elif not user.is_active: elif not user.is_active:
self.raise_credential_error(errors.reason_user_inactive) self.raise_credential_error(errors.reason_user_inactive)
self._check_is_local_user(user)
self._check_is_block(user.username) self._check_is_block(user.username)
self._check_login_acl(user, ip) self._check_login_acl(user, ip)
@ -363,6 +362,12 @@ class AuthMixin(PasswordEncryptionViewMixin):
self.request.session['auth_mfa_required'] = '' self.request.session['auth_mfa_required'] = ''
self.request.session['auth_mfa_type'] = mfa_type self.request.session['auth_mfa_type'] = mfa_type
def clean_mfa_mark(self):
self.request.session['auth_mfa'] = ''
self.request.session['auth_mfa_time'] = ''
self.request.session['auth_mfa_required'] = ''
self.request.session['auth_mfa_type'] = ''
def check_mfa_is_block(self, username, ip, raise_exception=True): def check_mfa_is_block(self, username, ip, raise_exception=True):
if MFABlockUtils(username, ip).is_block(): if MFABlockUtils(username, ip).is_block():
logger.warn('Ip was blocked' + ': ' + username + ':' + ip) logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
@ -414,10 +419,12 @@ class AuthMixin(PasswordEncryptionViewMixin):
self.request.session["auth_confirm"] = "1" self.request.session["auth_confirm"] = "1"
return return
elif ticket.state_reject: elif ticket.state_reject:
self.clean_mfa_mark()
raise errors.LoginConfirmOtherError( raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display() ticket.id, ticket.get_state_display()
) )
elif ticket.state_close: elif ticket.state_close:
self.clean_mfa_mark()
raise errors.LoginConfirmOtherError( raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display() ticket.id, ticket.get_state_display()
) )

View File

@ -134,6 +134,7 @@ function cancelTicket() {
} }
function cancelCloseConfirm() { function cancelCloseConfirm() {
cancelTicket();
window.onbeforeunload = function() {}; window.onbeforeunload = function() {};
window.onunload = function(){}; window.onunload = function(){};
} }

View File

@ -215,7 +215,7 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
user = get_object_or_none(User, dingtalk_id=userid) user = get_object_or_none(User, dingtalk_id=userid)
if user is None: if user is None:
title = _('DingTalk is not bound') title = _('DingTalk is not bound')
msg = _('Please login with a password and then bind the WeCom') msg = _('Please login with a password and then bind the DingTalk')
response = self.get_failed_reponse(login_url, title=title, msg=msg) response = self.get_failed_reponse(login_url, title=title, msg=msg)
return response return response

View File

@ -82,6 +82,15 @@ class TencentSMS(BaseSMSClient):
resp = self.client.SendSms(req) resp = self.client.SendSms(req)
try:
code = resp.SendStatusSet[0].Code
msg = resp.SendStatusSet[0].Message
except IndexError:
raise JMSException(code='response_bad', detail=resp)
if code.lower() != 'ok':
raise JMSException(code=code, detail=msg)
return resp return resp
except TencentCloudSDKException as e: except TencentCloudSDKException as e:
raise JMSException(code=e.code, detail=e.message) raise JMSException(code=e.code, detail=e.message)

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.validators import ( from rest_framework.validators import (
@ -32,3 +34,12 @@ class NoSpecialChars:
raise serializers.ValidationError( raise serializers.ValidationError(
_("Should not contains special characters") _("Should not contains special characters")
) )
class PhoneValidator:
pattern = re.compile(r"^1[356789]\d{9}$")
message = _('The mobile phone number format is incorrect')
def __call__(self, value):
if not self.pattern.match(value):
raise serializers.ValidationError(self.message)

View File

@ -212,12 +212,12 @@ class Config(dict):
# Cas 认证 # Cas 认证
'AUTH_CAS': False, 'AUTH_CAS': False,
'CAS_SERVER_URL': "https://example.com/cas/", 'CAS_SERVER_URL': "https://example.com/cas/",
'CAS_ROOT_PROXIED_AS': '', 'CAS_ROOT_PROXIED_AS': 'https://example.com',
'CAS_LOGOUT_COMPLETELY': True, 'CAS_LOGOUT_COMPLETELY': True,
'CAS_VERSION': 3, 'CAS_VERSION': 3,
'CAS_USERNAME_ATTRIBUTE': 'uid', 'CAS_USERNAME_ATTRIBUTE': 'uid',
'CAS_APPLY_ATTRIBUTES_TO_USER': False, 'CAS_APPLY_ATTRIBUTES_TO_USER': False,
'CAS_RENAME_ATTRIBUTES': {}, 'CAS_RENAME_ATTRIBUTES': {'uid': 'username'},
'CAS_CREATE_USER': True, 'CAS_CREATE_USER': True,
'AUTH_SSO': False, 'AUTH_SSO': False,

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-13 20:32+0800\n" "POT-Creation-Date: 2021-09-15 20:51+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -118,7 +118,7 @@ msgstr "系统用户"
#: acls/models/login_asset_acl.py:22 #: acls/models/login_asset_acl.py:22
#: applications/serializers/attrs/application_category/remote_app.py:37 #: applications/serializers/attrs/application_category/remote_app.py:37
#: assets/models/asset.py:357 assets/models/authbook.py:15 #: assets/models/asset.py:357 assets/models/authbook.py:18
#: assets/models/gathered_user.py:14 assets/serializers/system_user.py:207 #: assets/models/gathered_user.py:14 assets/serializers/system_user.py:207
#: audits/models.py:38 perms/models/asset_permission.py:99 #: audits/models.py:38 perms/models/asset_permission.py:99
#: templates/index.html:82 terminal/backends/command/models.py:19 #: templates/index.html:82 terminal/backends/command/models.py:19
@ -246,7 +246,7 @@ msgstr "远程应用"
msgid "Custom" msgid "Custom"
msgstr "自定义" msgstr "自定义"
#: applications/models/account.py:11 assets/models/authbook.py:16 #: applications/models/account.py:11 assets/models/authbook.py:19
#: assets/models/user.py:281 audits/models.py:39 #: assets/models/user.py:281 audits/models.py:39
#: perms/models/application_permission.py:32 #: perms/models/application_permission.py:32
#: perms/models/asset_permission.py:101 templates/_nav.html:45 #: perms/models/asset_permission.py:101 templates/_nav.html:45
@ -263,8 +263,8 @@ msgstr "自定义"
msgid "System user" msgid "System user"
msgstr "系统用户" msgstr "系统用户"
#: applications/models/account.py:12 assets/models/authbook.py:17 #: applications/models/account.py:12 assets/models/authbook.py:20
#: settings/serializers/auth/cas.py:14 #: settings/serializers/auth/cas.py:15
msgid "Version" msgid "Version"
msgstr "版本" msgstr "版本"
@ -421,7 +421,7 @@ msgstr "基础"
msgid "Charset" msgid "Charset"
msgstr "编码" msgstr "编码"
#: assets/models/asset.py:142 assets/serializers/asset.py:168 #: assets/models/asset.py:142 assets/serializers/asset.py:172
#: tickets/models/ticket.py:50 #: tickets/models/ticket.py:50
msgid "Meta" msgid "Meta"
msgstr "元数据" msgstr "元数据"
@ -548,7 +548,7 @@ msgstr "创建者"
msgid "Date created" msgid "Date created"
msgstr "创建日期" msgstr "创建日期"
#: assets/models/authbook.py:23 #: assets/models/authbook.py:26
msgid "AuthBook" msgid "AuthBook"
msgstr "账号" msgstr "账号"
@ -834,16 +834,16 @@ msgstr "网域名称"
msgid "Nodes name" msgid "Nodes name"
msgstr "节点名称" msgstr "节点名称"
#: assets/serializers/asset.py:97 #: assets/serializers/asset.py:101
msgid "Hardware info" msgid "Hardware info"
msgstr "硬件信息" msgstr "硬件信息"
#: assets/serializers/asset.py:98 assets/serializers/system_user.py:225 #: assets/serializers/asset.py:102 assets/serializers/system_user.py:225
#: orgs/mixins/serializers.py:26 #: orgs/mixins/serializers.py:26
msgid "Org name" msgid "Org name"
msgstr "用户名" msgstr "组织名称"
#: assets/serializers/asset.py:99 #: assets/serializers/asset.py:103
msgid "Admin user display" msgid "Admin user display"
msgstr "特权用户名称" msgstr "特权用户名称"
@ -1240,13 +1240,13 @@ msgstr "认证令牌"
#: audits/signals_handler.py:68 #: audits/signals_handler.py:68
#: authentication/templates/authentication/login.html:210 #: authentication/templates/authentication/login.html:210
#: notifications/backends/__init__.py:11 #: notifications/backends/__init__.py:11 users/models/user.py:659
msgid "WeCom" msgid "WeCom"
msgstr "企业微信" msgstr "企业微信"
#: audits/signals_handler.py:69 #: audits/signals_handler.py:69
#: authentication/templates/authentication/login.html:215 #: authentication/templates/authentication/login.html:215
#: notifications/backends/__init__.py:12 #: notifications/backends/__init__.py:12 users/models/user.py:660
msgid "DingTalk" msgid "DingTalk"
msgstr "钉钉" msgstr "钉钉"
@ -1616,15 +1616,15 @@ msgstr "来源 IP 不被允许登录"
msgid "SSO auth closed" msgid "SSO auth closed"
msgstr "SSO 认证关闭了" msgstr "SSO 认证关闭了"
#: authentication/errors.py:298 authentication/mixins.py:319 #: authentication/errors.py:298 authentication/mixins.py:318
msgid "Your password is too simple, please change it for security" msgid "Your password is too simple, please change it for security"
msgstr "你的密码过于简单,为了安全,请修改" msgstr "你的密码过于简单,为了安全,请修改"
#: authentication/errors.py:307 authentication/mixins.py:326 #: authentication/errors.py:307 authentication/mixins.py:325
msgid "You should to change your password before login" msgid "You should to change your password before login"
msgstr "登录完成前,请先修改密码" msgstr "登录完成前,请先修改密码"
#: authentication/errors.py:316 authentication/mixins.py:333 #: authentication/errors.py:316 authentication/mixins.py:332
msgid "Your password has expired, please reset before logging in" msgid "Your password has expired, please reset before logging in"
msgstr "您的密码已过期,先修改再登录" msgstr "您的密码已过期,先修改再登录"
@ -1649,7 +1649,7 @@ msgstr "MFA 类型"
msgid "MFA code" msgid "MFA code"
msgstr "多因子认证验证码" msgstr "多因子认证验证码"
#: authentication/mixins.py:309 #: authentication/mixins.py:308
msgid "Please change your password" msgid "Please change your password"
msgstr "请修改密码" msgstr "请修改密码"
@ -1657,7 +1657,7 @@ msgstr "请修改密码"
msgid "Private Token" msgid "Private Token"
msgstr "SSH密钥" msgstr "SSH密钥"
#: authentication/models.py:49 settings/serializers/security.py:115 #: authentication/models.py:49 settings/serializers/security.py:118
msgid "Login Confirm" msgid "Login Confirm"
msgstr "登录复核" msgstr "登录复核"
@ -1788,7 +1788,7 @@ msgid "CAS"
msgstr "CAS" msgstr "CAS"
#: authentication/templates/authentication/login.html:220 #: authentication/templates/authentication/login.html:220
#: notifications/backends/__init__.py:14 #: notifications/backends/__init__.py:14 users/models/user.py:661
msgid "FeiShu" msgid "FeiShu"
msgstr "飞书" msgstr "飞书"
@ -1885,9 +1885,9 @@ msgstr "从钉钉获取用户失败"
msgid "DingTalk is not bound" msgid "DingTalk is not bound"
msgstr "钉钉没有绑定" msgstr "钉钉没有绑定"
#: authentication/views/dingtalk.py:218 authentication/views/wecom.py:216 #: authentication/views/dingtalk.py:218
msgid "Please login with a password and then bind the WeCom" msgid "Please login with a password and then bind the DingTalk"
msgstr "请使用密码登录,然后绑定企业微信" msgstr "请使用密码登录,然后绑定钉钉"
#: authentication/views/dingtalk.py:260 authentication/views/dingtalk.py:261 #: authentication/views/dingtalk.py:260 authentication/views/dingtalk.py:261
msgid "Binding DingTalk failed" msgid "Binding DingTalk failed"
@ -1930,19 +1930,19 @@ msgstr "请使用密码登录,然后绑定飞书"
msgid "Binding FeiShu failed" msgid "Binding FeiShu failed"
msgstr "绑定飞书失败" msgstr "绑定飞书失败"
#: authentication/views/login.py:78 #: authentication/views/login.py:80
msgid "Redirecting" msgid "Redirecting"
msgstr "跳转中" msgstr "跳转中"
#: authentication/views/login.py:79 #: authentication/views/login.py:81
msgid "Redirecting to {} authentication" msgid "Redirecting to {} authentication"
msgstr "正在跳转到 {} 认证" msgstr "正在跳转到 {} 认证"
#: authentication/views/login.py:105 #: authentication/views/login.py:107
msgid "Please enable cookies and try again." msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie" msgstr "设置你的浏览器支持cookie"
#: authentication/views/login.py:217 #: authentication/views/login.py:219
msgid "" msgid ""
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n" "Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
" Don't close this page" " Don't close this page"
@ -1950,15 +1950,15 @@ msgstr ""
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n" "等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
" 不要关闭本页面" " 不要关闭本页面"
#: authentication/views/login.py:222 #: authentication/views/login.py:224
msgid "No ticket found" msgid "No ticket found"
msgstr "没有发现工单" msgstr "没有发现工单"
#: authentication/views/login.py:254 #: authentication/views/login.py:256
msgid "Logout success" msgid "Logout success"
msgstr "退出登录成功" msgstr "退出登录成功"
#: authentication/views/login.py:255 #: authentication/views/login.py:257
msgid "Logout success, return login page" msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面" msgstr "退出登录成功,返回到登录页面"
@ -2003,6 +2003,10 @@ msgstr "从企业微信获取用户失败"
msgid "WeCom is not bound" msgid "WeCom is not bound"
msgstr "没有绑定企业微信" msgstr "没有绑定企业微信"
#: authentication/views/wecom.py:216
msgid "Please login with a password and then bind the WeCom"
msgstr "请使用密码登录,然后绑定企业微信"
#: authentication/views/wecom.py:258 authentication/views/wecom.py:259 #: authentication/views/wecom.py:258 authentication/views/wecom.py:259
msgid "Binding WeCom failed" msgid "Binding WeCom failed"
msgstr "绑定企业微信失败" msgstr "绑定企业微信失败"
@ -2134,18 +2138,22 @@ msgstr "忽略时间"
msgid "Invalid ip" msgid "Invalid ip"
msgstr "无效IP" msgstr "无效IP"
#: common/validators.py:13 #: common/validators.py:15
msgid "Special char not allowed" msgid "Special char not allowed"
msgstr "不能包含特殊字符" msgstr "不能包含特殊字符"
#: common/validators.py:25 #: common/validators.py:27
msgid "This field must be unique." msgid "This field must be unique."
msgstr "字段必须唯一" msgstr "字段必须唯一"
#: common/validators.py:33 #: common/validators.py:35
msgid "Should not contains special characters" msgid "Should not contains special characters"
msgstr "不能包含特殊字符" msgstr "不能包含特殊字符"
#: common/validators.py:41
msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确"
#: jumpserver/context_processor.py:20 #: jumpserver/context_processor.py:20
msgid "JumpServer Open Source Bastion Host" msgid "JumpServer Open Source Bastion Host"
msgstr "JumpServer 开源堡垒机" msgstr "JumpServer 开源堡垒机"
@ -2334,22 +2342,22 @@ msgstr "任务结束"
msgid "Server performance" msgid "Server performance"
msgstr "监控告警" msgstr "监控告警"
#: ops/notifications.py:39 #: ops/notifications.py:40
#, python-brace-format #, python-brace-format
msgid "The terminal is offline: {name}" msgid "The terminal is offline: {name}"
msgstr "终端已离线: {name}" msgstr "终端已离线: {name}"
#: ops/notifications.py:45 #: ops/notifications.py:46
#, python-brace-format #, python-brace-format
msgid "[Disk] Disk used more than {max_threshold}%: => {value} ({name})" msgid "[Disk] Disk used more than {max_threshold}%: => {value} ({name})"
msgstr "[Disk] 硬盘使用率超过 {max_threshold}%: => {value} ({name})" msgstr "[Disk] 硬盘使用率超过 {max_threshold}%: => {value} ({name})"
#: ops/notifications.py:52 #: ops/notifications.py:53
#, python-brace-format #, python-brace-format
msgid "[Memory] Memory used more than {max_threshold}%: => {value} ({name})" msgid "[Memory] Memory used more than {max_threshold}%: => {value} ({name})"
msgstr "[Memory] 内存使用率超过 {max_threshold}%: => {value} ({name})" msgstr "[Memory] 内存使用率超过 {max_threshold}%: => {value} ({name})"
#: ops/notifications.py:59 #: ops/notifications.py:60
#, python-brace-format #, python-brace-format
msgid "[CPU] CPU load more than {max_threshold}: => {value} ({name})" msgid "[CPU] CPU load more than {max_threshold}: => {value} ({name})"
msgstr "[CPU] CPU 使用率超过 {max_threshold}: => {value} ({name})" msgstr "[CPU] CPU 使用率超过 {max_threshold}: => {value} ({name})"
@ -2382,7 +2390,7 @@ msgstr "组织存在资源 ({}) 不能被删除"
#: orgs/models.py:432 orgs/serializers.py:106 #: orgs/models.py:432 orgs/serializers.py:106
#: tickets/serializers/ticket/ticket.py:77 #: tickets/serializers/ticket/ticket.py:77
msgid "Organization" msgid "Organization"
msgstr "组织审计员" msgstr "组织"
#: orgs/models.py:17 users/const.py:12 #: orgs/models.py:17 users/const.py:12
msgid "Organization administrator" msgid "Organization administrator"
@ -2396,7 +2404,7 @@ msgstr "组织审计员"
msgid "GLOBAL" msgid "GLOBAL"
msgstr "全局组织" msgstr "全局组织"
#: orgs/models.py:434 users/models/user.py:614 users/serializers/user.py:36 #: orgs/models.py:434 users/models/user.py:614 users/serializers/user.py:37
#: users/templates/users/_select_user_modal.html:15 #: users/templates/users/_select_user_modal.html:15
msgid "Role" msgid "Role"
msgstr "角色" msgstr "角色"
@ -2483,15 +2491,15 @@ msgstr "来自工单"
#: perms/serializers/application/permission.py:18 #: perms/serializers/application/permission.py:18
#: perms/serializers/application/permission.py:38 #: perms/serializers/application/permission.py:38
#: perms/serializers/asset/permission.py:42 #: perms/serializers/asset/permission.py:42
#: perms/serializers/asset/permission.py:68 users/serializers/user.py:76 #: perms/serializers/asset/permission.py:68 users/serializers/user.py:78
msgid "Is valid" msgid "Is valid"
msgstr "账户是否有效" msgstr "账户是否有效"
#: perms/serializers/application/permission.py:19 #: perms/serializers/application/permission.py:19
#: perms/serializers/application/permission.py:37 #: perms/serializers/application/permission.py:37
#: perms/serializers/asset/permission.py:43 #: perms/serializers/asset/permission.py:43
#: perms/serializers/asset/permission.py:67 users/serializers/user.py:28 #: perms/serializers/asset/permission.py:67 users/serializers/user.py:29
#: users/serializers/user.py:77 #: users/serializers/user.py:79
msgid "Is expired" msgid "Is expired"
msgstr "是否过期" msgstr "是否过期"
@ -2550,6 +2558,18 @@ msgstr "测试成功"
msgid "Test mail sent to {}, please check" msgid "Test mail sent to {}, please check"
msgstr "邮件已经发送{}, 请检查" msgstr "邮件已经发送{}, 请检查"
#: settings/api/ldap.py:152
msgid "Synchronization start, please wait."
msgstr "同步开始,请稍等"
#: settings/api/ldap.py:156
msgid "Synchronization is running, please wait."
msgstr "同步正在运行,请稍等"
#: settings/api/ldap.py:161
msgid "Synchronization error: {}"
msgstr "同步错误: {}"
#: settings/api/ldap.py:194 #: settings/api/ldap.py:194
msgid "Get ldap users is None" msgid "Get ldap users is None"
msgstr "获取 LDAP 用户为 None" msgstr "获取 LDAP 用户为 None"
@ -2563,7 +2583,7 @@ msgstr "成功导入 {} 个用户 ( 组织: {} )"
msgid "Welcome to the JumpServer open source Bastion Host" msgid "Welcome to the JumpServer open source Bastion Host"
msgstr "欢迎使用JumpServer开源堡垒机" msgstr "欢迎使用JumpServer开源堡垒机"
#: settings/models.py:123 users/templates/users/reset_password.html:29 #: settings/models.py:155 users/templates/users/reset_password.html:29
msgid "Setting" msgid "Setting"
msgstr "设置" msgstr "设置"
@ -2612,22 +2632,26 @@ msgid "Server url"
msgstr "服务端地址" msgstr "服务端地址"
#: settings/serializers/auth/cas.py:13 #: settings/serializers/auth/cas.py:13
msgid "Proxy server url"
msgstr "代理服务地址"
#: settings/serializers/auth/cas.py:14
msgid "Logout completely" msgid "Logout completely"
msgstr "同步注销" msgstr "同步注销"
#: settings/serializers/auth/cas.py:15 #: settings/serializers/auth/cas.py:16
msgid "Username attr" msgid "Username attr"
msgstr "用户名属性" msgstr "用户名属性"
#: settings/serializers/auth/cas.py:16 #: settings/serializers/auth/cas.py:17
msgid "Enable attributes map" msgid "Enable attributes map"
msgstr "启用属性映射" msgstr "启用属性映射"
#: settings/serializers/auth/cas.py:17 #: settings/serializers/auth/cas.py:18
msgid "Rename attr" msgid "Rename attr"
msgstr "映射属性" msgstr "映射属性"
#: settings/serializers/auth/cas.py:18 #: settings/serializers/auth/cas.py:19
msgid "Create user if not" msgid "Create user if not"
msgstr "创建用户(如果不存在)" msgstr "创建用户(如果不存在)"
@ -2686,15 +2710,15 @@ msgstr ""
msgid "Periodic display" msgid "Periodic display"
msgstr "定时执行" msgstr "定时执行"
#: settings/serializers/auth/ldap.py:66 #: settings/serializers/auth/ldap.py:68
msgid "Connect timeout" msgid "Connect timeout"
msgstr "连接超时时间" msgstr "连接超时时间"
#: settings/serializers/auth/ldap.py:67 #: settings/serializers/auth/ldap.py:70
msgid "Search paged size" msgid "Search paged size"
msgstr "搜索分页数量" msgstr "搜索分页数量"
#: settings/serializers/auth/ldap.py:69 #: settings/serializers/auth/ldap.py:72
msgid "Enable LDAP auth" msgid "Enable LDAP auth"
msgstr "启用 LDAP 认证" msgstr "启用 LDAP 认证"
@ -2829,7 +2853,7 @@ msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过
msgid "SSO auth key TTL" msgid "SSO auth key TTL"
msgstr "Token 有效期" msgstr "Token 有效期"
#: settings/serializers/auth/sso.py:16 settings/serializers/security.py:72 #: settings/serializers/auth/sso.py:16 settings/serializers/security.py:74
msgid "Unit: second" msgid "Unit: second"
msgstr "单位: 秒" msgstr "单位: 秒"
@ -2869,29 +2893,29 @@ msgstr "全局组织名"
msgid "The name of global organization to display" msgid "The name of global organization to display"
msgstr "全局组织的显示名称,默认为 全局组织" msgstr "全局组织的显示名称,默认为 全局组织"
#: settings/serializers/cleaning.py:9 #: settings/serializers/cleaning.py:10
msgid "Login log keep days" msgid "Login log keep days"
msgstr "登录日志" msgstr "登录日志"
#: settings/serializers/cleaning.py:9 settings/serializers/cleaning.py:12 #: settings/serializers/cleaning.py:10 settings/serializers/cleaning.py:14
#: settings/serializers/cleaning.py:15 settings/serializers/cleaning.py:18 #: settings/serializers/cleaning.py:18 settings/serializers/cleaning.py:22
#: settings/serializers/cleaning.py:21 #: settings/serializers/cleaning.py:26
msgid "Unit: day" msgid "Unit: day"
msgstr "单位: 天" msgstr "单位: 天"
#: settings/serializers/cleaning.py:12 #: settings/serializers/cleaning.py:14
msgid "Task log keep days" msgid "Task log keep days"
msgstr "任务日志" msgstr "任务日志"
#: settings/serializers/cleaning.py:15 #: settings/serializers/cleaning.py:18
msgid "Operate log keep days" msgid "Operate log keep days"
msgstr "操作日志" msgstr "操作日志"
#: settings/serializers/cleaning.py:18 #: settings/serializers/cleaning.py:22
msgid "FTP log keep days" msgid "FTP log keep days"
msgstr "上传下载" msgstr "上传下载"
#: settings/serializers/cleaning.py:21 #: settings/serializers/cleaning.py:26
msgid "Cloud sync record keep days" msgid "Cloud sync record keep days"
msgstr "云同步记录" msgstr "云同步记录"
@ -2998,31 +3022,27 @@ msgstr "启用工单系统"
msgid "OTP issuer name" msgid "OTP issuer name"
msgstr "OTP 扫描后的名称" msgstr "OTP 扫描后的名称"
#: settings/serializers/other.py:15 #: settings/serializers/other.py:17
msgid "OTP valid window" msgid "OTP valid window"
msgstr "OTP 延迟有效次数" msgstr "OTP 延迟有效次数"
#: settings/serializers/other.py:17 #: settings/serializers/other.py:22
msgid "Enable period task"
msgstr "启用周期任务"
#: settings/serializers/other.py:20
msgid "CMD" msgid "CMD"
msgstr "" msgstr ""
#: settings/serializers/other.py:21 #: settings/serializers/other.py:23
msgid "PowerShell" msgid "PowerShell"
msgstr "" msgstr ""
#: settings/serializers/other.py:23 #: settings/serializers/other.py:25
msgid "Shell (Windows)" msgid "Shell (Windows)"
msgstr "ShellWindows 资产)" msgstr "ShellWindows 资产)"
#: settings/serializers/other.py:24 #: settings/serializers/other.py:26
msgid "The shell type used when Windows assets perform ansible tasks" msgid "The shell type used when Windows assets perform ansible tasks"
msgstr "windows 资产执行 Ansible 任务时,使用的 Shell 类型。" msgstr "windows 资产执行 Ansible 任务时,使用的 Shell 类型。"
#: settings/serializers/other.py:28 #: settings/serializers/other.py:30
msgid "Perm single to ungroup node" msgid "Perm single to ungroup node"
msgstr "直接授权资产放在未分组节点" msgstr "直接授权资产放在未分组节点"
@ -3120,77 +3140,77 @@ msgstr "开启后如果系统中不存在该用户CAS、OIDC 登录将会
msgid "Only from source login" msgid "Only from source login"
msgstr "仅从用户来源登录" msgstr "仅从用户来源登录"
#: settings/serializers/security.py:72 #: settings/serializers/security.py:74
msgid "MFA verify TTL" msgid "MFA verify TTL"
msgstr "MFA 校验有效期" msgstr "MFA 校验有效期"
#: settings/serializers/security.py:75 #: settings/serializers/security.py:78
msgid "Enable Login captcha" msgid "Enable Login captcha"
msgstr "启用登录验证码" msgstr "启用登录验证码"
#: settings/serializers/security.py:81 #: settings/serializers/security.py:84
msgid "Enable terminal register" msgid "Enable terminal register"
msgstr "终端注册" msgstr "终端注册"
#: settings/serializers/security.py:82 #: settings/serializers/security.py:85
msgid "" msgid ""
"Allow terminal register, after all terminal setup, you should disable this " "Allow terminal register, after all terminal setup, you should disable this "
"for security" "for security"
msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭"
#: settings/serializers/security.py:85 #: settings/serializers/security.py:88
msgid "Replay watermark" msgid "Replay watermark"
msgstr "录像水印" msgstr "录像水印"
#: settings/serializers/security.py:86 #: settings/serializers/security.py:89
msgid "Enabled, the session replay contains watermark information" msgid "Enabled, the session replay contains watermark information"
msgstr "启用后,会话录像将包含水印信息" msgstr "启用后,会话录像将包含水印信息"
#: settings/serializers/security.py:90 #: settings/serializers/security.py:93
msgid "Connection max idle time" msgid "Connection max idle time"
msgstr "连接最大空闲时间" msgstr "连接最大空闲时间"
#: settings/serializers/security.py:91 #: settings/serializers/security.py:94
msgid "If idle time more than it, disconnect connection Unit: minute" msgid "If idle time more than it, disconnect connection Unit: minute"
msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
#: settings/serializers/security.py:94 #: settings/serializers/security.py:97
msgid "Remember manual auth" msgid "Remember manual auth"
msgstr "保存手动输入密码" msgstr "保存手动输入密码"
#: settings/serializers/security.py:97 #: settings/serializers/security.py:100
msgid "Enable change auth secure mode" msgid "Enable change auth secure mode"
msgstr "启用改密安全模式" msgstr "启用改密安全模式"
#: settings/serializers/security.py:100 #: settings/serializers/security.py:103
msgid "Insecure command alert" msgid "Insecure command alert"
msgstr "危险命令告警" msgstr "危险命令告警"
#: settings/serializers/security.py:103 #: settings/serializers/security.py:106
msgid "Email recipient" msgid "Email recipient"
msgstr "邮件收件人" msgstr "邮件收件人"
#: settings/serializers/security.py:104 #: settings/serializers/security.py:107
msgid "Multiple user using , split" msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割" msgstr "多个用户,使用 , 分割"
#: settings/serializers/security.py:107 #: settings/serializers/security.py:110
msgid "Batch command execution" msgid "Batch command execution"
msgstr "批量命令执行" msgstr "批量命令执行"
#: settings/serializers/security.py:108 #: settings/serializers/security.py:111
msgid "Allow user run batch command or not using ansible" msgid "Allow user run batch command or not using ansible"
msgstr "是否允许用户使用 ansible 执行批量命令" msgstr "是否允许用户使用 ansible 执行批量命令"
#: settings/serializers/security.py:111 #: settings/serializers/security.py:114
msgid "Session share" msgid "Session share"
msgstr "会话分享" msgstr "会话分享"
#: settings/serializers/security.py:112 #: settings/serializers/security.py:115
msgid "Enabled, Allows user active session to be shared with other users" msgid "Enabled, Allows user active session to be shared with other users"
msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作" msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作"
#: settings/serializers/security.py:116 #: settings/serializers/security.py:119
msgid "Enabled, please go to the user detail add approver" msgid "Enabled, please go to the user detail add approver"
msgstr "启用后, 请在用户详情中添加审批人" msgstr "启用后, 请在用户详情中添加审批人"
@ -3542,7 +3562,7 @@ msgstr "数据库应用"
msgid "Perms" msgid "Perms"
msgstr "权限管理" msgstr "权限管理"
#: templates/_nav.html:97 terminal/notifications.py:19 #: templates/_nav.html:97 terminal/notifications.py:20
msgid "Sessions" msgid "Sessions"
msgstr "会话管理" msgstr "会话管理"
@ -4051,11 +4071,11 @@ msgstr "命令存储"
msgid "Replay storage" msgid "Replay storage"
msgstr "录像存储" msgstr "录像存储"
#: terminal/notifications.py:65 #: terminal/notifications.py:66
msgid "Danger command alert" msgid "Danger command alert"
msgstr "危险命令告警" msgstr "危险命令告警"
#: terminal/notifications.py:78 #: terminal/notifications.py:82
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4074,7 +4094,7 @@ msgstr ""
"会话: %(session_detail_url)s?oid=%(oid)s\n" "会话: %(session_detail_url)s?oid=%(oid)s\n"
" " " "
#: terminal/notifications.py:106 #: terminal/notifications.py:113
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4104,11 +4124,11 @@ msgstr ""
" <br>\n" " <br>\n"
" " " "
#: terminal/notifications.py:135 #: terminal/notifications.py:142
msgid "Batch danger command alert" msgid "Batch danger command alert"
msgstr "批量危险命令告警" msgstr "批量危险命令告警"
#: terminal/notifications.py:146 #: terminal/notifications.py:153
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4140,7 +4160,7 @@ msgstr ""
"<br>\n" "<br>\n"
" " " "
#: terminal/notifications.py:173 #: terminal/notifications.py:180
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4178,6 +4198,14 @@ msgstr "是否可重放"
msgid "Can join" msgid "Can join"
msgstr "是否可加入" msgstr "是否可加入"
#: terminal/serializers/session.py:40
msgid "Can terminate"
msgstr "是否可中断"
#: terminal/serializers/session.py:50
msgid "Command amount"
msgstr "命令数量"
#: terminal/serializers/storage.py:21 #: terminal/serializers/storage.py:21
msgid "Endpoint invalid: remove path `{}`" msgid "Endpoint invalid: remove path `{}`"
msgstr "端点无效: 移除路径 `{}`" msgstr "端点无效: 移除路径 `{}`"
@ -4311,42 +4339,42 @@ msgstr "自定义用户"
msgid "Ticket already closed" msgid "Ticket already closed"
msgstr "工单已经关闭" msgstr "工单已经关闭"
#: tickets/handler/apply_application.py:51 #: tickets/handler/apply_application.py:52
msgid "Applied category" msgid "Applied category"
msgstr "申请的类别" msgstr "申请的类别"
#: tickets/handler/apply_application.py:52 #: tickets/handler/apply_application.py:53
msgid "Applied type" msgid "Applied type"
msgstr "申请的类型" msgstr "申请的类型"
#: tickets/handler/apply_application.py:53 #: tickets/handler/apply_application.py:54
msgid "Applied application group" msgid "Applied application group"
msgstr "申请的应用组" msgstr "申请的应用组"
#: tickets/handler/apply_application.py:54 tickets/handler/apply_asset.py:47 #: tickets/handler/apply_application.py:55 tickets/handler/apply_asset.py:48
msgid "Applied system user group" msgid "Applied system user group"
msgstr "申请的系统用户组" msgstr "申请的系统用户组"
#: tickets/handler/apply_application.py:55 tickets/handler/apply_asset.py:49 #: tickets/handler/apply_application.py:56 tickets/handler/apply_asset.py:50
msgid "Applied date start" msgid "Applied date start"
msgstr "申请的开始日期" msgstr "申请的开始日期"
#: tickets/handler/apply_application.py:56 tickets/handler/apply_asset.py:50 #: tickets/handler/apply_application.py:57 tickets/handler/apply_asset.py:51
msgid "Applied date expired" msgid "Applied date expired"
msgstr "申请的失效日期" msgstr "申请的失效日期"
#: tickets/handler/apply_application.py:78 tickets/handler/apply_asset.py:71 #: tickets/handler/apply_application.py:79 tickets/handler/apply_asset.py:72
msgid "" msgid ""
"Created by the ticket, ticket title: {}, ticket applicant: {}, ticket " "Created by the ticket, ticket title: {}, ticket applicant: {}, ticket "
"processor: {}, ticket ID: {}" "processor: {}, ticket ID: {}"
msgstr "" msgstr ""
"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}" "通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}"
#: tickets/handler/apply_asset.py:46 #: tickets/handler/apply_asset.py:47
msgid "Applied hostname group" msgid "Applied hostname group"
msgstr "申请的主机名组" msgstr "申请的主机名组"
#: tickets/handler/apply_asset.py:48 #: tickets/handler/apply_asset.py:49
msgid "Applied actions" msgid "Applied actions"
msgstr "申请的动作" msgstr "申请的动作"
@ -4499,15 +4527,15 @@ msgstr "流程"
msgid "TicketFlow" msgid "TicketFlow"
msgstr "工单流程" msgstr "工单流程"
#: tickets/notifications.py:44 tickets/notifications.py:56 #: tickets/notifications.py:57
msgid "click here to review" msgid "click here to review"
msgstr "点击查看" msgstr "点击查看"
#: tickets/notifications.py:74 #: tickets/notifications.py:75
msgid "Your has a new ticket, applicant - {}" msgid "Your has a new ticket, applicant - {}"
msgstr "你有一个新的工单, 申请人 - {}" msgstr "你有一个新的工单, 申请人 - {}"
#: tickets/notifications.py:87 #: tickets/notifications.py:88
msgid "Your ticket has been processed, processor - {}" msgid "Your ticket has been processed, processor - {}"
msgstr "你的工单已被处理, 处理人 - {}" msgstr "你的工单已被处理, 处理人 - {}"
@ -4538,6 +4566,11 @@ msgstr "批准的系统用户名称"
msgid "Permission named `{}` already exists" msgid "Permission named `{}` already exists"
msgstr "授权名称 `{}` 已存在" msgstr "授权名称 `{}` 已存在"
#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:75
#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:67
msgid "The expiration date should be greater than the start date"
msgstr "过期时间要大于开始时间"
#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:20 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:20
msgid "Apply assets" msgid "Apply assets"
msgstr "申请资产" msgstr "申请资产"
@ -4774,13 +4807,13 @@ msgstr "管理员"
msgid "Administrator is the super user of system" msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员" msgstr "Administrator是初始的超级管理员"
#: users/notifications.py:12 users/notifications.py:45 #: users/notifications.py:17 users/notifications.py:50
#: users/templates/users/reset_password.html:5 #: users/templates/users/reset_password.html:5
#: users/templates/users/reset_password.html:6 #: users/templates/users/reset_password.html:6
msgid "Reset password" msgid "Reset password"
msgstr "重置密码" msgstr "重置密码"
#: users/notifications.py:13 #: users/notifications.py:18
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4820,7 +4853,7 @@ msgstr ""
"%(login_url)s\n" "%(login_url)s\n"
"\n" "\n"
#: users/notifications.py:46 #: users/notifications.py:51
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4864,12 +4897,12 @@ msgstr ""
" <br>\n" " <br>\n"
" " " "
#: users/notifications.py:85 users/notifications.py:119 #: users/notifications.py:90 users/notifications.py:122
#: users/views/profile/reset.py:127 #: users/views/profile/reset.py:127
msgid "Reset password success" msgid "Reset password success"
msgstr "重置密码成功" msgstr "重置密码成功"
#: users/notifications.py:86 #: users/notifications.py:91
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4889,10 +4922,9 @@ msgid ""
"\n" "\n"
"\n" "\n"
"IP Address: %(ip_address)s\n" "IP Address: %(ip_address)s\n"
"<br>\n" "\n"
"<br>\n" "\n"
"Browser: %(browser)s\n" "Browser: %(browser)s\n"
"<br>\n"
" \n" " \n"
" " " "
msgstr "" msgstr ""
@ -4911,14 +4943,12 @@ msgstr ""
"\n" "\n"
"\n" "\n"
"IP 地址: %(ip_address)s\n" "IP 地址: %(ip_address)s\n"
"<br>\n" "\n"
"<br>\n"
"浏览器: %(browser)s\n" "浏览器: %(browser)s\n"
"<br>\n"
" \n" " \n"
" " " "
#: users/notifications.py:120 #: users/notifications.py:123
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -4981,11 +5011,11 @@ msgstr ""
" \n" " \n"
" " " "
#: users/notifications.py:163 users/notifications.py:199 #: users/notifications.py:166 users/notifications.py:202
msgid "Security notice" msgid "Security notice"
msgstr "安全通知" msgstr "安全通知"
#: users/notifications.py:164 #: users/notifications.py:167
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5029,7 +5059,7 @@ msgstr ""
"\n" "\n"
" " " "
#: users/notifications.py:200 #: users/notifications.py:203
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5078,11 +5108,11 @@ msgstr ""
" <br>\n" " <br>\n"
" " " "
#: users/notifications.py:237 users/notifications.py:256 #: users/notifications.py:240 users/notifications.py:259
msgid "Expiration notice" msgid "Expiration notice"
msgstr "过期通知" msgstr "过期通知"
#: users/notifications.py:238 #: users/notifications.py:241
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5103,7 +5133,7 @@ msgstr ""
"为了不影响您正常工作,请联系管理员确认。\n" "为了不影响您正常工作,请联系管理员确认。\n"
" " " "
#: users/notifications.py:257 #: users/notifications.py:260
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5125,11 +5155,11 @@ msgstr ""
" <br>\n" " <br>\n"
" " " "
#: users/notifications.py:277 users/notifications.py:298 #: users/notifications.py:280 users/notifications.py:301
msgid "SSH Key Reset" msgid "SSH Key Reset"
msgstr "重置SSH密钥" msgstr "重置SSH密钥"
#: users/notifications.py:278 #: users/notifications.py:281
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5154,7 +5184,7 @@ msgstr ""
"\n" "\n"
" " " "
#: users/notifications.py:299 #: users/notifications.py:302
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5179,11 +5209,11 @@ msgstr ""
" <br>\n" " <br>\n"
" " " "
#: users/notifications.py:321 users/notifications.py:341 #: users/notifications.py:324 users/notifications.py:344
msgid "MFA Reset" msgid "MFA Reset"
msgstr "重置 MFA" msgstr "重置 MFA"
#: users/notifications.py:322 #: users/notifications.py:325
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5208,7 +5238,7 @@ msgstr ""
"\n" "\n"
" " " "
#: users/notifications.py:342 #: users/notifications.py:345
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
@ -5237,7 +5267,7 @@ msgstr ""
msgid "The old password is incorrect" msgid "The old password is incorrect"
msgstr "旧密码错误" msgstr "旧密码错误"
#: users/serializers/profile.py:36 users/serializers/user.py:137 #: users/serializers/profile.py:36 users/serializers/user.py:140
msgid "Password does not match security rules" msgid "Password does not match security rules"
msgstr "密码不满足安全规则" msgstr "密码不满足安全规则"
@ -5249,81 +5279,85 @@ msgstr "新密码不能是最近 {} 次的密码"
msgid "The newly set password is inconsistent" msgid "The newly set password is inconsistent"
msgstr "两次密码不一致" msgstr "两次密码不一致"
#: users/serializers/profile.py:121 users/serializers/user.py:75 #: users/serializers/profile.py:121 users/serializers/user.py:77
msgid "Is first login" msgid "Is first login"
msgstr "首次登录" msgstr "首次登录"
#: users/serializers/user.py:22 #: users/serializers/user.py:23
#: xpack/plugins/change_auth_plan/models/base.py:32 #: xpack/plugins/change_auth_plan/models/base.py:32
#: xpack/plugins/change_auth_plan/serializers/base.py:24 #: xpack/plugins/change_auth_plan/serializers/base.py:24
msgid "Password strategy" msgid "Password strategy"
msgstr "密码策略" msgstr "密码策略"
#: users/serializers/user.py:24 #: users/serializers/user.py:25
msgid "MFA enabled" msgid "MFA enabled"
msgstr "是否开启多因子认证" msgstr "是否开启多因子认证"
#: users/serializers/user.py:25 #: users/serializers/user.py:26
msgid "MFA force enabled" msgid "MFA force enabled"
msgstr "强制启用多因子认证" msgstr "强制启用多因子认证"
#: users/serializers/user.py:26 #: users/serializers/user.py:27
msgid "MFA level display" msgid "MFA level display"
msgstr "多因子认证等级名称" msgstr "多因子认证等级名称"
#: users/serializers/user.py:27 #: users/serializers/user.py:28
msgid "Login blocked" msgid "Login blocked"
msgstr "登录被阻塞" msgstr "登录被阻塞"
#: users/serializers/user.py:29 #: users/serializers/user.py:30
msgid "Can update" msgid "Can update"
msgstr "是否可更新" msgstr "是否可更新"
#: users/serializers/user.py:30 #: users/serializers/user.py:31
msgid "Can delete" msgid "Can delete"
msgstr "是否可删除" msgstr "是否可删除"
#: users/serializers/user.py:33 users/serializers/user.py:82 #: users/serializers/user.py:32
msgid "Can public key authentication"
msgstr "能否公钥认证"
#: users/serializers/user.py:34 users/serializers/user.py:84
msgid "Organization role name" msgid "Organization role name"
msgstr "组织角色名称" msgstr "组织角色名称"
#: users/serializers/user.py:78 #: users/serializers/user.py:80
msgid "Avatar url" msgid "Avatar url"
msgstr "头像路径" msgstr "头像路径"
#: users/serializers/user.py:80 #: users/serializers/user.py:82
msgid "Groups name" msgid "Groups name"
msgstr "用户组名" msgstr "用户组名"
#: users/serializers/user.py:81 #: users/serializers/user.py:83
msgid "Source name" msgid "Source name"
msgstr "用户来源名" msgstr "用户来源名"
#: users/serializers/user.py:83 #: users/serializers/user.py:85
msgid "Super role name" msgid "Super role name"
msgstr "超级角色名称" msgstr "超级角色名称"
#: users/serializers/user.py:84 #: users/serializers/user.py:86
msgid "Total role name" msgid "Total role name"
msgstr "汇总角色名称" msgstr "汇总角色名称"
#: users/serializers/user.py:86 #: users/serializers/user.py:88
msgid "Is wecom bound" msgid "Is wecom bound"
msgstr "是否绑定了企业微信" msgstr "是否绑定了企业微信"
#: users/serializers/user.py:87 #: users/serializers/user.py:89
msgid "Is dingtalk bound" msgid "Is dingtalk bound"
msgstr "是否绑定了钉钉" msgstr "是否绑定了钉钉"
#: users/serializers/user.py:88 #: users/serializers/user.py:90
msgid "Is feishu bound" msgid "Is feishu bound"
msgstr "是否绑定了飞书" msgstr "是否绑定了飞书"
#: users/serializers/user.py:111 #: users/serializers/user.py:114
msgid "Role limit to {}" msgid "Role limit to {}"
msgstr "角色只能为 {}" msgstr "角色只能为 {}"
#: users/serializers/user.py:231 #: users/serializers/user.py:234
msgid "name not unique" msgid "name not unique"
msgstr "名称重复" msgstr "名称重复"
@ -5682,8 +5716,8 @@ msgid "Empty and append SSH KEY"
msgstr "清空所有并添加" msgstr "清空所有并添加"
#: xpack/plugins/change_auth_plan/models/asset.py:32 #: xpack/plugins/change_auth_plan/models/asset.py:32
msgid "Empty pre add and append SSH KEY" msgid "Replace (The key generated by JumpServer) "
msgstr "清空上次并添加" msgstr "替换 (由 JumpServer 生成的密钥)"
#: xpack/plugins/change_auth_plan/models/asset.py:50 #: xpack/plugins/change_auth_plan/models/asset.py:50
#: xpack/plugins/change_auth_plan/serializers/asset.py:34 #: xpack/plugins/change_auth_plan/serializers/asset.py:34

View File

@ -164,7 +164,6 @@ class SystemMessage(Message):
self.send_msg(users, receive_backends) self.send_msg(users, receive_backends)
@classmethod @classmethod
def post_insert_to_db(cls, subscription: SystemMsgSubscription): def post_insert_to_db(cls, subscription: SystemMsgSubscription):
pass pass

View File

@ -18,8 +18,9 @@ class ServerPerformanceMessage(SystemMessage):
self._msg = msg self._msg = msg
def get_common_msg(self): def get_common_msg(self):
subject = self._msg[:80]
return { return {
'subject': self._msg[:80], 'subject': subject.replace('<br>', '; '),
'message': self._msg 'message': self._msg
} }

View File

@ -19,6 +19,7 @@ from common.decorator import on_transaction_commit
from common.signals import django_ready from common.signals import django_ready
from common.utils import get_logger from common.utils import get_logger
from common.utils.connection import RedisPubSub from common.utils.connection import RedisPubSub
from assets.models import CommandFilterRule
logger = get_logger(__file__) logger = get_logger(__file__)
@ -91,17 +92,18 @@ def on_org_delete(sender, instance, **kwargs):
root_node.delete() root_node.delete()
def _remove_users(model, users, org): def _remove_users(model, users, org, user_field_name='users'):
with tmp_to_org(org): with tmp_to_org(org):
if not isinstance(users, (tuple, list, set)): if not isinstance(users, (tuple, list, set)):
users = (users, ) users = (users, )
m2m_model = model.users.through user_field = getattr(model, user_field_name)
reverse = model.users.reverse m2m_model = user_field.through
reverse = user_field.reverse
if reverse: if reverse:
m2m_field_name = model.users.field.m2m_reverse_field_name() m2m_field_name = user_field.field.m2m_reverse_field_name()
else: else:
m2m_field_name = model.users.field.m2m_field_name() m2m_field_name = user_field.field.m2m_field_name()
relations = m2m_model.objects.filter(**{ relations = m2m_model.objects.filter(**{
'user__in': users, 'user__in': users,
f'{m2m_field_name}__org_id': org.id f'{m2m_field_name}__org_id': org.id
@ -149,6 +151,8 @@ def _clear_users_from_org(org, users):
for m in models: for m in models:
_remove_users(m, users, org) _remove_users(m, users, org)
_remove_users(CommandFilterRule, users, org, user_field_name='reviewers')
@receiver(m2m_changed, sender=OrganizationMember) @receiver(m2m_changed, sender=OrganizationMember)
def on_org_user_changed(action, instance, reverse, pk_set, **kwargs): def on_org_user_changed(action, instance, reverse, pk_set, **kwargs):

View File

@ -54,13 +54,24 @@ def get_user_all_asset_perm_ids(user) -> set:
class UserGrantedTreeRefreshController: class UserGrantedTreeRefreshController:
key_template = 'perms.user.node_tree.builded_orgs.user_id:{user_id}' key_template = 'perms.user.node_tree.built_orgs.user_id:{user_id}'
def __init__(self, user): def __init__(self, user):
self.user = user self.user = user
self.key = self.key_template.format(user_id=user.id) self.key = self.key_template.format(user_id=user.id)
self.client = self.get_redis_client() self.client = self.get_redis_client()
@classmethod
def clean_all_user_tree_built_mark(cls):
""" 清除所有用户已构建树的标记 """
client = cls.get_redis_client()
key_match = cls.key_template.format(user_id='*')
keys = client.keys(key_match)
with client.pipeline() as p:
for key in keys:
p.delete(key)
p.execute()
@classmethod @classmethod
def get_redis_client(cls): def get_redis_client(cls):
return cache.client.get_client(write=True) return cache.client.get_client(write=True)
@ -69,13 +80,13 @@ class UserGrantedTreeRefreshController:
org_ids = self.client.smembers(self.key) org_ids = self.client.smembers(self.key)
return {org_id.decode() for org_id in org_ids} return {org_id.decode() for org_id in org_ids}
def set_all_orgs_as_builed(self): def set_all_orgs_as_built(self):
self.client.sadd(self.key, *self.org_ids) self.client.sadd(self.key, *self.org_ids)
def have_need_refresh_orgs(self): def have_need_refresh_orgs(self):
builded_org_ids = self.client.smembers(self.key) built_org_ids = self.client.smembers(self.key)
builded_org_ids = {org_id.decode() for org_id in builded_org_ids} built_org_ids = {org_id.decode() for org_id in built_org_ids}
have = self.org_ids - builded_org_ids have = self.org_ids - built_org_ids
return have return have
def get_need_refresh_orgs_and_fill_up(self): def get_need_refresh_orgs_and_fill_up(self):
@ -85,15 +96,18 @@ class UserGrantedTreeRefreshController:
p.smembers(self.key) p.smembers(self.key)
p.sadd(self.key, *org_ids) p.sadd(self.key, *org_ids)
ret = p.execute() ret = p.execute()
builded_org_ids = {org_id.decode() for org_id in ret[0]} built_org_ids = {org_id.decode() for org_id in ret[0]}
ids = org_ids - builded_org_ids ids = org_ids - built_org_ids
orgs = {*Organization.objects.filter(id__in=ids)} orgs = {*Organization.objects.filter(id__in=ids)}
logger.info(f'Need rebuild orgs are {orgs}, builed orgs are {ret[0]}, all orgs are {org_ids}') logger.info(
f'Need rebuild orgs are {orgs}, built orgs are {ret[0]}, '
f'all orgs are {org_ids}'
)
return orgs return orgs
@classmethod @classmethod
@on_transaction_commit @on_transaction_commit
def remove_builed_orgs_from_users(cls, org_ids, user_ids): def remove_built_orgs_from_users(cls, org_ids, user_ids):
client = cls.get_redis_client() client = cls.get_redis_client()
org_ids = [str(org_id) for org_id in org_ids] org_ids = [str(org_id) for org_id in org_ids]
@ -102,11 +116,12 @@ class UserGrantedTreeRefreshController:
key = cls.key_template.format(user_id=user_id) key = cls.key_template.format(user_id=user_id)
p.srem(key, *org_ids) p.srem(key, *org_ids)
p.execute() p.execute()
logger.info(f'Remove orgs from users builded tree: users:{user_ids} orgs:{org_ids}') logger.info(f'Remove orgs from users built tree: users:{user_ids} '
f'orgs:{org_ids}')
@classmethod @classmethod
def add_need_refresh_orgs_for_users(cls, org_ids, user_ids): def add_need_refresh_orgs_for_users(cls, org_ids, user_ids):
cls.remove_builed_orgs_from_users(org_ids, user_ids) cls.remove_built_orgs_from_users(org_ids, user_ids)
@classmethod @classmethod
@ensure_in_real_or_default_org @ensure_in_real_or_default_org
@ -168,7 +183,7 @@ class UserGrantedTreeRefreshController:
).values_list('user_id', flat=True) ).values_list('user_id', flat=True)
user_ids.update(group_user_ids) user_ids.update(group_user_ids)
cls.remove_builed_orgs_from_users( cls.remove_built_orgs_from_users(
[current_org.id], user_ids [current_org.id], user_ids
) )
@ -193,7 +208,7 @@ class UserGrantedTreeRefreshController:
with UserGrantedTreeRebuildLock(user_id=user.id): with UserGrantedTreeRebuildLock(user_id=user.id):
if force: if force:
orgs = self.orgs orgs = self.orgs
self.set_all_orgs_as_builed() self.set_all_orgs_as_built()
else: else:
orgs = self.get_need_refresh_orgs_and_fill_up() orgs = self.get_need_refresh_orgs_and_fill_up()

View File

@ -149,16 +149,16 @@ class LDAPUserListApi(generics.ListAPIView):
sync_util.set_task_status(sync_util.TASK_STATUS_IS_RUNNING) sync_util.set_task_status(sync_util.TASK_STATUS_IS_RUNNING)
t = threading.Thread(target=sync_ldap_user) t = threading.Thread(target=sync_ldap_user)
t.start() t.start()
data = {'msg': 'Sync start.'} data = {'msg': _('Synchronization start, please wait.')}
return Response(data=data, status=409) return Response(data=data, status=409)
# 同步任务正在执行 # 同步任务正在执行
if sync_util.task_is_running: if sync_util.task_is_running:
data = {'msg': 'synchronization is running.'} data = {'msg': _('Synchronization is running, please wait.')}
return Response(data=data, status=409) return Response(data=data, status=409)
# 同步任务执行结束 # 同步任务执行结束
if sync_util.task_is_over: if sync_util.task_is_over:
msg = sync_util.get_task_error_msg() msg = sync_util.get_task_error_msg()
data = {'error': 'Synchronization error: {}'.format(msg)} data = {'error': _('Synchronization error: {}'.format(msg))}
return Response(data=data, status=400) return Response(data=data, status=400)
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)

View File

@ -117,6 +117,10 @@ class Setting(models.Model):
# 设置内存值 # 设置内存值
setattr(settings, name, setting.cleaned_value) setattr(settings, name, setting.cleaned_value)
@classmethod
def refresh_AUTH_CAS(cls):
cls.refresh_authentications('AUTH_CAS')
@classmethod @classmethod
def refresh_AUTH_LDAP(cls): def refresh_AUTH_LDAP(cls):
cls.refresh_authentications('AUTH_LDAP') cls.refresh_authentications('AUTH_LDAP')

View File

@ -1,4 +1,4 @@
import json
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -10,8 +10,9 @@ __all__ = [
class CASSettingSerializer(serializers.Serializer): class CASSettingSerializer(serializers.Serializer):
AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth')) AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth'))
CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url')) CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url'))
CAS_ROOT_PROXIED_AS = serializers.CharField(required=False, max_length=1024, label=_('Proxy server url'))
CAS_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely')) CAS_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
CAS_VERSION = serializers.IntegerField(required=False, label=_('Version')) CAS_VERSION = serializers.IntegerField(required=False, label=_('Version'), min_value=1, max_value=3)
CAS_USERNAME_ATTRIBUTE = serializers.CharField(required=False, max_length=1024, label=_('Username attr')) CAS_USERNAME_ATTRIBUTE = serializers.CharField(required=False, max_length=1024, label=_('Username attr'))
CAS_APPLY_ATTRIBUTES_TO_USER = serializers.BooleanField(required=False, label=_('Enable attributes map')) CAS_APPLY_ATTRIBUTES_TO_USER = serializers.BooleanField(required=False, label=_('Enable attributes map'))
CAS_RENAME_ATTRIBUTES = serializers.DictField(required=False, label=_('Rename attr')) CAS_RENAME_ATTRIBUTES = serializers.DictField(required=False, label=_('Rename attr'))

View File

@ -63,7 +63,10 @@ class LDAPSettingSerializer(serializers.Serializer):
AUTH_LDAP_SYNC_CRONTAB = serializers.CharField( AUTH_LDAP_SYNC_CRONTAB = serializers.CharField(
required=False, max_length=1024, allow_null=True, label=_('Regularly perform') required=False, max_length=1024, allow_null=True, label=_('Regularly perform')
) )
AUTH_LDAP_CONNECT_TIMEOUT = serializers.IntegerField(required=False, label=_('Connect timeout')) AUTH_LDAP_CONNECT_TIMEOUT = serializers.IntegerField(
min_value=1, max_value=300,
required=False, label=_('Connect timeout'),
)
AUTH_LDAP_SEARCH_PAGED_SIZE = serializers.IntegerField(required=False, label=_('Search paged size')) AUTH_LDAP_SEARCH_PAGED_SIZE = serializers.IntegerField(required=False, label=_('Search paged size'))
AUTH_LDAP = serializers.BooleanField(required=False, label=_('Enable LDAP auth')) AUTH_LDAP = serializers.BooleanField(required=False, label=_('Enable LDAP auth'))

View File

@ -13,5 +13,6 @@ class SSOSettingSerializer(serializers.Serializer):
help_text=_("Other service can using SSO token login to JumpServer without password") help_text=_("Other service can using SSO token login to JumpServer without password")
) )
AUTH_SSO_AUTHKEY_TTL = serializers.IntegerField( AUTH_SSO_AUTHKEY_TTL = serializers.IntegerField(
required=False, label=_('SSO auth key TTL'), help_text=_("Unit: second") required=False, label=_('SSO auth key TTL'), help_text=_("Unit: second"),
min_value=1, max_value=60*30
) )

View File

@ -6,17 +6,22 @@ __all__ = ['CleaningSerializer']
class CleaningSerializer(serializers.Serializer): class CleaningSerializer(serializers.Serializer):
LOGIN_LOG_KEEP_DAYS = serializers.IntegerField( LOGIN_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Login log keep days"), help_text=_("Unit: day") label=_("Login log keep days"), help_text=_("Unit: day")
) )
TASK_LOG_KEEP_DAYS = serializers.IntegerField( TASK_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Task log keep days"), help_text=_("Unit: day") label=_("Task log keep days"), help_text=_("Unit: day")
) )
OPERATE_LOG_KEEP_DAYS = serializers.IntegerField( OPERATE_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Operate log keep days"), help_text=_("Unit: day") label=_("Operate log keep days"), help_text=_("Unit: day")
) )
FTP_LOG_KEEP_DAYS = serializers.IntegerField( FTP_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("FTP log keep days"), help_text=_("Unit: day") label=_("FTP log keep days"), help_text=_("Unit: day")
) )
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = serializers.IntegerField( CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Cloud sync record keep days"), help_text=_("Unit: day") label=_("Cloud sync record keep days"), help_text=_("Unit: day")
) )

View File

@ -9,7 +9,7 @@ __all__ = ['MailTestSerializer', 'EmailSettingSerializer', 'EmailContentSettingS
class MailTestSerializer(serializers.Serializer): class MailTestSerializer(serializers.Serializer):
EMAIL_HOST = serializers.CharField(max_length=1024, required=True) EMAIL_HOST = serializers.CharField(max_length=1024, required=True)
EMAIL_PORT = serializers.IntegerField(default=25) EMAIL_PORT = serializers.IntegerField(default=25, min_value=1, max_value=65535)
EMAIL_HOST_USER = serializers.CharField(max_length=1024) EMAIL_HOST_USER = serializers.CharField(max_length=1024)
EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True) EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True)
EMAIL_FROM = serializers.CharField(required=False, allow_blank=True) EMAIL_FROM = serializers.CharField(required=False, allow_blank=True)

View File

@ -12,9 +12,11 @@ class OtherSettingSerializer(serializers.Serializer):
OTP_ISSUER_NAME = serializers.CharField( OTP_ISSUER_NAME = serializers.CharField(
required=False, max_length=1024, label=_('OTP issuer name'), required=False, max_length=1024, label=_('OTP issuer name'),
) )
OTP_VALID_WINDOW = serializers.IntegerField(label=_("OTP valid window")) OTP_VALID_WINDOW = serializers.IntegerField(
min_value=1, max_value=10,
label=_("OTP valid window")
)
PERIOD_TASK_ENABLED = serializers.BooleanField(required=False, label=_("Enable period task"))
WINDOWS_SSH_DEFAULT_SHELL = serializers.ChoiceField( WINDOWS_SSH_DEFAULT_SHELL = serializers.ChoiceField(
choices=[ choices=[
('cmd', _("CMD")), ('cmd', _("CMD")),
@ -28,4 +30,8 @@ class OtherSettingSerializer(serializers.Serializer):
required=False, label=_("Perm single to ungroup node") required=False, label=_("Perm single to ungroup node")
) )
# 准备废弃
# PERIOD_TASK_ENABLED = serializers.BooleanField(
# required=False, label=_("Enable period task")
# )

View File

@ -69,7 +69,10 @@ class SecurityAuthSerializer(serializers.Serializer):
required=False, default=False, label=_("Only from source login"), required=False, default=False, label=_("Only from source login"),
help_text=_("If enable, CAS、OIDC auth will be failed, if user not exist yet") help_text=_("If enable, CAS、OIDC auth will be failed, if user not exist yet")
) )
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField(label=_("MFA verify TTL"), help_text=_("Unit: second")) SECURITY_MFA_VERIFY_TTL = serializers.IntegerField(
min_value=5, max_value=60*30,
label=_("MFA verify TTL"), help_text=_("Unit: second"),
)
SECURITY_LOGIN_CAPTCHA_ENABLED = serializers.BooleanField( SECURITY_LOGIN_CAPTCHA_ENABLED = serializers.BooleanField(
required=False, default=True, required=False, default=True,
label=_("Enable Login captcha") label=_("Enable Login captcha")

View File

@ -33,8 +33,17 @@ setting_pub_sub = SettingSubPub()
@receiver(post_save, sender=Setting) @receiver(post_save, sender=Setting)
@on_transaction_commit @on_transaction_commit
def refresh_settings_on_changed(sender, instance=None, **kwargs): def refresh_settings_on_changed(sender, instance=None, **kwargs):
if instance: if not instance:
setting_pub_sub.publish(instance.name) return
setting_pub_sub.publish(instance.name)
# 配置变化: PERM_SINGLE_ASSET_TO_UNGROUP_NODE
if instance.name == 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE':
# 清除所有用户授权树已构建的标记,下次访问重新生成
logger.debug('Clean ALL User perm tree built mark')
from perms.utils.asset import UserGrantedTreeRefreshController
UserGrantedTreeRefreshController.clean_all_user_tree_built_mark()
@receiver(django_ready) @receiver(django_ready)

View File

@ -4,8 +4,8 @@
<div class="group"> <div class="group">
<h2>JumpServer Clients</h2> <h2>JumpServer Clients</h2>
<ul> <ul>
<li><a href="/download/jumpserver-client.msi.zip">jumpserver-client-windows.msi.zip</a></li> <li><a href="/download/JumpServer-Client-Installer.msi">jumpserver-client-windows.msi</a></li>
<li><a href="/download/jumpserver-client.dmg">jumpserver-client-macos.dmg</a></li> <li><a href="/download/JumpServer-Client-Installer.dmg">jumpserver-client-macos.dmg</a></li>
</ul> </ul>
</div> </div>

View File

@ -32,7 +32,7 @@ class CommandStore():
hosts = config.get("HOSTS") hosts = config.get("HOSTS")
kwargs = config.get("OTHER", {}) kwargs = config.get("OTHER", {})
self.index = config.get("INDEX") or 'jumpserver' self.index = config.get("INDEX") or 'jumpserver'
self.doc_type = config.get("DOC_TYPE") or 'command_store' self.doc_type = config.get("DOC_TYPE") or '_doc'
self.exact_fields = {} self.exact_fields = {}
self.match_fields = {} self.match_fields = {}

View File

@ -9,6 +9,7 @@ from notifications.notifications import SystemMessage
from terminal.models import Session, Command from terminal.models import Session, Command
from notifications.models import SystemMsgSubscription from notifications.models import SystemMsgSubscription
from notifications.backends import BACKEND from notifications.backends import BACKEND
from orgs.utils import tmp_to_root_org
from common.utils import lazyproperty from common.utils import lazyproperty
logger = get_logger(__name__) logger = get_logger(__name__)
@ -69,7 +70,10 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
def get_text_msg(self) -> dict: def get_text_msg(self) -> dict:
command = self.command command = self.command
session = Session.objects.get(id=command['session'])
with tmp_to_root_org():
session = Session.objects.get(id=command['session'])
session_detail_url = reverse( session_detail_url = reverse(
'api-terminal:session-detail', kwargs={'pk': command['session']}, 'api-terminal:session-detail', kwargs={'pk': command['session']},
external=True, api_to_ui=True external=True, api_to_ui=True
@ -97,7 +101,10 @@ Session: %(session_detail_url)s?oid=%(oid)s
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
command = self.command command = self.command
session = Session.objects.get(id=command['session'])
with tmp_to_root_org():
session = Session.objects.get(id=command['session'])
session_detail_url = reverse( session_detail_url = reverse(
'api-terminal:session-detail', kwargs={'pk': command['session']}, 'api-terminal:session-detail', kwargs={'pk': command['session']},
external=True, api_to_ui=True external=True, api_to_ui=True

View File

@ -37,6 +37,7 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
'can_join': {'label': _('Can join')}, 'can_join': {'label': _('Can join')},
'terminal': {'label': _('Terminal')}, 'terminal': {'label': _('Terminal')},
'is_finished': {'label': _('Is finished')}, 'is_finished': {'label': _('Is finished')},
'can_terminate': {'label': _('Can terminate')},
} }
@ -45,6 +46,9 @@ class SessionDisplaySerializer(SessionSerializer):
class Meta(SessionSerializer.Meta): class Meta(SessionSerializer.Meta):
fields = SessionSerializer.Meta.fields + ['command_amount'] fields = SessionSerializer.Meta.fields + ['command_amount']
extra_kwargs = {
'command_amount': {'label': _('Command amount')},
}
class ReplaySerializer(serializers.Serializer): class ReplaySerializer(serializers.Serializer):

View File

@ -23,9 +23,16 @@ class BaseTerminal(object):
name = f'[{suffix_name}]-{hostname}' name = f'[{suffix_name}]-{hostname}'
self.name = name self.name = name
self.interval = 30 self.interval = 30
self.remote_addr = socket.gethostbyname(hostname) self.remote_addr = self.get_remote_addr(hostname)
self.type = _type self.type = _type
@staticmethod
def get_remote_addr(hostname):
try:
return socket.gethostbyname(hostname)
except socket.gaierror:
return '127.0.0.1'
def start_heartbeat_thread(self): def start_heartbeat_thread(self):
print(f'- Start heartbeat thread => ({self.name})') print(f'- Start heartbeat thread => ({self.name})')
t = threading.Thread(target=self.start_heartbeat) t = threading.Thread(target=self.start_heartbeat)

View File

@ -26,10 +26,11 @@ class Handler(BaseHandler):
meta_display = dict(zip(meta_display_fields, meta_display_values)) meta_display = dict(zip(meta_display_fields, meta_display_values))
apply_system_users = self.ticket.meta.get('apply_system_users') apply_system_users = self.ticket.meta.get('apply_system_users')
apply_applications = self.ticket.meta.get('apply_applications') apply_applications = self.ticket.meta.get('apply_applications')
meta_display.update({ with tmp_to_org(self.ticket.org_id):
'apply_system_users_display': [str(i) for i in SystemUser.objects.filter(id__in=apply_system_users)], meta_display.update({
'apply_applications_display': [str(i) for i in Application.objects.filter(id__in=apply_applications)] 'apply_system_users_display': [str(i) for i in SystemUser.objects.filter(id__in=apply_system_users)],
}) 'apply_applications_display': [str(i) for i in Application.objects.filter(id__in=apply_applications)]
})
return meta_display return meta_display

View File

@ -24,10 +24,11 @@ class Handler(BaseHandler):
meta_display = dict(zip(meta_display_fields, meta_display_values)) meta_display = dict(zip(meta_display_fields, meta_display_values))
apply_assets = self.ticket.meta.get('apply_assets') apply_assets = self.ticket.meta.get('apply_assets')
apply_system_users = self.ticket.meta.get('apply_system_users') apply_system_users = self.ticket.meta.get('apply_system_users')
meta_display.update({ with tmp_to_org(self.ticket.org_id):
'apply_assets_display': [str(i) for i in Asset.objects.filter(id__in=apply_assets)], meta_display.update({
'apply_system_users_display': [str(i)for i in SystemUser.objects.filter(id__in=apply_system_users)] 'apply_assets_display': [str(i) for i in Asset.objects.filter(id__in=apply_assets)],
}) 'apply_system_users_display': [str(i)for i in SystemUser.objects.filter(id__in=apply_system_users)]
})
return meta_display return meta_display
# body # body

View File

@ -13,13 +13,15 @@ EMAIL_TEMPLATE = '''
<div> <div>
<p> <p>
{title} {title}
<a href={ticket_detail_url}>
<strong>{ticket_detail_url_description}</strong>
</a>
</p> </p>
<div> <div>
{body} {body}
</div> </div>
<div>
<a href={ticket_detail_url}>
<strong>{ticket_detail_url_description}</strong>
</a>
</div>
</div> </div>
''' '''
@ -36,13 +38,12 @@ class BaseTicketMessage(UserMessage):
def get_text_msg(self) -> dict: def get_text_msg(self) -> dict:
message = """ message = """
{title}: {ticket_detail_url} -> {ticket_detail_url_description} {title}: {ticket_detail_url}
{body} {body}
""".format( """.format(
title=self.content_title, title=self.content_title,
ticket_detail_url=self.ticket_detail_url, ticket_detail_url=self.ticket_detail_url,
ticket_detail_url_description=_('click here to review'), body=self.ticket.body.replace('<div style="margin-left: 20px;">', '').replace('</div>', '')
body=self.ticket.body
) )
return { return {
'subject': self.subject, 'subject': self.subject,

View File

@ -69,5 +69,9 @@ class ApplySerializer(serializers.Serializer):
'Permission named `{}` already exists'.format(permission_name) 'Permission named `{}` already exists'.format(permission_name)
)) ))
def validate_apply_date_expired(self, apply_date_expired):
apply_date_start = self.root.initial_data['meta'].get('apply_date_start')
if str(apply_date_expired) <= apply_date_start:
error = _('The expiration date should be greater than the start date')
raise serializers.ValidationError(error)
return apply_date_expired

View File

@ -60,3 +60,10 @@ class ApplySerializer(serializers.Serializer):
raise serializers.ValidationError(_( raise serializers.ValidationError(_(
'Permission named `{}` already exists'.format(permission_name) 'Permission named `{}` already exists'.format(permission_name)
)) ))
def validate_apply_date_expired(self, apply_date_expired):
apply_date_start = self.root.initial_data['meta'].get('apply_date_start')
if str(apply_date_expired) <= apply_date_start:
error = _('The expiration date should be greater than the start date')
raise serializers.ValidationError(error)
return apply_date_expired

View File

@ -9,13 +9,15 @@ logger = get_logger(__file__)
def send_ticket_applied_mail_to_assignees(ticket): def send_ticket_applied_mail_to_assignees(ticket):
assignees = ticket.current_node.first().ticket_assignees.all() ticket_assignees = ticket.current_node.first().ticket_assignees.all()
if not assignees: if not ticket_assignees:
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), assignees)) logger.debug(
"Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), ticket_assignees)
)
return return
for assignee in assignees: for ticket_assignee in ticket_assignees:
instance = TicketAppliedToAssignee(assignee, ticket) instance = TicketAppliedToAssignee(ticket_assignee.assignee, ticket)
if settings.DEBUG: if settings.DEBUG:
logger.debug(instance) logger.debug(instance)
instance.publish_async() instance.publish_async()

View File

@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='user', model_name='user',
name='dingtalk_id', name='dingtalk_id',
field=models.CharField(default=None, max_length=128, null=True, unique=True), field=models.CharField(default=None, max_length=128, null=True, unique=True, verbose_name='DingTalk'),
), ),
migrations.AddField( migrations.AddField(
model_name='user', model_name='user',
name='wecom_id', name='wecom_id',
field=models.CharField(default=None, max_length=128, null=True, unique=True), field=models.CharField(default=None, max_length=128, null=True, unique=True, verbose_name='WeCom'),
), ),
] ]

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='user', model_name='user',
name='feishu_id', name='feishu_id',
field=models.CharField(default=None, max_length=128, null=True, unique=True), field=models.CharField(default=None, max_length=128, null=True, unique=True, verbose_name='FeiShu'),
), ),
] ]

View File

@ -656,9 +656,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
need_update_password = models.BooleanField( need_update_password = models.BooleanField(
default=False, verbose_name=_('Need update password') default=False, verbose_name=_('Need update password')
) )
wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128) wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128, verbose_name=_('WeCom'))
dingtalk_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, verbose_name=_('DingTalk'))
feishu_id = models.CharField(null=True, default=None, unique=True, max_length=128) feishu_id = models.CharField(null=True, default=None, unique=True, max_length=128, verbose_name=_('FeiShu'))
def __str__(self): def __str__(self):
return '{0.name}({0.username})'.format(self) return '{0.name}({0.username})'.format(self)

View File

@ -7,6 +7,11 @@ from notifications.notifications import UserMessage
class ResetPasswordMsg(UserMessage): class ResetPasswordMsg(UserMessage):
def __init__(self, user):
super().__init__(user)
self.reset_passwd_token = user.generate_reset_token()
def get_text_msg(self) -> dict: def get_text_msg(self) -> dict:
user = self.user user = self.user
subject = _('Reset password') subject = _('Reset password')
@ -30,7 +35,7 @@ Login direct 👇
""") % { """) % {
'name': user.name, 'name': user.name,
'rest_password_url': reverse('authentication:reset-password', external=True), 'rest_password_url': reverse('authentication:reset-password', external=True),
'rest_password_token': user.generate_reset_token(), 'rest_password_token': self.reset_passwd_token,
'forget_password_url': reverse('authentication:forgot-password', external=True), 'forget_password_url': reverse('authentication:forgot-password', external=True),
'email': user.email, 'email': user.email,
'login_url': reverse('authentication:login', external=True), 'login_url': reverse('authentication:login', external=True),
@ -62,7 +67,7 @@ Login direct 👇
""") % { """) % {
'name': user.name, 'name': user.name,
'rest_password_url': reverse('authentication:reset-password', external=True), 'rest_password_url': reverse('authentication:reset-password', external=True),
'rest_password_token': user.generate_reset_token(), 'rest_password_token': self.reset_passwd_token,
'forget_password_url': reverse('authentication:forgot-password', external=True), 'forget_password_url': reverse('authentication:forgot-password', external=True),
'email': user.email, 'email': user.email,
'login_url': reverse('authentication:login', external=True), 'login_url': reverse('authentication:login', external=True),
@ -98,10 +103,8 @@ If you have any questions, you can contact the administrator.
IP Address: %(ip_address)s IP Address: %(ip_address)s
<br> \n
<br>
Browser: %(browser)s Browser: %(browser)s
<br>
""") % { """) % {
'name': user.name, 'name': user.name,

View File

@ -6,6 +6,7 @@ from rest_framework import serializers
from common.mixins import CommonBulkSerializerMixin from common.mixins import CommonBulkSerializerMixin
from common.permissions import CanUpdateDeleteUser from common.permissions import CanUpdateDeleteUser
from common.validators import PhoneValidator
from orgs.models import ROLE as ORG_ROLE from orgs.models import ROLE as ORG_ROLE
from ..models import User from ..models import User
from ..const import SystemOrOrgRole, PasswordStrategy from ..const import SystemOrOrgRole, PasswordStrategy
@ -28,7 +29,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
is_expired = serializers.BooleanField(read_only=True, label=_('Is expired')) is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
can_update = serializers.SerializerMethodField(label=_('Can update')) can_update = serializers.SerializerMethodField(label=_('Can update'))
can_delete = serializers.SerializerMethodField(label=_('Can delete')) can_delete = serializers.SerializerMethodField(label=_('Can delete'))
can_public_key_auth = serializers.ReadOnlyField(source='can_use_ssh_key_login') can_public_key_auth = serializers.ReadOnlyField(source='can_use_ssh_key_login', label=_('Can public key authentication'))
org_roles = serializers.ListField( org_roles = serializers.ListField(
label=_('Organization role name'), allow_null=True, required=False, label=_('Organization role name'), allow_null=True, required=False,
child=serializers.ChoiceField(choices=ORG_ROLE.choices), default=["User"] child=serializers.ChoiceField(choices=ORG_ROLE.choices), default=["User"]
@ -51,6 +52,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
'date_expired', 'date_joined', 'last_login', # 日期字段 'date_expired', 'date_joined', 'last_login', # 日期字段
'created_by', 'comment', # 通用字段 'created_by', 'comment', # 通用字段
'is_wecom_bound', 'is_dingtalk_bound', 'is_feishu_bound', 'is_wecom_bound', 'is_dingtalk_bound', 'is_feishu_bound',
'wecom_id', 'dingtalk_id', 'feishu_id'
] ]
# 包含不太常用的字段,可以没有 # 包含不太常用的字段,可以没有
fields_verbose = fields_small + [ fields_verbose = fields_small + [
@ -86,6 +88,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
'is_wecom_bound': {'label': _('Is wecom bound')}, 'is_wecom_bound': {'label': _('Is wecom bound')},
'is_dingtalk_bound': {'label': _('Is dingtalk bound')}, 'is_dingtalk_bound': {'label': _('Is dingtalk bound')},
'is_feishu_bound': {'label': _('Is feishu bound')}, 'is_feishu_bound': {'label': _('Is feishu bound')},
'phone': {'validators': [PhoneValidator()]},
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):