Merge branch 'dev' into pr@dev@connectivity_choice

This commit is contained in:
feng626 2025-05-07 17:30:30 +08:00 committed by GitHub
commit dd5bcab4ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 379 additions and 187 deletions

View File

@ -1,9 +1,12 @@
import time
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import render
from django.utils.translation import gettext as _
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from authentication.mixins import AuthMixin
from common.api import JMSModelViewSet
@ -44,6 +47,9 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
@action(methods=['get'], detail=False, url_path='login', permission_classes=[AllowAny])
def login(self, request):
confirm_mfa = request.GET.get('mfa')
if confirm_mfa:
request.session['passkey_confirm_mfa'] = '1'
return render(request, 'authentication/passkey.html', {})
def redirect_to_error(self, error):
@ -64,8 +70,16 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
if not user:
return self.redirect_to_error(_('Auth failed'))
confirm_mfa = request.session.get('passkey_confirm_mfa')
if confirm_mfa:
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index('mfa') + 1
request.session['CONFIRM_TIME'] = int(time.time())
request.session['passkey_confirm_mfa'] = ''
return Response('ok')
try:
self.check_oauth2_auth(user, settings.AUTH_BACKEND_PASSKEY)
self.mark_mfa_ok('passkey', user)
return self.redirect_to_guard_view()
except Exception as e:
msg = getattr(e, 'msg', '') or str(e)

View File

