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 .. import serializers
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
class LoginAssetCheckAPI(CreateAPIView):
permission_classes = (IsAppUser, )
permission_classes = (IsAppUser,)
serializer_class = serializers.LoginAssetCheckSerializer
def create(self, request, *args, **kwargs):
@ -57,11 +56,12 @@ class LoginAssetCheckAPI(CreateAPIView):
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_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
@ -74,4 +74,3 @@ class LoginAssetCheckAPI(CreateAPIView):
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass

View File

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

View File

@ -18,7 +18,7 @@ def migrate_old_authbook_to_history(apps, schema_editor):
print()
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:
break
historys = []

View File

@ -5,9 +5,12 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from simple_history.models import HistoricalRecords
from common.utils import lazyproperty
from common.utils import lazyproperty, get_logger
from .base import BaseUser, AbsConnectivity
logger = get_logger(__name__)
__all__ = ['AuthBook']
@ -93,6 +96,24 @@ class AuthBook(BaseUser, AbsConnectivity):
i.comment = 'Update triggered by account {}'.format(self.id)
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):
return self.smart_name

View File

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

View File

@ -38,7 +38,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'username_same_with_user', 'auto_push', 'auto_generate_key',
'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
extra_kwargs = {
'password': {

View File

@ -1,7 +1,7 @@
from django.dispatch import receiver
from django.apps import apps
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 ..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)
@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)
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)

View File

@ -23,6 +23,7 @@ from common.drf.api import SerializerMixin
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true
from assets.models import SystemUser
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@ -88,6 +89,9 @@ class ClientProtocolMixin:
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
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:
options['drivestoredirect:s'] = '*'
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):
if allowed_auth_backends is not None and backend_path not in allowed_auth_backends:
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 True
@ -201,7 +201,7 @@ class AuthMixin(PasswordEncryptionViewMixin):
data = request.POST
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()
self._set_partial_credential_error(username=username, ip=ip, request=request)
@ -285,7 +285,6 @@ class AuthMixin(PasswordEncryptionViewMixin):
elif not user.is_active:
self.raise_credential_error(errors.reason_user_inactive)
self._check_is_local_user(user)
self._check_is_block(user.username)
self._check_login_acl(user, ip)
@ -363,6 +362,12 @@ class AuthMixin(PasswordEncryptionViewMixin):
self.request.session['auth_mfa_required'] = ''
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):
if MFABlockUtils(username, ip).is_block():
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
@ -414,10 +419,12 @@ class AuthMixin(PasswordEncryptionViewMixin):
self.request.session["auth_confirm"] = "1"
return
elif ticket.state_reject:
self.clean_mfa_mark()
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
elif ticket.state_close:
self.clean_mfa_mark()
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)

View File

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

View File

@ -215,7 +215,7 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
user = get_object_or_none(User, dingtalk_id=userid)
if user is None:
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)
return response

View File

