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 000000000..23d2b7d89
Binary files /dev/null and b/apps/static/img/login_passkey.png differ
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 @@