diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..950e4a6ca --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# 安全说明 + +JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装. + +如果你发现安全问题,请直接联系我们,我们携手让世界更好: + +- ibuler@fit2cloud.com +- support@fit2cloud.com +- 400-052-0755 diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index ee2fba4d5..2adee4604 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -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 - diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index 948300d93..d7ed099ed 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -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 - diff --git a/apps/assets/migrations/0072_historicalauthbook.py b/apps/assets/migrations/0072_historicalauthbook.py index 9a55e47f3..978584824 100644 --- a/apps/assets/migrations/0072_historicalauthbook.py +++ b/apps/assets/migrations/0072_historicalauthbook.py @@ -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 = [] diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index c4e39bcd4..180dc90f7 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -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 diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 8266c69c2..051db7330 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -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() diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 8c4ebf98d..7ffe51fa0 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -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': { diff --git a/apps/assets/signals_handler/authbook.py b/apps/assets/signals_handler/authbook.py index 2a149d57a..7564e738d 100644 --- a/apps/assets/signals_handler/authbook.py +++ b/apps/assets/signals_handler/authbook.py @@ -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) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index f13254b45..b53a379e7 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -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' diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 79f747ee0..ef76c8b74 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -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() ) diff --git a/apps/authentication/templates/authentication/login_wait_confirm.html b/apps/authentication/templates/authentication/login_wait_confirm.html index 7ee7632f0..36584f26b 100644 --- a/apps/authentication/templates/authentication/login_wait_confirm.html +++ b/apps/authentication/templates/authentication/login_wait_confirm.html @@ -134,6 +134,7 @@ function cancelTicket() { } function cancelCloseConfirm() { + cancelTicket(); window.onbeforeunload = function() {}; window.onunload = function(){}; } diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 6243b80b7..60b82c2cd 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -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 diff --git a/apps/common/message/backends/sms/tencent.py b/apps/common/message/backends/sms/tencent.py index 427e8b7dd..40fcad9dc 100644 --- a/apps/common/message/backends/sms/tencent.py +++ b/apps/common/message/backends/sms/tencent.py @@ -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) diff --git a/apps/common/validators.py b/apps/common/validators.py index 0ce334552..c22ea69da 100644 --- a/apps/common/validators.py +++ b/apps/common/validators.py @@ -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) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index a2e96966f..7a807dd5d 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -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, diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 46be54fe7..507c4781a 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 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 \n" "Language-Team: JumpServer team\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 {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1950,15 +1950,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\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 "Shell(Windows 资产)" -#: 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 "" "
\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 "" "
\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 "" "
\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" -"
\n" -"
\n" +"\n" +"\n" "Browser: %(browser)s\n" -"
\n" " \n" " " msgstr "" @@ -4911,14 +4943,12 @@ msgstr "" "\n" "\n" "IP 地址: %(ip_address)s\n" -"
\n" -"
\n" +"\n" "浏览器: %(browser)s\n" -"
\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 "" "
\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 "" "
\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 "" "
\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 diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py index ab2f25492..d4456ac90 100644 --- a/apps/notifications/notifications.py +++ b/apps/notifications/notifications.py @@ -164,7 +164,6 @@ class SystemMessage(Message): self.send_msg(users, receive_backends) - @classmethod def post_insert_to_db(cls, subscription: SystemMsgSubscription): pass diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py index a8c659108..ac5973596 100644 --- a/apps/ops/notifications.py +++ b/apps/ops/notifications.py @@ -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('
', '; '), 'message': self._msg } diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py index c7aee3c32..aed695b6e 100644 --- a/apps/orgs/signals_handler/common.py +++ b/apps/orgs/signals_handler/common.py @@ -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): diff --git a/apps/perms/utils/asset/user_permission.py b/apps/perms/utils/asset/user_permission.py index 980a0e101..1ea189230 100644 --- a/apps/perms/utils/asset/user_permission.py +++ b/apps/perms/utils/asset/user_permission.py @@ -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() diff --git a/apps/settings/api/ldap.py b/apps/settings/api/ldap.py index e1a34da67..ea90e7970 100644 --- a/apps/settings/api/ldap.py +++ b/apps/settings/api/ldap.py @@ -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) diff --git a/apps/settings/models.py b/apps/settings/models.py index 44867dc6b..d6dc601ee 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -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') diff --git a/apps/settings/serializers/auth/cas.py b/apps/settings/serializers/auth/cas.py index 49a02505f..a111a236e 100644 --- a/apps/settings/serializers/auth/cas.py +++ b/apps/settings/serializers/auth/cas.py @@ -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')) diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py index d4eba4089..8096e5ca2 100644 --- a/apps/settings/serializers/auth/ldap.py +++ b/apps/settings/serializers/auth/ldap.py @@ -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')) diff --git a/apps/settings/serializers/auth/sso.py b/apps/settings/serializers/auth/sso.py index 38481cf2a..c04f38920 100644 --- a/apps/settings/serializers/auth/sso.py +++ b/apps/settings/serializers/auth/sso.py @@ -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 ) diff --git a/apps/settings/serializers/cleaning.py b/apps/settings/serializers/cleaning.py index 182a97eaa..39aae4a80 100644 --- a/apps/settings/serializers/cleaning.py +++ b/apps/settings/serializers/cleaning.py @@ -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") ) diff --git a/apps/settings/serializers/email.py b/apps/settings/serializers/email.py index 2ad7f84e6..a20ff080c 100644 --- a/apps/settings/serializers/email.py +++ b/apps/settings/serializers/email.py @@ -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) diff --git a/apps/settings/serializers/other.py b/apps/settings/serializers/other.py index 131cdf5b4..b94875765 100644 --- a/apps/settings/serializers/other.py +++ b/apps/settings/serializers/other.py @@ -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") + # ) diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 1681b08a3..98bf38d52 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -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") diff --git a/apps/settings/signals_handler.py b/apps/settings/signals_handler.py index 6264fb9e6..fefcc6ec1 100644 --- a/apps/settings/signals_handler.py +++ b/apps/settings/signals_handler.py @@ -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) diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html index da8242a12..df1dbc31c 100644 --- a/apps/templates/resource_download.html +++ b/apps/templates/resource_download.html @@ -4,8 +4,8 @@ diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index 72a39d13c..678698ff6 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -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 = {} diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py index d7581d1e1..c263440c3 100644 --- a/apps/terminal/notifications.py +++ b/apps/terminal/notifications.py @@ -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 diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 083a57fcf..fdf130210 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -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): diff --git a/apps/terminal/startup.py b/apps/terminal/startup.py index f2d8d64e7..7c3fffe69 100644 --- a/apps/terminal/startup.py +++ b/apps/terminal/startup.py @@ -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) diff --git a/apps/tickets/handler/apply_application.py b/apps/tickets/handler/apply_application.py index 4bdd58984..aa298efa9 100644 --- a/apps/tickets/handler/apply_application.py +++ b/apps/tickets/handler/apply_application.py @@ -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 diff --git a/apps/tickets/handler/apply_asset.py b/apps/tickets/handler/apply_asset.py index 42daf6742..fdc7f2103 100644 --- a/apps/tickets/handler/apply_asset.py +++ b/apps/tickets/handler/apply_asset.py @@ -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 diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py index e39edee0f..e0fba3d6c 100644 --- a/apps/tickets/notifications.py +++ b/apps/tickets/notifications.py @@ -13,13 +13,15 @@ EMAIL_TEMPLATE = '''