@ -34,6 +34,7 @@ class MFAType(TextChoices):
Email = 'email', _('Email')
Face = 'face', _('Face Recognition')
Radius = 'otp_radius', _('Radius')
Passkey = 'passkey', _('Passkey')
Custom = 'mfa_custom', _('Custom')

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
class BaseMFA(abc.ABC):
placeholder = _('Please input security code')
skip_cache_check = False
has_code = True
def __init__(self, user):
"""

View File

@ -11,6 +11,7 @@ class MFAFace(BaseMFA, AuthFaceMixin):
display_name = MFAType.Face.name
placeholder = 'Face Recognition'
skip_cache_check = True
has_code = False
def _check_code(self, code):
assert self.is_authenticated()

View File

@ -49,4 +49,3 @@ class MFAOtp(BaseMFA):
def help_text_of_disable(self):
return ''

View File

@ -0,0 +1,46 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from authentication.mfa.base import BaseMFA
from ..const import MFAType
class MFAPasskey(BaseMFA):
name = MFAType.Passkey.value
display_name = MFAType.Passkey.name
placeholder = 'Passkey'
has_code = False
def _check_code(self, code):
assert self.is_authenticated()
return False, ''
def is_active(self):
if not self.is_authenticated():
return True
return self.user.passkey_set.count()
@staticmethod
def global_enabled():
return settings.AUTH_PASSKEY
def get_enable_url(self) -> str:
return '/ui/#/profile/passkeys'
def get_disable_url(self) -> str:
return '/ui/#/profile/passkeys'
def disable(self):
pass
def can_disable(self) -> bool:
return False
@staticmethod
def help_text_of_enable():
return _("Using passkey as MFA")
@staticmethod
def help_text_of_disable():
return _("Using passkey as MFA")

View File

@ -5,12 +5,13 @@
<head>
<meta charset="UTF-8">
<title>Login passkey</title>
<script src="{% static "js/jquery-3.6.1.min.js" %}?_=9"></script>
<script src="{% static 'js/jquery-3.6.1.min.js' %}?_=9"></script>
</head>
<body>
<form action='{% url 'api-auth:passkey-auth' %}' method="post" id="loginForm">
<input type="hidden" name="passkeys" id="passkeys"/>
</form>
<form action="{% url 'api-auth:passkey-auth' %}" method="post" id="loginForm">
{% csrf_token %}
<input type="hidden" name="passkeys" id="passkeys"/>
</form>
</body>
<script>
const loginUrl = "/core/auth/login/";

View File

@ -5,11 +5,14 @@ from datetime import datetime, timedelta
from urllib.parse import urljoin, urlparse
from django.conf import settings
from django.shortcuts import reverse
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from audits.models import UserLoginLog
from common.utils import get_ip_city, get_request_ip
from common.utils import get_logger, get_object_or_none
from common.utils import static_or_direct
from users.models import User
from .notifications import DifferentCityLoginMessage
@ -75,3 +78,72 @@ def check_user_property_is_correct(username, **properties):
user = None
break
return user
def get_auth_methods():
return [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'),
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': reverse('authentication:cas:cas-login'),
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'),
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': reverse('authentication:oauth2:login'),
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'),
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': reverse('authentication:dingtalk-qr-login'),
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': reverse('authentication:feishu-qr-login'),
'logo': static('img/login_feishu_logo.png')
},
{
'name': 'Lark',
'enabled': settings.AUTH_LARK,
'url': reverse('authentication:lark-qr-login'),
'logo': static('img/login_lark_logo.png')
},
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
'url': reverse('authentication:slack-qr-login'),
'logo': static('img/login_slack_logo.png')
},
{
'name': _("Passkey"),
'enabled': settings.AUTH_PASSKEY,
'url': reverse('api-auth:passkey-login'),
'logo': static('img/login_passkey.png')
}
]

View File

@ -14,7 +14,6 @@ from django.contrib.auth import login as auth_login, logout as auth_logout
from django.db import IntegrityError
from django.http import HttpRequest
from django.shortcuts import reverse, redirect
from django.templatetags.static import static
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _, get_language
@ -25,13 +24,14 @@ from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.edit import FormView
from common.const import Language
from common.utils import FlashMessageUtil, static_or_direct, safe_next_url
from common.utils import FlashMessageUtil, safe_next_url
from users.utils import (
redirect_user_first_login_or_index
)
from .. import mixins, errors
from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
from ..forms import get_user_login_form_cls
from ..utils import get_auth_methods
__all__ = [
'UserLoginView', 'UserLogoutView',
@ -46,73 +46,17 @@ class UserLoginContextMixin:
def get_support_auth_methods(self):
query_string = self.request.GET.urlencode()
auth_methods = [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': f"{reverse('authentication:openid:login')}?{query_string}",
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': f"{reverse('authentication:cas:cas-login')}?{query_string}",
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': f"{reverse('authentication:saml2:saml2-login')}?{query_string}",
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': f"{reverse('authentication:oauth2:login')}?{query_string}",
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': f"{reverse('authentication:wecom-qr-login')}?{query_string}",
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': f"{reverse('authentication:dingtalk-qr-login')}?{query_string}",
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': f"{reverse('authentication:feishu-qr-login')}?{query_string}",
'logo': static('img/login_feishu_logo.png')
},
{
'name': 'Lark',
'enabled': settings.AUTH_LARK,
'url': f"{reverse('authentication:lark-qr-login')}?{query_string}",
'logo': static('img/login_lark_logo.png')
},
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
'url': f"{reverse('authentication:slack-qr-login')}?{query_string}",
'logo': static('img/login_slack_logo.png')
},
{
'name': _("Passkey"),
'enabled': settings.AUTH_PASSKEY,
'url': f"{reverse('api-auth:passkey-login')}?{query_string}",
'logo': static('img/login_passkey.png')
}
]
return [method for method in auth_methods if method['enabled']]
all_methods = get_auth_methods()
methods = []
for method in all_methods:
method = method.copy()
if not method.get('enabled', False):
continue
url = method.get('url', '')
if query_string and url:
method['url'] = '{}?{}'.format(url, query_string)
methods.append(method)
return methods
@staticmethod
def get_support_langs():

View File

@ -40,6 +40,8 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
if mfa_type == MFAType.Face:
return redirect(reverse('authentication:login-face-capture'))
elif mfa_type == MFAType.Passkey:
return redirect('/api/v1/authentication/passkeys/login/')
return self.do_mfa_check(form, code, mfa_type)
def do_mfa_check(self, form, code, mfa_type):

View File

@ -136,6 +136,8 @@ msgstr ">>> 开始执行测试网关账号可连接性任务"
#: users/templates/users/_msg_user_created.html:13
#: users/templates/users/user_password_verify.html:18
#: xpack/plugins/cloud/serializers/account_attrs.py:27
#: xpack/plugins/cloud/serializers/account_attrs.py:89
#: xpack/plugins/cloud/serializers/account_attrs.py:96
msgid "Password"
msgstr "密码"
@ -746,7 +748,7 @@ msgstr "状态"
#: accounts/serializers/account/account.py:278
#: accounts/templates/accounts/change_secret_failed_info.html:13
#: assets/const/automation.py:9
#: authentication/templates/authentication/passkey.html:173
#: authentication/templates/authentication/passkey.html:174
#: authentication/views/base.py:42 authentication/views/base.py:43
#: authentication/views/base.py:44 common/const/choices.py:67
#: settings/templates/ldap/_msg_import_ldap_user.html:26
@ -855,6 +857,8 @@ msgstr "重复密码"
#: users/serializers/profile.py:186
#: users/templates/users/_msg_user_created.html:12
#: xpack/plugins/cloud/serializers/account_attrs.py:25
#: xpack/plugins/cloud/serializers/account_attrs.py:87
#: xpack/plugins/cloud/serializers/account_attrs.py:94
msgid "Username"
msgstr "用户名"
@ -1905,8 +1909,8 @@ msgid ""
"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name "
"support)"
msgstr ""
"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:"
"db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, "
"2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
#: acls/serializers/base.py:41 assets/serializers/asset/host.py:19
msgid "IP/Host"
@ -1934,8 +1938,8 @@ msgid ""
"With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, "
"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 "
msgstr ""
"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:"
"db8:2de::e13, 2001:db8:1a:1110::/64"
"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, "
"2001:db8:2de::e13, 2001:db8:1a:1110::/64"
#: acls/serializers/rules/rules.py:33
#: authentication/templates/authentication/_msg_oauth_bind.html:12
@ -2179,7 +2183,8 @@ msgstr "云服务"
msgid "Web"
msgstr "Web"
#: assets/const/category.py:16 common/sdk/sms/endpoint.py:20
#: assets/const/category.py:16 common/sdk/sms/custom_file.py:47
#: common/sdk/sms/endpoint.py:20
msgid "Custom type"
msgstr "自定义类型"
@ -2286,7 +2291,7 @@ msgid "Any"
msgstr "任意"
#: assets/const/protocol.py:88 rbac/tree.py:62
#: settings/serializers/security.py:245
#: settings/serializers/security.py:266
msgid "Security"
msgstr "安全"
@ -2884,7 +2889,7 @@ msgstr "端口超出范围 (0-65535)"
msgid "Protocol is required: {}"
msgstr "协议是必填的: {}"
#: assets/serializers/asset/common.py:353
#: assets/serializers/asset/common.py:353 labels/api.py:107
msgid "Invalid data"
msgstr "无效的数据"
@ -3553,29 +3558,29 @@ msgid "Auth Token"
msgstr "认证令牌"
#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73
#: authentication/views/login.py:79 notifications/backends/__init__.py:11
#: authentication/utils.py:114 notifications/backends/__init__.py:11
#: settings/serializers/auth/wecom.py:11 settings/serializers/auth/wecom.py:16
#: users/models/user/__init__.py:130 users/models/user/_source.py:19
msgid "WeCom"
msgstr "企业微信"
#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:97
#: authentication/views/login.py:91 notifications/backends/__init__.py:14
#: audits/signal_handlers/login_log.py:38 authentication/utils.py:126
#: authentication/views/feishu.py:97 notifications/backends/__init__.py:14
#: settings/serializers/auth/feishu.py:12
#: settings/serializers/auth/feishu.py:14 users/models/user/__init__.py:136
#: users/models/user/_source.py:21
msgid "FeiShu"
msgstr "飞书"
#: audits/signal_handlers/login_log.py:40 authentication/views/login.py:103
#: audits/signal_handlers/login_log.py:40 authentication/utils.py:138
#: authentication/views/slack.py:79 notifications/backends/__init__.py:16
#: settings/serializers/auth/slack.py:11 settings/serializers/auth/slack.py:13
#: users/models/user/__init__.py:142 users/models/user/_source.py:23
msgid "Slack"
msgstr "Slack"
#: audits/signal_handlers/login_log.py:41 authentication/views/dingtalk.py:151
#: authentication/views/login.py:85 notifications/backends/__init__.py:12
#: audits/signal_handlers/login_log.py:41 authentication/utils.py:120
#: authentication/views/dingtalk.py:151 notifications/backends/__init__.py:12
#: settings/serializers/auth/dingtalk.py:11 users/models/user/__init__.py:133
#: users/models/user/_source.py:20
msgid "DingTalk"
@ -3586,8 +3591,8 @@ msgstr "钉钉"
msgid "Temporary token"
msgstr "临时密码"
#: audits/signal_handlers/login_log.py:43 authentication/views/login.py:109
#: settings/serializers/auth/passkey.py:8
#: audits/signal_handlers/login_log.py:43 authentication/const.py:37
#: authentication/utils.py:144 settings/serializers/auth/passkey.py:8
#: settings/serializers/auth/passkey.py:11
msgid "Passkey"
msgstr "Passkey"
@ -3671,8 +3676,8 @@ msgstr "ACL 动作是人脸在线"
msgid "No available face feature"
msgstr "没有可用的人脸特征"
#: authentication/api/face.py:100 authentication/mfa/face.py:21
#: authentication/mfa/face.py:23 users/views/profile/face.py:72
#: authentication/api/face.py:100 authentication/mfa/face.py:22
#: authentication/mfa/face.py:24 users/views/profile/face.py:72
msgid "Facial comparison failed"
msgstr "人脸比对失败"
@ -3739,11 +3744,11 @@ msgstr "OpenID 错误"
msgid "Please check if a user with the same username or email already exists"
msgstr "请检查是否已经存在相同用户名或邮箱的用户"
#: authentication/backends/passkey/api.py:37
#: authentication/backends/passkey/api.py:40
msgid "Only register passkey for local user"
msgstr "仅为本地用户注册密钥"
#: authentication/backends/passkey/api.py:65
#: authentication/backends/passkey/api.py:71
msgid "Auth failed"
msgstr "认证失败"
@ -3793,7 +3798,7 @@ msgstr "人脸识别"
msgid "Radius"
msgstr "Radius"
#: authentication/const.py:37
#: authentication/const.py:38
msgid "Custom"
msgstr "自定义"
@ -3993,7 +3998,7 @@ msgstr "动态码"
msgid "Please input security code"
msgstr "请输入动态安全码"
#: authentication/mfa/base.py:27
#: authentication/mfa/base.py:28
msgid ""
"The two-factor code you entered has either already been used or has expired. "
"Please request a new one."
@ -4019,11 +4024,11 @@ msgstr "邮件验证码校验失败"
msgid "Email verification code"
msgstr "邮件验证码"
#: authentication/mfa/face.py:55
#: authentication/mfa/face.py:56
msgid "Bind face to enable"
msgstr "绑定人脸特征以启用"
#: authentication/mfa/face.py:59
#: authentication/mfa/face.py:60
msgid "Unbind face to disable"
msgstr "解绑人脸特征以禁用"
@ -4039,6 +4044,10 @@ msgstr "虚拟 MFA 验证码"
msgid "Virtual OTP based MFA"
msgstr "虚拟 MFA(OTP)"
#: authentication/mfa/passkey.py:42 authentication/mfa/passkey.py:46
msgid "Using passkey as MFA"
msgstr ""
#: authentication/mfa/radius.py:8
msgid "Radius verify code invalid"
msgstr "Radius 校验失败"
@ -4537,17 +4546,17 @@ msgstr "返回"
msgid "Copy success"
msgstr "复制成功"
#: authentication/templates/authentication/passkey.html:162
#: authentication/templates/authentication/passkey.html:163
msgid ""
"This page is not served over HTTPS. Please use HTTPS to ensure security of "
"your credentials."
msgstr "本页面未使用 HTTPS 协议,请使用 HTTPS 协议以确保您的凭据安全。"
#: authentication/templates/authentication/passkey.html:173
#: authentication/templates/authentication/passkey.html:174
msgid "Do you want to retry ?"
msgstr "是否重试 "
#: authentication/utils.py:24 common/utils/ip/geoip/utils.py:24
#: authentication/utils.py:27 common/utils/ip/geoip/utils.py:24
#: xpack/plugins/cloud/const.py:33
msgid "LAN"
msgstr "局域网"
@ -4632,23 +4641,23 @@ msgstr "Lark 已经绑定"
msgid "Failed to get user from Lark"
msgstr "从 Lark 获取用户失败"
#: authentication/views/login.py:219
#: authentication/views/login.py:163
msgid "Redirecting"
msgstr "跳转中"
#: authentication/views/login.py:220
#: authentication/views/login.py:164
msgid "Redirecting to {} authentication"
msgstr "正在跳转到 {} 认证"
#: authentication/views/login.py:247
#: authentication/views/login.py:191
msgid "Login timeout, please try again."
msgstr "登录超时,请重新登录"
#: authentication/views/login.py:292
#: authentication/views/login.py:236
msgid "User email already exists ({})"
msgstr "用户邮箱已存在 ({})"
#: authentication/views/login.py:370
#: authentication/views/login.py:314
msgid ""
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
" Don't close this page"
@ -4656,15 +4665,15 @@ msgstr ""
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
" 不要关闭本页面"
#: authentication/views/login.py:375
#: authentication/views/login.py:319
msgid "No ticket found"
msgstr "没有发现工单"
#: authentication/views/login.py:411
#: authentication/views/login.py:355
msgid "Logout success"
msgstr "退出登录成功"
#: authentication/views/login.py:412
#: authentication/views/login.py:356
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
@ -4759,7 +4768,7 @@ msgstr "企业专业版"
msgid "Ultimate edition"
msgstr "企业旗舰版"
#: common/const/common.py:5 xpack/plugins/cloud/manager.py:454
#: common/const/common.py:5 xpack/plugins/cloud/manager.py:450
#, python-format
msgid "%(name)s was created successfully"
msgstr "%(name)s 创建成功"
@ -5033,6 +5042,10 @@ msgstr "自定义短信文件无效"
msgid "SMS sending failed[%s]: %s"
msgstr "短信发送失败[%s]: %s"
#: common/sdk/sms/custom_file.py:47 common/serializers/common.py:98
msgid "File"
msgstr "文件"
#: common/sdk/sms/endpoint.py:16
msgid "Alibaba cloud"
msgstr "阿里云"
@ -5077,10 +5090,6 @@ msgstr "请在 {} 秒后发送"
msgid "Children"
msgstr "节点"
#: common/serializers/common.py:98
msgid "File"
msgstr "文件"
#: common/serializers/fields.py:139
msgid "Invalid data type"
msgstr "无效的数据"
@ -5202,6 +5211,10 @@ msgstr "你的账号已创建成功"
msgid "JumpServer - An open-source PAM"
msgstr "JumpServer 开源堡垒机"
#: jumpserver/context_processor.py:28
msgid "FIT2CLOUD"
msgstr ""
#: jumpserver/views/celery_flower.py:22
msgid "<h1>Flower service unavailable, check it</h1>"
msgstr "Flower 服务不可用,请检查"
@ -5233,7 +5246,7 @@ msgstr ""
msgid "App Labels"
msgstr "标签管理"
#: labels/models.py:15
#: labels/models.py:15 settings/serializers/security.py:205
msgid "Color"
msgstr "颜色"
@ -7207,7 +7220,7 @@ msgstr "租户 ID"
#: settings/serializers/feature.py:110 terminal/serializers/storage.py:68
#: xpack/plugins/cloud/manager.py:119 xpack/plugins/cloud/manager.py:124
#: xpack/plugins/cloud/models.py:292
#: xpack/plugins/cloud/manager.py:161 xpack/plugins/cloud/models.py:292
msgid "Region"
msgstr "地域"
@ -7618,63 +7631,88 @@ msgstr "每天检测一次,超过预设时间的用户自动禁用"
msgid "Watermark"
msgstr "开启水印"
#: settings/serializers/security.py:200
#: settings/serializers/security.py:199
msgid "Watermark session content"
msgstr "会话水印自定义内容"
#: settings/serializers/security.py:202
msgid "Watermark console content"
msgstr "管理页面水印自定义内容"
#: settings/serializers/security.py:208
msgid "Watermark font size"
msgstr "字体字号"
#: settings/serializers/security.py:211
msgid "Watermark height"
msgstr "单个水印高度"
#: settings/serializers/security.py:214
msgid "Watermark width"
msgstr "单个水印宽度"
#: settings/serializers/security.py:217
msgid "Watermark rotate"
msgstr "水印旋转角度"
#: settings/serializers/security.py:221
msgid "Max idle time (minute)"
msgstr "连接最大空闲时间 (分)"
#: settings/serializers/security.py:201
#: settings/serializers/security.py:222
msgid "If idle time more than it, disconnect connection."
msgstr "提示:如果超过该配置没有操作,连接会被断开"
#: settings/serializers/security.py:204
#: settings/serializers/security.py:225
msgid "Session expire at browser closed"
msgstr "会话在浏览器关闭时过期"
#: settings/serializers/security.py:205
#: settings/serializers/security.py:226
msgid "Whether to expire the session when the user closes their browser."
msgstr "当用户关闭浏览器时是否使会话过期。"
#: settings/serializers/security.py:210
#: settings/serializers/security.py:231
msgid "Allow users to view asset session information"
msgstr "允许用户查看资产在线会话信息"
#: settings/serializers/security.py:212
#: settings/serializers/security.py:233
msgid ""
"When a user connects to an asset, the account selection popup displays the "
"number of active sessions for the current asset (RDP protocol only)."
msgstr ""
"当用户连接资产时,账号选择弹窗中显示当前资产的在线会话数量(仅 rdp 协议)"
#: settings/serializers/security.py:218
#: settings/serializers/security.py:239
msgid "Max online time (hour)"
msgstr "会话连接最大时间 (时)"
#: settings/serializers/security.py:219
#: settings/serializers/security.py:240
msgid "If session connection time more than it, disconnect connection."
msgstr "提示:如果会话连接超过该配置,连接会被断开"
#: settings/serializers/security.py:222
#: settings/serializers/security.py:243
msgid "Remember manual auth"
msgstr "保存手动输入密码"
#: settings/serializers/security.py:225
#: settings/serializers/security.py:246
#: terminal/templates/terminal/_msg_session_sharing.html:10
msgid "Session share"
msgstr "会话分享"
#: settings/serializers/security.py:226
#: settings/serializers/security.py:247
msgid "Enabled, Allows user active session to be shared with other users"
msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作"
#: settings/serializers/security.py:232
#: settings/serializers/security.py:253
msgid "Insecure command alert"
msgstr "危险命令告警"
#: settings/serializers/security.py:235
#: settings/serializers/security.py:256
msgid "Email recipient"
msgstr "邮件收件人"
#: settings/serializers/security.py:236
#: settings/serializers/security.py:257
msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割"
@ -8039,17 +8077,17 @@ msgstr ""
"您的SSH密钥没有设置或已失效请点击 <a href=\"%(user_pubkey_update)s\"> 链接 "
"</a> 更新"
#: templates/_mfa_login_field.html:31
#: templates/_mfa_login_field.html:29
#: users/templates/users/forgot_password.html:101
msgid "Send"
msgstr "发送"
#: templates/_mfa_login_field.html:110
#: templates/_mfa_login_field.html:108
#: users/templates/users/forgot_password.html:176
msgid "Wait: "
msgstr "等待:"
#: templates/_mfa_login_field.html:120
#: templates/_mfa_login_field.html:118
#: users/templates/users/forgot_password.html:192
msgid "The verification code has been sent"
msgstr "验证码已发送"
@ -8162,7 +8200,7 @@ msgstr "会话不存在: {}"
msgid "Session is finished or the protocol not supported"
msgstr "会话已经完成或协议不支持"
#: terminal/api/session/session.py:345
#: terminal/api/session/session.py:345 tickets/api/ticket.py:140
msgid "User does not have permission"
msgstr "用户没有权限"
@ -8710,9 +8748,9 @@ msgid ""
"days. <a href=\"https://learn.microsoft.com/en-us/windows-server/remote/"
"remote-desktop-services/rds-client-access-license\">Detail</a>"
msgstr ""
"如果不存在RDS 将处于试用模式,试用期为 120 天。<a href='https://learn."
"microsoft.com/en-us/windows-server/remote/remote-desktop-services/rds-client-"
"access-license'>详情</a>"
"如果不存在RDS 将处于试用模式,试用期为 120 天。<a href='https://"
"learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/rds-"
"client-access-license'>详情</a>"
#: terminal/serializers/applet_host.py:55
msgid "RDS License Server"
@ -8930,8 +8968,8 @@ msgid ""
"If there are multiple hosts, use a comma (,) to separate them. <br>(For "
"example: http://www.jumpserver.a.com:9100, http://www.jumpserver.b.com:9100)"
msgstr ""
"如果有多个主机,请用逗号 (,) 分隔它们。<br>例如http://www.jumpserver.a."
"com:9100http://www.jumpserver.b.com:9100"
"如果有多个主机,请用逗号 (,) 分隔它们。<br>例如http://"
"www.jumpserver.a.com:9100http://www.jumpserver.b.com:9100"
#: terminal/serializers/storage.py:199
msgid "Index by date"
@ -10529,59 +10567,63 @@ msgstr "同步地区"
msgid "Get instances of region \"%s\" error, error: %s"
msgstr "获取区域 \"%s\" 的实例错误,错误:%s"
#: xpack/plugins/cloud/manager.py:161 xpack/plugins/cloud/models.py:289
msgid "Instance"
msgstr "实例"
#: xpack/plugins/cloud/manager.py:187
#, python-format
msgid "Failed to synchronize the instance \"%s\""
msgstr "无法同步实例 %s"
#: xpack/plugins/cloud/manager.py:379
#: xpack/plugins/cloud/manager.py:378
#, python-format
msgid ""
"The updated platform of asset \"%s\" is inconsistent with the original "
"platform type. Skip platform and protocol updates"
msgstr "资产 \"%s\" 的更新平台与原平台类型不一致。跳过平台和协议更新"
#: xpack/plugins/cloud/manager.py:436
#: xpack/plugins/cloud/manager.py:432
#, python-format
msgid "The asset \"%s\" already exists"
msgstr "资产 \"%s\" 已存在"
#: xpack/plugins/cloud/manager.py:438
#: xpack/plugins/cloud/manager.py:434
#, python-format
msgid "Update asset \"%s\""
msgstr "更新资产 \"%s\""
#: xpack/plugins/cloud/manager.py:441
#: xpack/plugins/cloud/manager.py:437
#, python-format
msgid "Asset \"%s\" has been updated"
msgstr "资产 \"%s\" 已更新"
#: xpack/plugins/cloud/manager.py:450
#: xpack/plugins/cloud/manager.py:446
#, python-format
msgid "Prepare to create asset \"%s\""
msgstr "准备创建资产 %s"
#: xpack/plugins/cloud/manager.py:471
#: xpack/plugins/cloud/manager.py:467
#, python-format
msgid "Set nodes \"%s\""
msgstr "设置节点: \"%s\""
#: xpack/plugins/cloud/manager.py:497
#: xpack/plugins/cloud/manager.py:493
#, python-format
msgid "Set accounts \"%s\""
msgstr "设置账号: %s"
#: xpack/plugins/cloud/manager.py:513
#: xpack/plugins/cloud/manager.py:509
#, python-format
msgid "Set protocols \"%s\""
msgstr "设置协议 \"%s\""
#: xpack/plugins/cloud/manager.py:521
#: xpack/plugins/cloud/manager.py:517
#, python-format
msgid "Set labels \"%s\""
msgstr "设置标签: \"%s\""
#: xpack/plugins/cloud/manager.py:535 xpack/plugins/cloud/tasks.py:31
#: xpack/plugins/cloud/manager.py:531 xpack/plugins/cloud/tasks.py:31
msgid "Run sync instance task"
msgstr "执行同步实例任务"
@ -10671,10 +10713,6 @@ msgstr "同步任务"
msgid "Sync instance task history"
msgstr "同步实例任务历史"
#: xpack/plugins/cloud/models.py:289
msgid "Instance"
msgstr "实例"
#: xpack/plugins/cloud/models.py:306
msgid "Sync instance detail"
msgstr "同步实例详情"
@ -10979,6 +11017,12 @@ msgstr "订阅 ID"
msgid "Auto node classification"
msgstr "自动节点分类"
#: xpack/plugins/cloud/serializers/account_attrs.py:92
#, fuzzy
#| msgid "Domain name"
msgid "domain_name"
msgstr "域名称"
#: xpack/plugins/cloud/serializers/account_attrs.py:98
#: xpack/plugins/cloud/serializers/account_attrs.py:102
#: xpack/plugins/cloud/serializers/account_attrs.py:126

View File

@ -1536,5 +1536,13 @@
"disallowSelfUpdateFields": "Not allowed to modify the current fields yourself",
"forceEnableMFAHelpText": "If force enable, user can not disable by themselves",
"removeWarningMsg": "Are you sure you want to remove",
"setVariable": "Set variable"
"setVariable": "Set variable",
"WatermarkVariableHelpText": "You can use ${key} to read built-in variables in watermark content",
"isConsoleCanUse": "is Console page can use",
"userId": "User ID",
"userName": "User name",
"currentTime": "Current time",
"assetId": "Asset ID",
"assetName": "Asset name",
"assetAddress": "Asset address"
}

View File

@ -1538,5 +1538,15 @@
"disallowSelfUpdateFields": "不允许自己修改当前字段",
"forceEnableMFAHelpText": "如果强制启用,用户无法自行禁用",
"removeWarningMsg": "你确定要移除",
"setVariable": "设置参数"
"setVariable": "设置参数",
"Watermark": "水印",
"WatermarkVariableHelpText": "您可以在自定义水印内容中使用 ${key} 读取内置变量",
"isConsoleCanUse": "管理页面是否可用",
"userId": "用户ID",
"name": "用户名称",
"userName": "用户名",
"currentTime": "当前时间",
"assetId": "资产 ID",
"assetName": "资产名称",
"assetAddress": "资产地址"
}

View File

@ -589,6 +589,13 @@ class Config(dict):
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
'SECURITY_LUNA_REMEMBER_AUTH': True,
'SECURITY_WATERMARK_ENABLED': True,
'SECURITY_WATERMARK_SESSION_CONTENT': '${name}(${userName})\n${assetName}',
'SECURITY_WATERMARK_CONSOLE_CONTENT': '${userName}(${name})',
'SECURITY_WATERMARK_COLOR': 'rgba(184, 184, 184, 0.8)',
'SECURITY_WATERMARK_FONT_SIZE': 13,
'SECURITY_WATERMARK_HEIGHT': 200,
'SECURITY_WATERMARK_WIDTH': 200,
'SECURITY_WATERMARK_ROTATE': 45,
'SECURITY_MFA_VERIFY_TTL': 3600,
'SECURITY_UNCOMMON_USERS_TTL': 999,
'VERIFY_CODE_TTL': 60,

View File

@ -328,9 +328,13 @@ MFA_BACKEND_FACE = 'authentication.mfa.face.MFAFace'
MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius'
MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms'
MFA_BACKEND_EMAIL = 'authentication.mfa.email.MFAEmail'
MFA_BACKEND_PASSKEY = 'authentication.mfa.passkey.MFAPasskey'
MFA_BACKEND_CUSTOM = 'authentication.mfa.custom.MFACustom'
MFA_BACKENDS = [MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS, MFA_BACKEND_FACE, MFA_BACKEND_EMAIL]
MFA_BACKENDS = [
MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS,
MFA_BACKEND_PASSKEY, MFA_BACKEND_FACE, MFA_BACKEND_EMAIL
]
MFA_CUSTOM = CONFIG.MFA_CUSTOM
MFA_CUSTOM_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5

View File

@ -160,9 +160,17 @@ HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH
# 水印
SECURITY_WATERMARK_ENABLED = CONFIG.SECURITY_WATERMARK_ENABLED
SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE
SECURITY_WATERMARK_SESSION_CONTENT = CONFIG.SECURITY_WATERMARK_SESSION_CONTENT
SECURITY_WATERMARK_COLOR = CONFIG.SECURITY_WATERMARK_COLOR
SECURITY_WATERMARK_FONT_SIZE = CONFIG.SECURITY_WATERMARK_FONT_SIZE
SECURITY_WATERMARK_HEIGHT = CONFIG.SECURITY_WATERMARK_HEIGHT
SECURITY_WATERMARK_WIDTH = CONFIG.SECURITY_WATERMARK_WIDTH
SECURITY_WATERMARK_ROTATE = CONFIG.SECURITY_WATERMARK_ROTATE
SECURITY_WATERMARK_CONSOLE_CONTENT = CONFIG.SECURITY_WATERMARK_CONSOLE_CONTENT
SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE
LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND
LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED

View File

@ -37,12 +37,16 @@ class ComponentI18nApi(RetrieveAPIView):
def retrieve(self, request, *args, **kwargs):
name = kwargs.get('name')
lang = request.query_params.get('lang')
flat = request.query_params.get('flat', '1')
data = self.get_component_translations(name)
if lang:
code = Language.to_internal_code(lang, with_filename=True)
data = data.get(code) or {}
flat = request.query_params.get('flat', '1')
if flat == '0':
# 这里要使用原始的 lang, lina 会 merge
data = {lang: data}
if not lang:
return Response(data)
if lang not in Language.choices:
lang = 'en'
code = Language.to_internal_code(lang, with_filename=True)
data = data.get(code) or {}
if flat == '0':
# 这里要使用原始的 lang, lina 会 merge
data = {lang: data}
return Response(data)

View File

@ -29,6 +29,13 @@ class PrivateSettingSerializer(PublicSettingSerializer):
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField()
SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField()
SECURITY_WATERMARK_ENABLED = serializers.BooleanField()
SECURITY_WATERMARK_SESSION_CONTENT = serializers.CharField()
SECURITY_WATERMARK_CONSOLE_CONTENT = serializers.CharField()
SECURITY_WATERMARK_COLOR = serializers.CharField()
SECURITY_WATERMARK_FONT_SIZE = serializers.IntegerField()
SECURITY_WATERMARK_HEIGHT = serializers.IntegerField()
SECURITY_WATERMARK_WIDTH = serializers.IntegerField()
SECURITY_WATERMARK_ROTATE = serializers.IntegerField()
SESSION_EXPIRE_AT_BROWSER_CLOSE = serializers.BooleanField()
VIEW_ASSET_ONLINE_SESSION_INFO = serializers.BooleanField()
PASSWORD_RULE = serializers.DictField()

View File

@ -195,6 +195,27 @@ class SecuritySessionSerializer(serializers.Serializer):
SECURITY_WATERMARK_ENABLED = serializers.BooleanField(
required=True, label=_('Watermark'),
)
SECURITY_WATERMARK_SESSION_CONTENT = serializers.CharField(
required=False, label=_('Watermark session content'),
)
SECURITY_WATERMARK_CONSOLE_CONTENT = serializers.CharField(
required=False, label=_("Watermark console content")
)
SECURITY_WATERMARK_COLOR = serializers.CharField(
max_length=32, default="", label=_("Color")
)
SECURITY_WATERMARK_FONT_SIZE = serializers.IntegerField(
required=False, label=_('Watermark font size'), min_value=1, max_value=100,
)
SECURITY_WATERMARK_HEIGHT = serializers.IntegerField(
required=False, label=_('Watermark height'), default=200
)
SECURITY_WATERMARK_WIDTH = serializers.IntegerField(
required=False, label=_('Watermark width'), default=200
)
SECURITY_WATERMARK_ROTATE = serializers.IntegerField(
required=False, label=_('Watermark rotate'), default=45
)
SECURITY_MAX_IDLE_TIME = serializers.IntegerField(
min_value=1, max_value=99999, required=False,
label=_('Max idle time (minute)'),

View File

@ -5,34 +5,32 @@
onchange="selectChange(this.value)"
>
{% for backend in mfa_backends %}
<option value="{{ backend.name }}"
{% if not backend.is_active %} disabled {% endif %}
>
{{ backend.display_name }}
</option>
<option value="{{ backend.name }}"
{% if not backend.is_active %} disabled {% endif %}
>
{{ backend.display_name }}
</option>
{% endfor %}
</select>
<div class="mfa-div">
{% for backend in mfa_backends %}
<div id="mfa-{{ backend.name }}" class="mfa-field
<div id="mfa-{{ backend.name }}" class="mfa-field
{% if backend.challenge_required %}challenge-required{% endif %}"
style="display: none"
style="display: none"
>
{% if backend.has_code %}
<input type="text" class="form-control input-style"
placeholder="{{ backend.placeholder }}"
>
{% if backend.name == 'face' %}
{% else %}
<input type="text" class="form-control input-style"
placeholder="{{ backend.placeholder }}"
>
{% if backend.challenge_required %}
<button class="btn btn-primary full-width btn-challenge"
type='button' onclick="sendChallengeCode(this)"
>
{% trans 'Send' %}
</button>
{% endif %}
{% endif %}
</div>
{% if backend.challenge_required %}
<button class="btn btn-primary full-width btn-challenge"
type='button' onclick="sendChallengeCode(this)"
>
{% trans 'Send' %}
</button>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
@ -121,7 +119,7 @@
})
}
function onError (responseText, responseJson, status) {
function onError(responseText, responseJson, status) {
setTimeout(function () {
toastr.error(responseJson.detail || responseJson.error);
});