From 72b215ed03e2475b83eb1b52bdeae9c72803356a Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:15:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20passkey=20?= =?UTF-8?q?=E7=99=BB=E5=BD=95=20(#11519)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: 基本完成功能 * perf: 优化 passkey * perf: 优化 passkey * perf: 完成 passkey --------- Co-authored-by: ibuler --- apps/audits/signal_handlers/login_log.py | 1 + apps/authentication/api/__init__.py | 12 +- .../backends/passkey/__init__.py | 1 + apps/authentication/backends/passkey/api.py | 59 ++++++ .../backends/passkey/backends.py | 9 + apps/authentication/backends/passkey/fido.py | 155 ++++++++++++++ .../authentication/backends/passkey/models.py | 19 ++ .../backends/passkey/serializer.py | 13 ++ apps/authentication/backends/passkey/urls.py | 9 + .../authentication/migrations/0022_passkey.py | 39 ++++ apps/authentication/mixins.py | 8 +- .../templates/authentication/passkey.html | 191 ++++++++++++++++++ apps/authentication/urls/api_urls.py | 13 +- apps/authentication/urls/view_urls.py | 16 +- apps/authentication/views/login.py | 12 ++ apps/authentication/views/mixins.py | 19 +- apps/common/api/mixin.py | 2 +- apps/common/views/msg.py | 2 +- apps/jumpserver/api.py | 6 +- apps/jumpserver/conf.py | 3 + apps/jumpserver/settings/auth.py | 13 +- apps/locale/ja/LC_MESSAGES/django.mo | 4 +- apps/locale/ja/LC_MESSAGES/django.po | 128 +++++++++--- apps/locale/zh/LC_MESSAGES/django.mo | 4 +- apps/locale/zh/LC_MESSAGES/django.po | 122 ++++++++--- apps/rbac/builtin.py | 1 + apps/rbac/const.py | 1 + apps/settings/api/settings.py | 1 + apps/settings/serializers/auth/__init__.py | 17 +- apps/settings/serializers/auth/passkey.py | 19 ++ apps/settings/serializers/public.py | 1 + apps/settings/serializers/settings.py | 3 +- apps/static/img/login_passkey.png | Bin 0 -> 4741 bytes apps/templates/_header_bar.html | 71 ++++--- apps/templates/flash_message_standalone.html | 7 +- poetry.lock | 59 +++++- pyproject.toml | 3 + 37 files changed, 899 insertions(+), 144 deletions(-) create mode 100644 apps/authentication/backends/passkey/__init__.py create mode 100644 apps/authentication/backends/passkey/api.py create mode 100644 apps/authentication/backends/passkey/backends.py create mode 100644 apps/authentication/backends/passkey/fido.py create mode 100644 apps/authentication/backends/passkey/models.py create mode 100644 apps/authentication/backends/passkey/serializer.py create mode 100644 apps/authentication/backends/passkey/urls.py create mode 100644 apps/authentication/migrations/0022_passkey.py create mode 100644 apps/authentication/templates/authentication/passkey.html create mode 100644 apps/settings/serializers/auth/passkey.py create mode 100644 apps/static/img/login_passkey.png diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py index f74d55e8e..8a4e5d262 100644 --- a/apps/audits/signal_handlers/login_log.py +++ b/apps/audits/signal_handlers/login_log.py @@ -32,6 +32,7 @@ class AuthBackendLabelMapping(LazyObject): backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") + backend_label_mapping[settings.AUTH_BACKEND_PASSKEY] = _("Passkey") return backend_label_mapping def _setup(self): diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 85fda3c1e..17e83813c 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- # -from .connection_token import * -from .token import * -from .mfa import * from .access_key import * from .confirm import * -from .login_confirm import * -from .sso import * -from .wecom import * +from .connection_token import * from .dingtalk import * from .feishu import * +from .login_confirm import * +from .mfa import * from .password import * +from .sso import * from .temp_token import * +from .token import * +from .wecom import * diff --git a/apps/authentication/backends/passkey/__init__.py b/apps/authentication/backends/passkey/__init__.py new file mode 100644 index 000000000..a0957e5c9 --- /dev/null +++ b/apps/authentication/backends/passkey/__init__.py @@ -0,0 +1 @@ +from .backends import * diff --git a/apps/authentication/backends/passkey/api.py b/apps/authentication/backends/passkey/api.py new file mode 100644 index 000000000..8f5414122 --- /dev/null +++ b/apps/authentication/backends/passkey/api.py @@ -0,0 +1,59 @@ +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.viewsets import ModelViewSet + +from authentication.mixins import AuthMixin +from .fido import register_begin, register_complete, auth_begin, auth_complete +from .models import Passkey +from .serializer import PasskeySerializer +from ...views import FlashMessageMixin + + +class PasskeyViewSet(AuthMixin, FlashMessageMixin, ModelViewSet): + serializer_class = PasskeySerializer + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + return Passkey.objects.filter(user=self.request.user) + + @action(methods=['get', 'post'], detail=False, url_path='register') + def register(self, request): + if request.method == 'GET': + register_data, state = register_begin(request) + return JsonResponse(dict(register_data)) + else: + passkey = register_complete(request) + return JsonResponse({'id': passkey.id.__str__(), 'name': passkey.name}) + + @action(methods=['get'], detail=False, url_path='login', permission_classes=[AllowAny]) + def login(self, request): + return render(request, 'authentication/passkey.html', {}) + + def redirect_to_error(self, error): + self.send_auth_signal(success=False, username='unknown', reason='passkey') + return render(self.request, 'authentication/passkey.html', {'error': error}) + + @action(methods=['get', 'post'], detail=False, url_path='auth', permission_classes=[AllowAny]) + def auth(self, request): + if request.method == 'GET': + auth_data = auth_begin(request) + return JsonResponse(dict(auth_data)) + + try: + user = auth_complete(request) + except ValueError as e: + return self.redirect_to_error(str(e)) + + if not user: + return self.redirect_to_error(_('Auth failed')) + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_PASSKEY) + return self.redirect_to_guard_view() + except Exception as e: + msg = getattr(e, 'msg', '') or str(e) + return self.redirect_to_error(msg) diff --git a/apps/authentication/backends/passkey/backends.py b/apps/authentication/backends/passkey/backends.py new file mode 100644 index 000000000..dc7e1349b --- /dev/null +++ b/apps/authentication/backends/passkey/backends.py @@ -0,0 +1,9 @@ +from django.conf import settings + +from ..base import JMSModelBackend + + +class PasskeyAuthBackend(JMSModelBackend): + @staticmethod + def is_enabled(): + return settings.AUTH_PASSKEY diff --git a/apps/authentication/backends/passkey/fido.py b/apps/authentication/backends/passkey/fido.py new file mode 100644 index 000000000..0eaff2fa1 --- /dev/null +++ b/apps/authentication/backends/passkey/fido.py @@ -0,0 +1,155 @@ +import json +from urllib.parse import urlparse + +import fido2.features +from django.conf import settings +from django.utils import timezone +from django.utils.translation import gettext as _ +from fido2.server import Fido2Server +from fido2.utils import websafe_decode, websafe_encode +from fido2.webauthn import PublicKeyCredentialRpEntity, AttestedCredentialData, PublicKeyCredentialUserEntity +from rest_framework.serializers import ValidationError +from user_agents.parsers import parse as ua_parse + +from common.utils import get_logger +from .models import Passkey + +logger = get_logger(__name__) + +try: + fido2.features.webauthn_json_mapping.enabled = True +except: + pass + + +def get_current_platform(request): + ua = ua_parse(request.META["HTTP_USER_AGENT"]) + if 'Safari' in ua.browser.family: + return "Apple" + elif 'Chrome' in ua.browser.family and ua.os.family == "Mac OS X": + return "Chrome on Apple" + elif 'Android' in ua.os.family: + return "Google" + elif "Windows" in ua.os.family: + return "Microsoft" + else: + return "Key" + + +def get_server_id_from_request(request, allowed=()): + origin = request.META.get('HTTP_REFERER') + if not origin: + origin = request.get_host() + p = urlparse(origin) + if p.netloc in allowed or p.hostname in allowed: + return p.hostname + else: + return 'localhost' + + +def default_server_id(request): + domains = settings.ALLOWED_DOMAINS + return get_server_id_from_request(request, allowed=domains) + + +def get_server(request=None): + """Get Server Info from settings and returns a Fido2Server""" + + server_id = settings.FIDO_SERVER_ID or default_server_id(request) + if callable(server_id): + fido_server_id = settings.FIDO_SERVER_ID(request) + elif ',' in server_id: + fido_server_id = get_server_id_from_request(request, allowed=server_id.split(',')) + else: + fido_server_id = server_id + + logger.debug('Fido server id: {}'.format(fido_server_id)) + if callable(settings.FIDO_SERVER_NAME): + fido_server_name = settings.FIDO_SERVER_NAME(request) + else: + fido_server_name = settings.FIDO_SERVER_NAME + + rp = PublicKeyCredentialRpEntity(id=fido_server_id, name=fido_server_name) + return Fido2Server(rp) + + +def get_user_credentials(username): + user_passkeys = Passkey.objects.filter(user__username=username) + return [AttestedCredentialData(websafe_decode(uk.token)) for uk in user_passkeys] + + +def register_begin(request): + server = get_server(request) + user = request.user + user_credentials = get_user_credentials(user.username) + + prefix = request.query_params.get('name', '') + prefix = '(' + prefix + ')' + user_entity = PublicKeyCredentialUserEntity( + id=str(user.id).encode('utf8'), + name=user.username + prefix, + display_name=user.name, + ) + auth_attachment = getattr(settings, 'KEY_ATTACHMENT', None) + data, state = server.register_begin( + user_entity, user_credentials, + authenticator_attachment=auth_attachment, + resident_key_requirement=fido2.webauthn.ResidentKeyRequirement.PREFERRED + ) + request.session['fido2_state'] = state + data = dict(data) + return data, state + + +def register_complete(request): + if not request.session.get("fido2_state"): + raise ValidationError("No state found") + data = request.data + server = get_server(request) + state = request.session.pop("fido2_state") + auth_data = server.register_complete(state, response=data) + encoded = websafe_encode(auth_data.credential_data) + platform = get_current_platform(request) + name = data.pop("key_name", '') or platform + passkey = Passkey.objects.create( + user=request.user, + token=encoded, + name=name, + platform=platform, + credential_id=data.get('id') + ) + return passkey + + +def auth_begin(request): + server = get_server(request) + credentials = [] + + username = None + if request.user.is_authenticated: + username = request.user.username + if username: + credentials = get_user_credentials(username) + auth_data, state = server.authenticate_begin(credentials) + request.session['fido2_state'] = state + return auth_data + + +def auth_complete(request): + server = get_server(request) + data = request.data.get("passkeys") + data = json.loads(data) + cid = data['id'] + + key = Passkey.objects.filter(credential_id=cid, is_active=True).first() + if not key: + raise ValueError(_("This key is not registered")) + + credentials = [AttestedCredentialData(websafe_decode(key.token))] + state = request.session.get('fido2_state') + server.authenticate_complete(state, credentials=credentials, response=data) + + request.session["passkey"] = '{}_{}'.format(key.id, key.name) + key.date_last_used = timezone.now() + key.save(update_fields=['date_last_used']) + return key.user diff --git a/apps/authentication/backends/passkey/models.py b/apps/authentication/backends/passkey/models.py new file mode 100644 index 000000000..0afe55abc --- /dev/null +++ b/apps/authentication/backends/passkey/models.py @@ -0,0 +1,19 @@ +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.db.models import JMSBaseModel + + +class Passkey(JMSBaseModel): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + name = models.CharField(max_length=255, verbose_name=_("Name")) + is_active = models.BooleanField(default=True, verbose_name=_("Enabled")) + platform = models.CharField(max_length=255, default='', verbose_name=_("Platform")) + added_on = models.DateTimeField(auto_now_add=True, verbose_name=_("Added on")) + date_last_used = models.DateTimeField(null=True, default=None, verbose_name=_("Date last used")) + credential_id = models.CharField(max_length=255, unique=True, null=False, verbose_name=_("Credential ID")) + token = models.CharField(max_length=255, null=False, verbose_name=_("Token")) + + def __str__(self): + return self.name diff --git a/apps/authentication/backends/passkey/serializer.py b/apps/authentication/backends/passkey/serializer.py new file mode 100644 index 000000000..681d23dff --- /dev/null +++ b/apps/authentication/backends/passkey/serializer.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from .models import Passkey + + +class PasskeySerializer(serializers.ModelSerializer): + class Meta: + model = Passkey + fields = [ + 'id', 'name', 'is_active', 'created_by', + 'date_last_used', 'date_created', + ] + read_only_fields = list(set(fields) - {'is_active'}) diff --git a/apps/authentication/backends/passkey/urls.py b/apps/authentication/backends/passkey/urls.py new file mode 100644 index 000000000..7a3f5e923 --- /dev/null +++ b/apps/authentication/backends/passkey/urls.py @@ -0,0 +1,9 @@ +from rest_framework.routers import DefaultRouter + +from . import api + +router = DefaultRouter() +router.register('passkeys', api.PasskeyViewSet, 'passkey') + +urlpatterns = [] +urlpatterns += router.urls diff --git a/apps/authentication/migrations/0022_passkey.py b/apps/authentication/migrations/0022_passkey.py new file mode 100644 index 000000000..322d5cf6f --- /dev/null +++ b/apps/authentication/migrations/0022_passkey.py @@ -0,0 +1,39 @@ +# Generated by Django 4.1.10 on 2023-09-08 08:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('authentication', '0021_auto_20230713_1459'), + ] + + operations = [ + migrations.CreateModel( + name='Passkey', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('is_active', models.BooleanField(default=True, verbose_name='Enabled')), + ('platform', models.CharField(default='', max_length=255, verbose_name='Platform')), + ('added_on', models.DateTimeField(auto_now_add=True, verbose_name='Added on')), + ('date_last_used', models.DateTimeField(default=None, null=True, verbose_name='Date last used')), + ('credential_id', models.CharField(max_length=255, unique=True, verbose_name='Credential ID')), + ('token', models.CharField(max_length=255, verbose_name='Token')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index f7d3856b0..b301af6c3 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -132,11 +132,11 @@ class CommonMixin: return user user_id = self.request.session.get('user_id') - auth_password = self.request.session.get('auth_password') + auth_ok = self.request.session.get('auth_password') auth_expired_at = self.request.session.get('auth_password_expired_at') auth_expired = auth_expired_at < time.time() if auth_expired_at else False - if not user_id or not auth_password or auth_expired: + if not user_id or not auth_ok or auth_expired: raise errors.SessionEmptyError() user = get_object_or_404(User, pk=user_id) @@ -479,6 +479,7 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost request.session['auto_login'] = auto_login if not auth_backend: auth_backend = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL) + request.session['auth_backend'] = auth_backend def check_oauth2_auth(self, user: User, auth_backend): @@ -511,7 +512,8 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost def clear_auth_mark(self): keys = [ - 'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id' + 'auth_password', 'user_id', 'auth_confirm_required', + 'auth_ticket_id', 'auth_acl_id' ] for k in keys: self.request.session.pop(k, '') diff --git a/apps/authentication/templates/authentication/passkey.html b/apps/authentication/templates/authentication/passkey.html new file mode 100644 index 000000000..6db7141e3 --- /dev/null +++ b/apps/authentication/templates/authentication/passkey.html @@ -0,0 +1,191 @@ +{% load static %} +{% load i18n %} + + + + + Login passkey + + + +
+ +
+ + + diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 99805a4e8..6d40f68e8 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -4,6 +4,7 @@ from django.urls import path from rest_framework.routers import DefaultRouter from .. import api +from ..backends.passkey.urls import urlpatterns as passkey_urlpatterns app_name = 'authentication' router = DefaultRouter() @@ -13,17 +14,19 @@ router.register('temp-tokens', api.TempTokenViewSet, 'temp-token') router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token') router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token') - urlpatterns = [ path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), path('wecom/qr/unbind//', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'), path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'), - path('dingtalk/qr/unbind//', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'), + path('dingtalk/qr/unbind//', api.DingTalkQRUnBindForAdminApi.as_view(), + name='dingtalk-qr-unbind-for-admin'), path('feishu/qr/unbind/', api.FeiShuQRUnBindForUserApi.as_view(), name='feishu-qr-unbind'), - path('feishu/qr/unbind//', api.FeiShuQRUnBindForAdminApi.as_view(), name='feishu-qr-unbind-for-admin'), - path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'), + path('feishu/qr/unbind//', api.FeiShuQRUnBindForAdminApi.as_view(), + name='feishu-qr-unbind-for-admin'), + path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), + name='feishu-event-subscription-callback'), path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'), @@ -38,4 +41,4 @@ urlpatterns = [ path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), ] -urlpatterns += router.urls +urlpatterns += router.urls + passkey_urlpatterns diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index bad1ded62..ea08eddd0 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -1,11 +1,11 @@ # coding:utf-8 # -from django.urls import path, include from django.db.transaction import non_atomic_requests +from django.urls import path, include -from .. import views from users import views as users_view +from .. import views app_name = 'authentication' @@ -18,7 +18,8 @@ urlpatterns = [ path('logout/', views.UserLogoutView.as_view(), name='logout'), # 原来在users中的 - path('password/forget/previewing/', users_view.UserForgotPasswordPreviewingView.as_view(), name='forgot-previewing'), + path('password/forget/previewing/', users_view.UserForgotPasswordPreviewingView.as_view(), + name='forgot-previewing'), path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'), path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), @@ -26,7 +27,8 @@ urlpatterns = [ path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'), path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'), path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'), - path('wecom/qr/bind//callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'), + path('wecom/qr/bind//callback/', views.WeComQRBindCallbackView.as_view(), + name='wecom-qr-bind-callback'), path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'), path('wecom/oauth/login/', views.WeComOAuthLoginView.as_view(), name='wecom-oauth-login'), path('wecom/oauth/login/callback/', views.WeComOAuthLoginCallbackView.as_view(), name='wecom-oauth-login-callback'), @@ -34,10 +36,12 @@ urlpatterns = [ path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'), path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'), path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'), - path('dingtalk/qr/bind//callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'), + path('dingtalk/qr/bind//callback/', views.DingTalkQRBindCallbackView.as_view(), + name='dingtalk-qr-bind-callback'), path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'), path('dingtalk/oauth/login/', views.DingTalkOAuthLoginView.as_view(), name='dingtalk-oauth-login'), - path('dingtalk/oauth/login/callback/', views.DingTalkOAuthLoginCallbackView.as_view(), name='dingtalk-oauth-login-callback'), + path('dingtalk/oauth/login/callback/', views.DingTalkOAuthLoginCallbackView.as_view(), + name='dingtalk-oauth-login-callback'), path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'), path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'), diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 6405ad771..a222aa4d8 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -90,6 +90,12 @@ class UserLoginContextMixin: 'enabled': settings.AUTH_FEISHU, 'url': reverse('authentication:feishu-qr-login'), 'logo': static('img/login_feishu_logo.png') + }, + { + 'name': _("Passkey"), + 'enabled': settings.AUTH_PASSKEY, + 'url': reverse('api-auth:passkey-login'), + 'logo': static('img/login_passkey.png') } ] return [method for method in auth_methods if method['enabled']] @@ -304,6 +310,12 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView): age = self.request.session.get_expiry_age() self.request.session.set_expiry(age) + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + if request.user.is_authenticated: + response.set_cookie('jms_username', request.user.username) + return response + def get_redirect_url(self, *args, **kwargs): try: user = self.get_user_from_session() diff --git a/apps/authentication/views/mixins.py b/apps/authentication/views/mixins.py index d56206dcd..c78872603 100644 --- a/apps/authentication/views/mixins.py +++ b/apps/authentication/views/mixins.py @@ -16,12 +16,19 @@ class METAMixin: class FlashMessageMixin: @staticmethod - def get_response(redirect_url, title, msg, m_type='message'): - message_data = {'title': title, 'interval': 5, 'redirect_url': redirect_url, m_type: msg} + def get_response(redirect_url='', title='', msg='', m_type='message', interval=5): + message_data = { + 'title': title, 'interval': interval, + 'redirect_url': redirect_url, + } + if m_type == 'error': + message_data['error'] = msg + else: + message_data['message'] = msg return FlashMessageUtil.gen_and_redirect_to(message_data) - def get_success_response(self, redirect_url, title, msg): - return self.get_response(redirect_url, title, msg) + def get_success_response(self, redirect_url, title, msg, **kwargs): + return self.get_response(redirect_url, title, msg, m_type='success', **kwargs) - def get_failed_response(self, redirect_url, title, msg): - return self.get_response(redirect_url, title, msg, 'error') + def get_failed_response(self, redirect_url, title, msg, interval=10): + return self.get_response(redirect_url, title, msg, 'error', interval) diff --git a/apps/common/api/mixin.py b/apps/common/api/mixin.py index bd4b6db2e..4795673d7 100644 --- a/apps/common/api/mixin.py +++ b/apps/common/api/mixin.py @@ -161,7 +161,7 @@ class OrderingFielderFieldsMixin: try: valid_fields = self.get_valid_ordering_fields() except Exception as e: - logger.debug('get_valid_ordering_fields error: %s' % e) + logger.debug('get_valid_ordering_fields error: %s, pass' % e) # 这里千万不要这么用,会让 logging 重复,至于为什么,我也不知道 # logging.debug('get_valid_ordering_fields error: %s' % e) valid_fields = [] diff --git a/apps/common/views/msg.py b/apps/common/views/msg.py index 4b0987da7..bb3cf0f98 100644 --- a/apps/common/views/msg.py +++ b/apps/common/views/msg.py @@ -1,7 +1,7 @@ # +from django.http import HttpResponse from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache -from django.http import HttpResponse from django.views.generic.base import TemplateView from common.utils import bulk_get, FlashMessageUtil diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index d6e31ae3d..b92bee18e 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -266,9 +266,9 @@ class DatesLoginMetricMixin: class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView): http_method_names = ['get'] - - def check_permissions(self, request): - return request.user.has_perm('rbac.view_audit | rbac.view_console') + rbac_perms = { + 'GET': ['rbac.view_audit | rbac.view_console'], + } def get(self, request, *args, **kwargs): data = {} diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 9327fe80b..9b6127c4b 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -382,6 +382,9 @@ class Config(dict): 'AUTH_OAUTH2_USER_ATTR_MAP': { 'name': 'name', 'username': 'username', 'email': 'email' }, + 'AUTH_PASSKEY': False, + 'FIDO_SERVER_ID': '', + 'FIDO_SERVER_NAME': 'JumpServer', # 企业微信 'AUTH_WECOM': False, diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 1654ae63c..63a96fe1d 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -171,6 +171,10 @@ AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/' AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/' AUTH_OAUTH2_LOGOUT_URL_NAME = "authentication:oauth2:logout" +AUTH_PASSKEY = CONFIG.AUTH_PASSKEY +FIDO_SERVER_ID = CONFIG.FIDO_SERVER_ID +FIDO_SERVER_NAME = CONFIG.FIDO_SERVER_NAME +KEY_ATTACHMENT = 2 # 0 any, 1 platform, 2 cross-platform 3 none # 临时 token AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN @@ -202,7 +206,7 @@ AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend' AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend' AUTH_BACKEND_CUSTOM = 'authentication.backends.custom.CustomAuthBackend' - +AUTH_BACKEND_PASSKEY = 'authentication.backends.passkey.PasskeyAuthBackend' AUTHENTICATION_BACKENDS = [ # 只做权限校验 RBAC_BACKEND, @@ -215,6 +219,7 @@ AUTHENTICATION_BACKENDS = [ AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, # Token模式 AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN, + AUTH_BACKEND_PASSKEY ] @@ -254,8 +259,10 @@ if MFA_CUSTOM and MFA_CUSTOM_FILE_MD5 == get_file_md5(MFA_CUSTOM_FILE_PATH): # 自定义多因子认证模块 MFA_BACKENDS.append(MFA_BACKEND_CUSTOM) -AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, - AUTH_BACKEND_OAUTH2] +AUTHENTICATION_BACKENDS_THIRD_PARTY = [ + AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, + AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2 +] ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 84f14fe87..8d72b8d8d 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9153bb31601c68f544e317e653b00e012817159b6f2b60ede4f43b4840dd7ff -size 157917 +oid sha256:169429c30eafe151b854fb90b3f7ea88d8778606f1a71155ac12329e3cfc17ae +size 157506 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 6770daca3..33f1148e6 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-11 15:24+0800\n" +"POT-Creation-Date: 2023-09-04 13:26+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -45,6 +45,7 @@ msgid "Access key" msgstr "アクセスキー" #: accounts/const/account.py:9 assets/models/_user.py:48 +#: authentication/backends/passkey/models.py:16 #: authentication/models/sso_token.py:14 settings/serializers/feature.py:50 msgid "Token" msgstr "トークン" @@ -446,6 +447,7 @@ msgstr "終了日" #: accounts/models/automations/change_secret.py:44 #: accounts/serializers/account/account.py:249 assets/const/automation.py:8 +#: authentication/templates/authentication/passkey.html:173 #: authentication/views/base.py:26 authentication/views/base.py:27 #: authentication/views/base.py:28 common/const/choices.py:20 msgid "Error" @@ -523,6 +525,7 @@ msgstr "アカウントの確認" #: assets/models/label.py:18 assets/models/platform.py:15 #: assets/models/platform.py:88 assets/serializers/asset/common.py:146 #: assets/serializers/platform.py:110 assets/serializers/platform.py:223 +#: authentication/backends/passkey/models.py:10 #: authentication/serializers/connect_token_secret.py:110 ops/mixin.py:21 #: ops/models/adhoc.py:20 ops/models/celery.py:15 ops/models/celery.py:57 #: ops/models/job.py:126 ops/models/playbook.py:28 ops/serializers/job.py:20 @@ -736,7 +739,7 @@ msgstr "ID" #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 #: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 -#: perms/serializers/permission.py:30 rbac/builtin.py:123 +#: perms/serializers/permission.py:30 rbac/builtin.py:124 #: rbac/models/rolebinding.py:49 rbac/serializers/rolebinding.py:17 #: terminal/backends/command/models.py:16 terminal/models/session/session.py:29 #: terminal/models/session/sharing.py:34 terminal/notifications.py:156 @@ -1291,8 +1294,6 @@ msgid "Other" msgstr "その他" #: assets/const/protocol.py:48 -#, fuzzy -#| msgid "SFTP Root" msgid "SFTP root" msgstr "SFTPルート" @@ -1528,6 +1529,7 @@ msgid "Address" msgstr "アドレス" #: assets/models/asset/common.py:151 assets/models/platform.py:119 +#: authentication/backends/passkey/models.py:12 #: authentication/serializers/connect_token_secret.py:115 #: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:321 msgid "Platform" @@ -1748,12 +1750,13 @@ msgid "Public" msgstr "開ける" #: assets/models/platform.py:21 assets/serializers/platform.py:48 -#: settings/serializers/settings.py:65 +#: settings/serializers/settings.py:66 #: users/templates/users/reset_password.html:29 msgid "Setting" msgstr "設定" -#: assets/models/platform.py:38 audits/const.py:49 settings/models.py:36 +#: assets/models/platform.py:38 audits/const.py:49 +#: authentication/backends/passkey/models.py:11 settings/models.py:36 #: terminal/serializers/applet_host.py:33 msgid "Enabled" msgstr "有効化" @@ -2192,7 +2195,7 @@ msgstr "接続" #: audits/const.py:30 authentication/templates/authentication/login.html:252 #: authentication/templates/authentication/login.html:325 -#: templates/_header_bar.html:89 +#: templates/_header_bar.html:95 msgid "Login" msgstr "ログイン" @@ -2407,6 +2410,11 @@ msgstr "DingTalk" msgid "Temporary token" msgstr "仮パスワード" +#: audits/signal_handlers/login_log.py:35 authentication/views/login.py:95 +#: settings/serializers/auth/passkey.py:8 +msgid "Passkey" +msgstr "" + #: audits/tasks.py:101 msgid "Clean audits session task log" msgstr "監査セッション タスク ログのクリーンアップ" @@ -2537,6 +2545,28 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "無効なトークンまたはキャッシュの更新。" +#: authentication/backends/passkey/api.py:52 +#, fuzzy +#| msgid "MFA failed" +msgid "Auth failed" +msgstr "MFAに失敗しました" + +#: authentication/backends/passkey/fido.py:146 +msgid "This key is not registered" +msgstr "このキーは登録されていません" + +#: authentication/backends/passkey/models.py:13 +msgid "Added on" +msgstr "に追加" + +#: authentication/backends/passkey/models.py:14 +msgid "Date last used" +msgstr "最後に使用した日付" + +#: authentication/backends/passkey/models.py:15 +msgid "Credential ID" +msgstr "資格情報ID" + #: authentication/confirm/password.py:16 msgid "Authentication failed password incorrect" msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません)" @@ -3059,7 +3089,7 @@ msgstr "コードエラー" #: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:444 +#: jumpserver/conf.py:447 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -3219,6 +3249,18 @@ msgstr "返品" msgid "Copy success" msgstr "コピー成功" +#: authentication/templates/authentication/passkey.html:162 +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 +msgid "Do you want to retry ?" +msgstr "再試行しますか?" + #: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 #: xpack/plugins/cloud/const.py:29 msgid "LAN" @@ -3295,23 +3337,23 @@ msgstr "本を飛ばすのバインドに成功" msgid "Failed to get user from FeiShu" msgstr "本を飛ばすからユーザーを取得できませんでした" -#: authentication/views/login.py:204 +#: authentication/views/login.py:210 msgid "Redirecting" msgstr "リダイレクト" -#: authentication/views/login.py:205 +#: authentication/views/login.py:211 msgid "Redirecting to {} authentication" msgstr "{} 認証へのリダイレクト" -#: authentication/views/login.py:228 +#: authentication/views/login.py:234 msgid "Login timeout, please try again." msgstr "ログインタイムアウト、もう一度お試しください" -#: authentication/views/login.py:271 +#: authentication/views/login.py:277 msgid "User email already exists ({})" msgstr "ユーザー メールボックスは既に存在します ({})" -#: authentication/views/login.py:349 +#: authentication/views/login.py:361 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -3319,15 +3361,15 @@ msgstr "" "{} 確認を待ちます。彼女/彼へのリンクをコピーすることもできます
\n" " このページを閉じないでください" -#: authentication/views/login.py:354 +#: authentication/views/login.py:366 msgid "No ticket found" msgstr "チケットが見つかりません" -#: authentication/views/login.py:390 +#: authentication/views/login.py:402 msgid "Logout success" msgstr "ログアウト成功" -#: authentication/views/login.py:391 +#: authentication/views/login.py:403 msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" @@ -3648,11 +3690,11 @@ msgstr "特殊文字を含むべきではない" msgid "The mobile phone number format is incorrect" msgstr "携帯電話番号の形式が正しくありません" -#: jumpserver/conf.py:443 +#: jumpserver/conf.py:446 msgid "Create account successfully" msgstr "アカウントを正常に作成" -#: jumpserver/conf.py:445 +#: jumpserver/conf.py:448 msgid "Your account has been created successfully" msgstr "アカウントが正常に作成されました" @@ -4266,27 +4308,27 @@ msgstr "{} 少なくとも1つのシステムロール" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:114 +#: rbac/builtin.py:115 msgid "SystemAdmin" msgstr "システム管理者" -#: rbac/builtin.py:117 +#: rbac/builtin.py:118 msgid "SystemAuditor" msgstr "システム監査人" -#: rbac/builtin.py:120 +#: rbac/builtin.py:121 msgid "SystemComponent" msgstr "システムコンポーネント" -#: rbac/builtin.py:126 +#: rbac/builtin.py:127 msgid "OrgAdmin" msgstr "組織管理者" -#: rbac/builtin.py:129 +#: rbac/builtin.py:130 msgid "OrgAuditor" msgstr "監査員を組織する" -#: rbac/builtin.py:132 +#: rbac/builtin.py:133 msgid "OrgUser" msgstr "組織ユーザー" @@ -4820,6 +4862,30 @@ msgstr "使用状態" msgid "Use nonce" msgstr "Nonceを使用" +#: settings/serializers/auth/passkey.py:11 +msgid "Enable passkey Auth" +msgstr "パスキー認証を有効にする" + +#: settings/serializers/auth/passkey.py:12 +msgid "Only SSL domain can use passkey auth" +msgstr "SSLドメインのみがパスキー認証を使用できます" + +#: settings/serializers/auth/passkey.py:15 +msgid "FIDO server ID" +msgstr "FIDOサーバーID" + +#: settings/serializers/auth/passkey.py:16 +msgid "" +"The hostname can using passkey auth, If not set, will use request host, If " +"multiple domains, use comma to separate" +msgstr "" +"ホスト名はパスキー認証を使用できます。設定されていない場合は、リクエストホス" +"トを使用します。複数のドメインの場合は、コンマで区切ってください。" + +#: settings/serializers/auth/passkey.py:19 +msgid "FIDO server name" +msgstr "FIDOサーバー名" + #: settings/serializers/auth/radius.py:13 msgid "Radius" msgstr "Radius" @@ -5506,7 +5572,7 @@ msgstr "メール受信者" msgid "Multiple user using , split" msgstr "複数のユーザーを使用して、分割" -#: settings/serializers/settings.py:69 +#: settings/serializers/settings.py:70 #, python-format msgid "[%s] %s" msgstr "[%s] %s" @@ -5716,27 +5782,27 @@ msgstr "ヘルプ" msgid "Docs" msgstr "ドキュメント" -#: templates/_header_bar.html:25 +#: templates/_header_bar.html:27 msgid "Commercial support" msgstr "商用サポート" -#: templates/_header_bar.html:76 users/forms/profile.py:44 +#: templates/_header_bar.html:79 users/forms/profile.py:44 msgid "Profile" msgstr "プロフィール" -#: templates/_header_bar.html:79 +#: templates/_header_bar.html:83 msgid "Admin page" msgstr "ページの管理" -#: templates/_header_bar.html:81 +#: templates/_header_bar.html:86 msgid "User page" msgstr "ユーザーページ" -#: templates/_header_bar.html:84 +#: templates/_header_bar.html:90 msgid "API Key" msgstr "API Key" -#: templates/_header_bar.html:85 +#: templates/_header_bar.html:91 msgid "Logout" msgstr "ログアウト" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 018663d21..06f8fb0f8 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d5dbd054fd58335116c493461b5a2553304246b208c77b990ec3e501419ee1f -size 128981 +oid sha256:f2e0f4a3b01d7b23f7c92b4d7be7a422b5c024b0115ae0b465562547eecf2979 +size 128958 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index fb89e4c96..af9421b8b 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: 2023-09-11 15:24+0800\n" +"POT-Creation-Date: 2023-09-04 13:26+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -44,6 +44,7 @@ msgid "Access key" msgstr "Access key" #: accounts/const/account.py:9 assets/models/_user.py:48 +#: authentication/backends/passkey/models.py:16 #: authentication/models/sso_token.py:14 settings/serializers/feature.py:50 msgid "Token" msgstr "Token" @@ -445,6 +446,7 @@ msgstr "结束日期" #: accounts/models/automations/change_secret.py:44 #: accounts/serializers/account/account.py:249 assets/const/automation.py:8 +#: authentication/templates/authentication/passkey.html:173 #: authentication/views/base.py:26 authentication/views/base.py:27 #: authentication/views/base.py:28 common/const/choices.py:20 msgid "Error" @@ -522,6 +524,7 @@ msgstr "账号验证" #: assets/models/label.py:18 assets/models/platform.py:15 #: assets/models/platform.py:88 assets/serializers/asset/common.py:146 #: assets/serializers/platform.py:110 assets/serializers/platform.py:223 +#: authentication/backends/passkey/models.py:10 #: authentication/serializers/connect_token_secret.py:110 ops/mixin.py:21 #: ops/models/adhoc.py:20 ops/models/celery.py:15 ops/models/celery.py:57 #: ops/models/job.py:126 ops/models/playbook.py:28 ops/serializers/job.py:20 @@ -734,7 +737,7 @@ msgstr "ID" #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 #: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 -#: perms/serializers/permission.py:30 rbac/builtin.py:123 +#: perms/serializers/permission.py:30 rbac/builtin.py:124 #: rbac/models/rolebinding.py:49 rbac/serializers/rolebinding.py:17 #: terminal/backends/command/models.py:16 terminal/models/session/session.py:29 #: terminal/models/session/sharing.py:34 terminal/notifications.py:156 @@ -1523,6 +1526,7 @@ msgid "Address" msgstr "地址" #: assets/models/asset/common.py:151 assets/models/platform.py:119 +#: authentication/backends/passkey/models.py:12 #: authentication/serializers/connect_token_secret.py:115 #: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:321 msgid "Platform" @@ -1743,12 +1747,13 @@ msgid "Public" msgstr "开放的" #: assets/models/platform.py:21 assets/serializers/platform.py:48 -#: settings/serializers/settings.py:65 +#: settings/serializers/settings.py:66 #: users/templates/users/reset_password.html:29 msgid "Setting" msgstr "设置" -#: assets/models/platform.py:38 audits/const.py:49 settings/models.py:36 +#: assets/models/platform.py:38 audits/const.py:49 +#: authentication/backends/passkey/models.py:11 settings/models.py:36 #: terminal/serializers/applet_host.py:33 msgid "Enabled" msgstr "启用" @@ -2178,7 +2183,7 @@ msgstr "连接" #: audits/const.py:30 authentication/templates/authentication/login.html:252 #: authentication/templates/authentication/login.html:325 -#: templates/_header_bar.html:89 +#: templates/_header_bar.html:95 msgid "Login" msgstr "登录" @@ -2393,6 +2398,11 @@ msgstr "钉钉" msgid "Temporary token" msgstr "临时密码" +#: audits/signal_handlers/login_log.py:35 authentication/views/login.py:95 +#: settings/serializers/auth/passkey.py:8 +msgid "Passkey" +msgstr "" + #: audits/tasks.py:101 msgid "Clean audits session task log" msgstr "清理审计会话任务日志" @@ -2517,6 +2527,26 @@ msgstr "无效的令牌头。符号字符串不应包含无效字符。" msgid "Invalid token or cache refreshed." msgstr "刷新的令牌或缓存无效。" +#: authentication/backends/passkey/api.py:52 +msgid "Auth failed" +msgstr "认证失败" + +#: authentication/backends/passkey/fido.py:146 +msgid "This key is not registered" +msgstr "此密钥未注册" + +#: authentication/backends/passkey/models.py:13 +msgid "Added on" +msgstr "附加" + +#: authentication/backends/passkey/models.py:14 +msgid "Date last used" +msgstr "最后使用日期" + +#: authentication/backends/passkey/models.py:15 +msgid "Credential ID" +msgstr "凭证 ID" + #: authentication/confirm/password.py:16 msgid "Authentication failed password incorrect" msgstr "认证失败 (用户名或密码不正确)" @@ -3029,7 +3059,7 @@ msgstr "代码错误" #: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:444 +#: jumpserver/conf.py:447 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -3179,6 +3209,16 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" +#: authentication/templates/authentication/passkey.html:162 +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 +msgid "Do you want to retry ?" +msgstr "是否重试 ?" + #: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 #: xpack/plugins/cloud/const.py:29 msgid "LAN" @@ -3255,23 +3295,23 @@ msgstr "绑定 飞书 成功" msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/login.py:204 +#: authentication/views/login.py:210 msgid "Redirecting" msgstr "跳转中" -#: authentication/views/login.py:205 +#: authentication/views/login.py:211 msgid "Redirecting to {} authentication" msgstr "正在跳转到 {} 认证" -#: authentication/views/login.py:228 +#: authentication/views/login.py:234 msgid "Login timeout, please try again." msgstr "登录超时,请重新登录" -#: authentication/views/login.py:271 +#: authentication/views/login.py:277 msgid "User email already exists ({})" msgstr "用户邮箱已存在 ({})" -#: authentication/views/login.py:349 +#: authentication/views/login.py:361 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -3279,15 +3319,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:354 +#: authentication/views/login.py:366 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:390 +#: authentication/views/login.py:402 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:391 +#: authentication/views/login.py:403 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -3606,11 +3646,11 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" -#: jumpserver/conf.py:443 +#: jumpserver/conf.py:446 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:445 +#: jumpserver/conf.py:448 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -4218,27 +4258,27 @@ msgstr "{} 至少有一个系统角色" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:114 +#: rbac/builtin.py:115 msgid "SystemAdmin" msgstr "系统管理员" -#: rbac/builtin.py:117 +#: rbac/builtin.py:118 msgid "SystemAuditor" msgstr "系统审计员" -#: rbac/builtin.py:120 +#: rbac/builtin.py:121 msgid "SystemComponent" msgstr "系统组件" -#: rbac/builtin.py:126 +#: rbac/builtin.py:127 msgid "OrgAdmin" msgstr "组织管理员" -#: rbac/builtin.py:129 +#: rbac/builtin.py:130 msgid "OrgAuditor" msgstr "组织审计员" -#: rbac/builtin.py:132 +#: rbac/builtin.py:133 msgid "OrgUser" msgstr "组织用户" @@ -4771,6 +4811,30 @@ msgstr "使用状态" msgid "Use nonce" msgstr "临时使用" +#: settings/serializers/auth/passkey.py:11 +msgid "Enable passkey Auth" +msgstr "启用 Passkey 认证" + +#: settings/serializers/auth/passkey.py:12 +msgid "Only SSL domain can use passkey auth" +msgstr "只有 SSL 域名可以使用 Passkey(通行密钥)认证" + +#: settings/serializers/auth/passkey.py:15 +msgid "FIDO server ID" +msgstr "Passkey 服务 ID" + +#: settings/serializers/auth/passkey.py:16 +msgid "" +"The hostname can using passkey auth, If not set, will use request host, If " +"multiple domains, use comma to separate" +msgstr "" +"可以使用 Passkey 认证的域名,如果不设置,将使用请求主机, 如果有多个域名,使用" +"逗号分隔, 不需要端口号" + +#: settings/serializers/auth/passkey.py:19 +msgid "FIDO server name" +msgstr "Passkey 服务名称" + #: settings/serializers/auth/radius.py:13 msgid "Radius" msgstr "Radius" @@ -5432,7 +5496,7 @@ msgstr "邮件收件人" msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/settings.py:69 +#: settings/serializers/settings.py:70 #, python-format msgid "[%s] %s" msgstr "[%s] %s" @@ -5636,27 +5700,27 @@ msgstr "帮助" msgid "Docs" msgstr "文档" -#: templates/_header_bar.html:25 +#: templates/_header_bar.html:27 msgid "Commercial support" msgstr "商业支持" -#: templates/_header_bar.html:76 users/forms/profile.py:44 +#: templates/_header_bar.html:79 users/forms/profile.py:44 msgid "Profile" msgstr "个人信息" -#: templates/_header_bar.html:79 +#: templates/_header_bar.html:83 msgid "Admin page" msgstr "管理页面" -#: templates/_header_bar.html:81 +#: templates/_header_bar.html:86 msgid "User page" msgstr "用户页面" -#: templates/_header_bar.html:84 +#: templates/_header_bar.html:90 msgid "API Key" msgstr "API Key" -#: templates/_header_bar.html:85 +#: templates/_header_bar.html:91 msgid "Logout" msgstr "注销登录" diff --git a/apps/rbac/builtin.py b/apps/rbac/builtin.py index a16944d47..8498f7776 100644 --- a/apps/rbac/builtin.py +++ b/apps/rbac/builtin.py @@ -29,6 +29,7 @@ system_user_perms = ( ('authentication', 'connectiontoken', 'add,view,reuse,expire', 'connectiontoken'), ('authentication', 'temptoken', 'add,change,view', 'temptoken'), ('authentication', 'accesskey', '*', '*'), + ('authentication', 'passkey', '*', '*'), ('tickets', 'ticket', 'view', 'ticket'), ) system_user_perms += (user_perms + _view_all_joined_org_perms) diff --git a/apps/rbac/const.py b/apps/rbac/const.py index 20f469611..5825231b5 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -147,6 +147,7 @@ only_system_permissions = ( ('authentication', 'accesskey', '*', '*'), ('authentication', 'superconnectiontoken', '*', '*'), ('authentication', 'temptoken', '*', '*'), + ('authentication', 'passkey', '*', '*'), ('tickets', '*', '*', '*'), ('orgs', 'organization', 'view', 'rootorg'), ('terminal', 'applet', '*', '*'), diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py index bb55dc453..4a8f6cc3d 100644 --- a/apps/settings/api/settings.py +++ b/apps/settings/api/settings.py @@ -46,6 +46,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'cas': serializers.CASSettingSerializer, 'saml2': serializers.SAML2SettingSerializer, 'oauth2': serializers.OAuth2SettingSerializer, + 'passkey': serializers.PasskeySettingSerializer, 'clean': serializers.CleaningSerializer, 'other': serializers.OtherSettingSerializer, 'sms': serializers.SMSSettingSerializer, diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py index 1f9f360f9..aeca390ac 100644 --- a/apps/settings/serializers/auth/__init__.py +++ b/apps/settings/serializers/auth/__init__.py @@ -1,12 +1,13 @@ +from .base import * from .cas import * -from .ldap import * -from .oidc import * -from .radius import * from .dingtalk import * from .feishu import * -from .wecom import * -from .sso import * -from .base import * -from .sms import * -from .saml2 import * +from .ldap import * from .oauth2 import * +from .oidc import * +from .passkey import * +from .radius import * +from .saml2 import * +from .sms import * +from .sso import * +from .wecom import * diff --git a/apps/settings/serializers/auth/passkey.py b/apps/settings/serializers/auth/passkey.py new file mode 100644 index 000000000..e63a51e91 --- /dev/null +++ b/apps/settings/serializers/auth/passkey.py @@ -0,0 +1,19 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +__all__ = ['PasskeySettingSerializer'] + + +class PasskeySettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('Passkey') + + AUTH_PASSKEY = serializers.BooleanField( + default=False, label=_('Enable passkey Auth'), + help_text=_('Only SSL domain can use passkey auth') + ) + FIDO_SERVER_ID = serializers.CharField( + max_length=255, label=_('FIDO server ID'), required=False, allow_blank=True, + help_text=_('The hostname can using passkey auth, If not set, will use request host, ' + 'If multiple domains, use comma to separate') + ) + FIDO_SERVER_NAME = serializers.CharField(max_length=255, label=_('FIDO server name')) diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index 370727b2c..1b5c78609 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -35,6 +35,7 @@ class PrivateSettingSerializer(PublicSettingSerializer): HELP_DOCUMENT_URL = serializers.CharField() HELP_SUPPORT_URL = serializers.CharField() + AUTH_PASSKEY = serializers.BooleanField() AUTH_WECOM = serializers.BooleanField() AUTH_DINGTALK = serializers.BooleanField() AUTH_FEISHU = serializers.BooleanField() diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 2d38ed74a..743f7cc60 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -9,7 +9,7 @@ from .auth import ( CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer, WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer, TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, AuthSettingSerializer, - SAML2SettingSerializer, OAuth2SettingSerializer, + SAML2SettingSerializer, OAuth2SettingSerializer, PasskeySettingSerializer, CustomSMSSettingSerializer, ) from .basic import BasicSettingSerializer @@ -47,6 +47,7 @@ class SettingsSerializer( TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, CustomSMSSettingSerializer, + PasskeySettingSerializer ): CACHE_KEY = 'SETTING_FIELDS_MAPPING' diff --git a/apps/static/img/login_passkey.png b/apps/static/img/login_passkey.png new file mode 100644 index 0000000000000000000000000000000000000000..23d2b7d89db639180b14305992a8817317eb945c GIT binary patch literal 4741 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 T5k+VN00000NkvXXu0mjfutMo& literal 0 HcmV?d00001 diff --git a/apps/templates/_header_bar.html b/apps/templates/_header_bar.html index 62ddb52c4..72fb77201 100644 --- a/apps/templates/_header_bar.html +++ b/apps/templates/_header_bar.html @@ -14,13 +14,15 @@