{title} - - {ticket_detail_url_description} -

{body}
+
+ + {ticket_detail_url_description} + +
''' @@ -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('
', '').replace('
', '') ) return { 'subject': self.subject, diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py index e8217bf2b..dd3be12a7 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py @@ -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 diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py index 489ded1a8..294ecd1ab 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py @@ -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 diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index 18de23371..66edb3828 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -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() diff --git a/apps/users/migrations/0034_auto_20210506_1448.py b/apps/users/migrations/0034_auto_20210506_1448.py index df6257064..f08b34b3e 100644 --- a/apps/users/migrations/0034_auto_20210506_1448.py +++ b/apps/users/migrations/0034_auto_20210506_1448.py @@ -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'), ), ] diff --git a/apps/users/migrations/0036_user_feishu_id.py b/apps/users/migrations/0036_user_feishu_id.py index 3e5882c70..472bc0970 100644 --- a/apps/users/migrations/0036_user_feishu_id.py +++ b/apps/users/migrations/0036_user_feishu_id.py @@ -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'), ), ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 9911bf91b..3a369e808 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -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) diff --git a/apps/users/notifications.py b/apps/users/notifications.py index a129c624e..87eae3b42 100644 --- a/apps/users/notifications.py +++ b/apps/users/notifications.py @@ -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 -
-
+\n Browser: %(browser)s -
""") % { 'name': user.name, diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index fd6bd8969..82e9c1c50 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -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):