@ -82,6 +82,15 @@ class TencentSMS(BaseSMSClient):
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
except TencentCloudSDKException as e:
raise JMSException(code=e.code, detail=e.message)

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
#
import re
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from rest_framework.validators import (
@ -32,3 +34,12 @@ class NoSpecialChars:
raise serializers.ValidationError(
_("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 认证
'AUTH_CAS': False,
'CAS_SERVER_URL': "https://example.com/cas/",
'CAS_ROOT_PROXIED_AS': '',
'CAS_ROOT_PROXIED_AS': 'https://example.com',
'CAS_LOGOUT_COMPLETELY': True,
'CAS_VERSION': 3,
'CAS_USERNAME_ATTRIBUTE': 'uid',
'CAS_APPLY_ATTRIBUTES_TO_USER': False,
'CAS_RENAME_ATTRIBUTES': {},
'CAS_RENAME_ATTRIBUTES': {'uid': 'username'},
'CAS_CREATE_USER': True,
'AUTH_SSO': False,

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ from common.decorator import on_transaction_commit
from common.signals import django_ready
from common.utils import get_logger
from common.utils.connection import RedisPubSub
from assets.models import CommandFilterRule
logger = get_logger(__file__)
@ -91,17 +92,18 @@ def on_org_delete(sender, instance, **kwargs):
root_node.delete()
def _remove_users(model, users, org):
def _remove_users(model, users, org, user_field_name='users'):
with tmp_to_org(org):
if not isinstance(users, (tuple, list, set)):
users = (users, )
m2m_model = model.users.through
reverse = model.users.reverse
user_field = getattr(model, user_field_name)
m2m_model = user_field.through
reverse = user_field.reverse
if reverse:
m2m_field_name = model.users.field.m2m_reverse_field_name()
m2m_field_name = user_field.field.m2m_reverse_field_name()
else:
m2m_field_name = model.users.field.m2m_field_name()
m2m_field_name = user_field.field.m2m_field_name()
relations = m2m_model.objects.filter(**{
'user__in': users,
f'{m2m_field_name}__org_id': org.id
@ -149,6 +151,8 @@ def _clear_users_from_org(org, users):
for m in models:
_remove_users(m, users, org)
_remove_users(CommandFilterRule, users, org, user_field_name='reviewers')
@receiver(m2m_changed, sender=OrganizationMember)
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:
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):
self.user = user
self.key = self.key_template.format(user_id=user.id)
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
def get_redis_client(cls):
return cache.client.get_client(write=True)
@ -69,13 +80,13 @@ class UserGrantedTreeRefreshController:
org_ids = self.client.smembers(self.key)
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)
def have_need_refresh_orgs(self):
builded_org_ids = self.client.smembers(self.key)
builded_org_ids = {org_id.decode() for org_id in builded_org_ids}
have = self.org_ids - builded_org_ids
built_org_ids = self.client.smembers(self.key)
built_org_ids = {org_id.decode() for org_id in built_org_ids}
have = self.org_ids - built_org_ids
return have
def get_need_refresh_orgs_and_fill_up(self):
@ -85,15 +96,18 @@ class UserGrantedTreeRefreshController:
p.smembers(self.key)
p.sadd(self.key, *org_ids)
ret = p.execute()
builded_org_ids = {org_id.decode() for org_id in ret[0]}
ids = org_ids - builded_org_ids
built_org_ids = {org_id.decode() for org_id in ret[0]}
ids = org_ids - built_org_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
@classmethod
@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()
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)
p.srem(key, *org_ids)
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
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
@ensure_in_real_or_default_org
@ -168,7 +183,7 @@ class UserGrantedTreeRefreshController:
).values_list('user_id', flat=True)
user_ids.update(group_user_ids)
cls.remove_builed_orgs_from_users(
cls.remove_built_orgs_from_users(
[current_org.id], user_ids
)
@ -193,7 +208,7 @@ class UserGrantedTreeRefreshController:
with UserGrantedTreeRebuildLock(user_id=user.id):
if force:
orgs = self.orgs
self.set_all_orgs_as_builed()
self.set_all_orgs_as_built()
else:
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)
t = threading.Thread(target=sync_ldap_user)
t.start()
data = {'msg': 'Sync start.'}
data = {'msg': _('Synchronization start, please wait.')}
return Response(data=data, status=409)
# 同步任务正在执行
if sync_util.task_is_running:
data = {'msg': 'synchronization is running.'}
data = {'msg': _('Synchronization is running, please wait.')}
return Response(data=data, status=409)
# 同步任务执行结束
if sync_util.task_is_over:
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 super().list(request, *args, **kwargs)

View File

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

View File

@ -1,4 +1,4 @@
import json
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
@ -10,8 +10,9 @@ __all__ = [
class CASSettingSerializer(serializers.Serializer):
AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth'))
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_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_APPLY_ATTRIBUTES_TO_USER = serializers.BooleanField(required=False, label=_('Enable attributes map'))
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(
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 = 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")
)
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):
LOGIN_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Login log keep days"), help_text=_("Unit: day")
)
TASK_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Task log keep days"), help_text=_("Unit: day")
)
OPERATE_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Operate log keep days"), help_text=_("Unit: day")
)
FTP_LOG_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("FTP log keep days"), help_text=_("Unit: day")
)
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = serializers.IntegerField(
min_value=1, max_value=9999,
label=_("Cloud sync record keep days"), help_text=_("Unit: day")
)

View File

@ -9,7 +9,7 @@ __all__ = ['MailTestSerializer', 'EmailSettingSerializer', 'EmailContentSettingS
class MailTestSerializer(serializers.Serializer):
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_PASSWORD = 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(
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(
choices=[
('cmd', _("CMD")),
@ -28,4 +30,8 @@ class OtherSettingSerializer(serializers.Serializer):
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"),
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(
required=False, default=True,
label=_("Enable Login captcha")

View File

@ -33,8 +33,17 @@ setting_pub_sub = SettingSubPub()
@receiver(post_save, sender=Setting)
@on_transaction_commit
def refresh_settings_on_changed(sender, instance=None, **kwargs):
if instance:
setting_pub_sub.publish(instance.name)
if not instance:
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)

View File

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

View File

@ -32,7 +32,7 @@ class CommandStore():
hosts = config.get("HOSTS")
kwargs = config.get("OTHER", {})
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.match_fields = {}

View File

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

View File

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

View File

@ -23,9 +23,16 @@ class BaseTerminal(object):
name = f'[{suffix_name}]-{hostname}'
self.name = name
self.interval = 30
self.remote_addr = socket.gethostbyname(hostname)
self.remote_addr = self.get_remote_addr(hostname)
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):
print(f'- Start heartbeat thread => ({self.name})')
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))
apply_system_users = self.ticket.meta.get('apply_system_users')
apply_applications = self.ticket.meta.get('apply_applications')
meta_display.update({
'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)]
})
with tmp_to_org(self.ticket.org_id):
meta_display.update({
'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

View File

@ -24,10 +24,11 @@ class Handler(BaseHandler):
meta_display = dict(zip(meta_display_fields, meta_display_values))
apply_assets = self.ticket.meta.get('apply_assets')
apply_system_users = self.ticket.meta.get('apply_system_users')
meta_display.update({
'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)]
})
with tmp_to_org(self.ticket.org_id):
meta_display.update({
'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
# body

View File

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

View File

@ -69,5 +69,9 @@ class ApplySerializer(serializers.Serializer):
'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(_(
'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):
assignees = ticket.current_node.first().ticket_assignees.all()
if not assignees:
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), assignees))
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
if not ticket_assignees:
logger.debug(
"Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), ticket_assignees)
)
return
for assignee in assignees:
instance = TicketAppliedToAssignee(assignee, ticket)
for ticket_assignee in ticket_assignees:
instance = TicketAppliedToAssignee(ticket_assignee.assignee, ticket)
if settings.DEBUG:
logger.debug(instance)
instance.publish_async()

View File

@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='user',
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(
model_name='user',
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(
model_name='user',
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(
default=False, verbose_name=_('Need update password')
)
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)
feishu_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, verbose_name=_('DingTalk'))
feishu_id = models.CharField(null=True, default=None, unique=True, max_length=128, verbose_name=_('FeiShu'))
def __str__(self):
return '{0.name}({0.username})'.format(self)

View File

@ -7,6 +7,11 @@ from notifications.notifications import 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:
user = self.user
subject = _('Reset password')
@ -30,7 +35,7 @@ Login direct 👇
""") % {
'name': user.name,
'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),
'email': user.email,
'login_url': reverse('authentication:login', external=True),
@ -62,7 +67,7 @@ Login direct 👇
""") % {
'name': user.name,
'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),
'email': user.email,
'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
<br>
<br>
\n
Browser: %(browser)s
<br>
""") % {
'name': user.name,

View File

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