mirror of https://github.com/jumpserver/jumpserver
				
				
				
			feat: 支持 passkey 登录 (#11519)
* perf: 基本完成功能 * perf: 优化 passkey * perf: 优化 passkey * perf: 完成 passkey --------- Co-authored-by: ibuler <ibuler@qq.com>pull/11543/head
							parent
							
								
									d7ca1a09d4
								
							
						
					
					
						commit
						72b215ed03
					
				| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
from .backends import *
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
from ..base import JMSModelBackend
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PasskeyAuthBackend(JMSModelBackend):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def is_enabled():
 | 
			
		||||
        return settings.AUTH_PASSKEY
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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'})
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -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, '')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,191 @@
 | 
			
		|||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <title>Login passkey</title>
 | 
			
		||||
    <script src="{% static "js/jquery-3.6.1.min.js" %}?_=9"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<form action='{% url 'api-auth:passkey-auth' %}' method="post" id="loginForm">
 | 
			
		||||
    <input type="hidden" name="passkeys" id="passkeys"/>
 | 
			
		||||
</form>
 | 
			
		||||
</body>
 | 
			
		||||
<script>
 | 
			
		||||
    const loginUrl = "/core/auth/login/";
 | 
			
		||||
    window.conditionalUI = false;
 | 
			
		||||
    window.conditionUIAbortController = new AbortController();
 | 
			
		||||
    window.conditionUIAbortSignal = conditionUIAbortController.signal;
 | 
			
		||||
 | 
			
		||||
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
 | 
			
		||||
 | 
			
		||||
    // Use a lookup table to find the index.
 | 
			
		||||
    const lookup = new Uint8Array(256)
 | 
			
		||||
    for (let i = 0; i < chars.length; i++) {
 | 
			
		||||
        lookup[chars.charCodeAt(i)] = i
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const encode = function (arraybuffer) {
 | 
			
		||||
        const bytes = new Uint8Array(arraybuffer)
 | 
			
		||||
        let i;
 | 
			
		||||
        const len = bytes.length;
 | 
			
		||||
        let base64url = ''
 | 
			
		||||
 | 
			
		||||
        for (i = 0; i < len; i += 3) {
 | 
			
		||||
            base64url += chars[bytes[i] >> 2]
 | 
			
		||||
            base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]
 | 
			
		||||
            base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]
 | 
			
		||||
            base64url += chars[bytes[i + 2] & 63]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ((len % 3) === 2) {
 | 
			
		||||
            base64url = base64url.substring(0, base64url.length - 1)
 | 
			
		||||
        } else if (len % 3 === 1) {
 | 
			
		||||
            base64url = base64url.substring(0, base64url.length - 2)
 | 
			
		||||
        }
 | 
			
		||||
        return base64url
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const decode = function (base64string) {
 | 
			
		||||
        const bufferLength = base64string.length * 0.75
 | 
			
		||||
        const len = base64string.length;
 | 
			
		||||
        let i;
 | 
			
		||||
        let p = 0
 | 
			
		||||
        let encoded1;
 | 
			
		||||
        let encoded2;
 | 
			
		||||
        let encoded3;
 | 
			
		||||
        let encoded4
 | 
			
		||||
 | 
			
		||||
        const bytes = new Uint8Array(bufferLength)
 | 
			
		||||
 | 
			
		||||
        for (i = 0; i < len; i += 4) {
 | 
			
		||||
            encoded1 = lookup[base64string.charCodeAt(i)]
 | 
			
		||||
            encoded2 = lookup[base64string.charCodeAt(i + 1)]
 | 
			
		||||
            encoded3 = lookup[base64string.charCodeAt(i + 2)]
 | 
			
		||||
            encoded4 = lookup[base64string.charCodeAt(i + 3)]
 | 
			
		||||
 | 
			
		||||
            bytes[p++] = (encoded1 << 2) | (encoded2 >> 4)
 | 
			
		||||
            bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2)
 | 
			
		||||
            bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63)
 | 
			
		||||
        }
 | 
			
		||||
        return bytes.buffer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function checkConditionalUI(form) {
 | 
			
		||||
        if (!navigator.credentials) {
 | 
			
		||||
            alert('WebAuthn is not supported in this browser')
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        if (window.PublicKeyCredential && PublicKeyCredential.isConditionalMediationAvailable) {
 | 
			
		||||
            // Check if conditional mediation is available.
 | 
			
		||||
            PublicKeyCredential.isConditionalMediationAvailable().then((result) => {
 | 
			
		||||
                window.conditionalUI = result;
 | 
			
		||||
                if (!window.conditionalUI) {
 | 
			
		||||
                    alert("Conditional UI is not available. Please use the legacy UI.");
 | 
			
		||||
                } else {
 | 
			
		||||
                    return true
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const publicKeyCredentialToJSON = (pubKeyCred) => {
 | 
			
		||||
        if (pubKeyCred instanceof Array) {
 | 
			
		||||
            const arr = []
 | 
			
		||||
            for (const i of pubKeyCred) {
 | 
			
		||||
                arr.push(publicKeyCredentialToJSON(i))
 | 
			
		||||
            }
 | 
			
		||||
            return arr
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (pubKeyCred instanceof ArrayBuffer) {
 | 
			
		||||
            return encode(pubKeyCred)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (pubKeyCred instanceof Object) {
 | 
			
		||||
            const obj = {}
 | 
			
		||||
            for (const key in pubKeyCred) {
 | 
			
		||||
                obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
 | 
			
		||||
            }
 | 
			
		||||
            return obj
 | 
			
		||||
        }
 | 
			
		||||
        return pubKeyCred
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function GetAssertReq(getAssert) {
 | 
			
		||||
        getAssert.publicKey.challenge = decode(getAssert.publicKey.challenge)
 | 
			
		||||
 | 
			
		||||
        for (const allowCred of getAssert.publicKey.allowCredentials) {
 | 
			
		||||
            allowCred.id = decode(allowCred.id)
 | 
			
		||||
        }
 | 
			
		||||
        return getAssert
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function startAuthn(form, conditionalUI = false) {
 | 
			
		||||
        window.loginForm = form
 | 
			
		||||
        fetch('/api/v1/authentication/passkeys/auth/', {method: 'GET'}).then(function (response) {
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                return response.json().then(function (req) {
 | 
			
		||||
                    return GetAssertReq(req)
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
            throw new Error('No credential available to authenticate!')
 | 
			
		||||
        }).then(function (options) {
 | 
			
		||||
            if (conditionalUI) {
 | 
			
		||||
                options.mediation = 'conditional'
 | 
			
		||||
                options.signal = window.conditionUIAbortSignal
 | 
			
		||||
            } else {
 | 
			
		||||
                window.conditionUIAbortController.abort()
 | 
			
		||||
            }
 | 
			
		||||
            return navigator.credentials.get(options)
 | 
			
		||||
        }).then(function (assertion) {
 | 
			
		||||
            const pk = $('#passkeys')
 | 
			
		||||
            if (pk.length === 0) {
 | 
			
		||||
                retry("Did you add the 'passkeys' hidden input field")
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            pk.val(JSON.stringify(publicKeyCredentialToJSON(assertion)))
 | 
			
		||||
            const x = document.getElementById(window.loginForm)
 | 
			
		||||
            if (x === null || x === undefined) {
 | 
			
		||||
                console.error('Did you pass the correct form id to auth function')
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            x.submit()
 | 
			
		||||
        }).catch(function (err) {
 | 
			
		||||
            retry(err)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function safeStartAuthn(form) {
 | 
			
		||||
        checkConditionalUI('loginForm')
 | 
			
		||||
        const errorMsg = "{% trans 'This page is not served over HTTPS. Please use HTTPS to ensure security of your credentials.' %}"
 | 
			
		||||
        const isSafe = window.location.protocol === 'https:'
 | 
			
		||||
        if (!isSafe && location.hostname !== 'localhost') {
 | 
			
		||||
            alert(errorMsg)
 | 
			
		||||
            window.location.href = loginUrl
 | 
			
		||||
        } else {
 | 
			
		||||
            setTimeout(() => startAuthn('loginForm'), 100)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function retry(error) {
 | 
			
		||||
        const fullError = "{% trans 'Error' %}" + ': ' + error + "\n\n " + "{% trans 'Do you want to retry ?' %}"
 | 
			
		||||
        const result = confirm(fullError)
 | 
			
		||||
        if (result) {
 | 
			
		||||
            safeStartAuthn()
 | 
			
		||||
        } else {
 | 
			
		||||
            window.location.href = loginUrl
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {% if not error %}
 | 
			
		||||
        window.onload = function () {
 | 
			
		||||
            safeStartAuthn()
 | 
			
		||||
        }
 | 
			
		||||
    {% else %}
 | 
			
		||||
        const error = "{{ error }}"
 | 
			
		||||
        retry(error)
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</script>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -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/<uuid:user_id>/', 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/<uuid:user_id>/', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'),
 | 
			
		||||
    path('dingtalk/qr/unbind/<uuid:user_id>/', 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/<uuid:user_id>/', 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/<uuid:user_id>/', 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'),
 | 
			
		||||
    path('wecom/qr/bind/<uuid:user_id>/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/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
 | 
			
		||||
    path('dingtalk/qr/bind/<uuid:user_id>/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'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = []
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:f9153bb31601c68f544e317e653b00e012817159b6f2b60ede4f43b4840dd7ff
 | 
			
		||||
size 157917
 | 
			
		||||
oid sha256:169429c30eafe151b854fb90b3f7ea88d8778606f1a71155ac12329e3cfc17ae
 | 
			
		||||
size 157506
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\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 <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
 | 
			
		||||
"                  Don't close this page"
 | 
			
		||||
| 
						 | 
				
			
			@ -3319,15 +3361,15 @@ msgstr ""
 | 
			
		|||
"<b>{}</b> 確認を待ちます。彼女/彼へのリンクをコピーすることもできます <br/>\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 "ログアウト"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:4d5dbd054fd58335116c493461b5a2553304246b208c77b990ec3e501419ee1f
 | 
			
		||||
size 128981
 | 
			
		||||
oid sha256:f2e0f4a3b01d7b23f7c92b4d7be7a422b5c024b0115ae0b465562547eecf2979
 | 
			
		||||
size 128958
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 <ibuler@qq.com>\n"
 | 
			
		||||
"Language-Team: JumpServer team<ibuler@qq.com>\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 <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
 | 
			
		||||
"                  Don't close this page"
 | 
			
		||||
| 
						 | 
				
			
			@ -3279,15 +3319,15 @@ msgstr ""
 | 
			
		|||
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\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 "注销登录"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -147,6 +147,7 @@ only_system_permissions = (
 | 
			
		|||
    ('authentication', 'accesskey', '*', '*'),
 | 
			
		||||
    ('authentication', 'superconnectiontoken', '*', '*'),
 | 
			
		||||
    ('authentication', 'temptoken', '*', '*'),
 | 
			
		||||
    ('authentication', 'passkey', '*', '*'),
 | 
			
		||||
    ('tickets', '*', '*', '*'),
 | 
			
		||||
    ('orgs', 'organization', 'view', 'rootorg'),
 | 
			
		||||
    ('terminal', 'applet', '*', '*'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'))
 | 
			
		||||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.6 KiB  | 
| 
						 | 
				
			
			@ -20,7 +20,9 @@
 | 
			
		|||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
 | 
			
		||||
                        <a class="count-info"
 | 
			
		||||
                           href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1"
 | 
			
		||||
                           target="_blank">
 | 
			
		||||
                            <i class="fa fa-suitcase"></i>
 | 
			
		||||
                            <span class="m-r-sm text-muted welcome-message">{% trans 'Commercial support' %}</span>
 | 
			
		||||
                        </a>
 | 
			
		||||
| 
						 | 
				
			
			@ -32,11 +34,11 @@
 | 
			
		|||
            <li class="dropdown">
 | 
			
		||||
                <a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
 | 
			
		||||
                    <i class="fa fa-globe"></i>
 | 
			
		||||
                    {% ifequal request.COOKIES.django_language 'en' %}
 | 
			
		||||
                    {% if request.COOKIES.django_language == 'en' %}
 | 
			
		||||
                        <span class="m-r-sm text-muted welcome-message">English<b class="caret"></b></span>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        <span class="m-r-sm text-muted welcome-message">中文<b class="caret"></b></span>
 | 
			
		||||
                    {% endifequal %}
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </a>
 | 
			
		||||
 | 
			
		||||
                <ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
 | 
			
		||||
| 
						 | 
				
			
			@ -73,15 +75,19 @@
 | 
			
		|||
                    </span>
 | 
			
		||||
                </a>
 | 
			
		||||
                    <ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
 | 
			
		||||
                    <li><a href="{% url 'users:user-profile' %}"><i class="fa fa-cogs"> </i><span> {% trans 'Profile' %}</span></a></li>
 | 
			
		||||
                    <li><a href="{% url 'users:user-profile' %}"><i
 | 
			
		||||
                            class="fa fa-cogs"> </i><span> {% trans 'Profile' %}</span></a></li>
 | 
			
		||||
                        {% if request.user.can_admin_or_audit_current_org %}
 | 
			
		||||
                            {% if request.COOKIES.IN_ADMIN_PAGE == 'No' %}
 | 
			
		||||
                            <li><a id="switch_admin"><i class="fa fa-exchange"></i><span> {% trans 'Admin page' %}</span></a></li>
 | 
			
		||||
                                <li><a id="switch_admin"><i
 | 
			
		||||
                                        class="fa fa-exchange"></i><span> {% trans 'Admin page' %}</span></a></li>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <li><a id="switch_user"><i class="fa fa-exchange"></i><span> {% trans 'User page' %}</span></a></li>
 | 
			
		||||
                                <li><a id="switch_user"><i
 | 
			
		||||
                                        class="fa fa-exchange"></i><span> {% trans 'User page' %}</span></a></li>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    <li><a href="#" data-toggle="modal" data-target="#access_key_modal" tabindex="0"><i class="fa fa-key"></i> {% trans 'API Key' %}</a></li>
 | 
			
		||||
                        <li><a href="#" data-toggle="modal" data-target="#access_key_modal" tabindex="0"><i
 | 
			
		||||
                                class="fa fa-key"></i> {% trans 'API Key' %}</a></li>
 | 
			
		||||
                    <li><a href="{% url 'authentication:logout' %}"><i class="fa fa-sign-out"></i> {% trans 'Logout' %}</a></li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                {% else %}
 | 
			
		||||
| 
						 | 
				
			
			@ -116,20 +122,19 @@
 | 
			
		|||
<script>
 | 
			
		||||
$(document).ready(function () {
 | 
			
		||||
})
 | 
			
		||||
.on('click', '#switch_admin', function () {
 | 
			
		||||
    .on('click', '#switch_admin', function () {
 | 
			
		||||
        var cookieName = "IN_ADMIN_PAGE";
 | 
			
		||||
        setTimeout(function () {
 | 
			
		||||
            delCookie(cookieName);
 | 
			
		||||
            setCookie(cookieName, "Yes");
 | 
			
		||||
            window.location = "/"
 | 
			
		||||
        }, 100)
 | 
			
		||||
})
 | 
			
		||||
.on('click', '#switch_user', function () {
 | 
			
		||||
    })
 | 
			
		||||
    .on('click', '#switch_user', function () {
 | 
			
		||||
        var cookieName = "IN_ADMIN_PAGE";
 | 
			
		||||
        setTimeout(function () {
 | 
			
		||||
            delCookie(cookieName);
 | 
			
		||||
            setCookie(cookieName, "No");
 | 
			
		||||
        window.location = "{% url 'assets:user-asset-list' %}"
 | 
			
		||||
        }, 100);
 | 
			
		||||
})
 | 
			
		||||
    })
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@
 | 
			
		|||
    </style>
 | 
			
		||||
    <div>
 | 
			
		||||
        <p>
 | 
			
		||||
            <div class="alert alert-msg" id="messages">
 | 
			
		||||
            <div class="alert {% if error %} alert-danger {% else %} alert-info {% endif %}" id="messages">
 | 
			
		||||
            {% if error %}
 | 
			
		||||
                {{ error }}
 | 
			
		||||
            {% else %}
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +44,7 @@
 | 
			
		|||
 | 
			
		||||
{% block custom_foot_js %}
 | 
			
		||||
    <script>
 | 
			
		||||
    var ttl = 2
 | 
			
		||||
    var message = ''
 | 
			
		||||
    var time = '{{ interval }}'
 | 
			
		||||
    {% if error %}
 | 
			
		||||
| 
						 | 
				
			
			@ -56,8 +57,8 @@
 | 
			
		|||
        if (time >= 0) {
 | 
			
		||||
            var msg = message + ', <b>' + time + '</b> ...';
 | 
			
		||||
            $('#messages').html(msg);
 | 
			
		||||
            time--;
 | 
			
		||||
            setTimeout(redirect_page, 1000);
 | 
			
		||||
            time -= ttl;
 | 
			
		||||
            setTimeout(redirect_page, ttl * 1000);
 | 
			
		||||
        } else {
 | 
			
		||||
            window.location.href = "{{ redirect_url }}";
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2452,6 +2452,28 @@ type = "legacy"
 | 
			
		|||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
 | 
			
		||||
reference = "tsinghua"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "fido2"
 | 
			
		||||
version = "1.1.2"
 | 
			
		||||
description = "FIDO2/WebAuthn library for implementing clients and servers."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.7,<4.0"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "fido2-1.1.2-py3-none-any.whl", hash = "sha256:a3b7d7d233dec3a4fa0d6178fc34d1cce17b820005a824f6ab96917a1e3be8d8"},
 | 
			
		||||
    {file = "fido2-1.1.2.tar.gz", hash = "sha256:6110d913106f76199201b32d262b2857562cc46ba1d0b9c51fbce30dc936c573"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
cryptography = ">=2.6,<35 || >35,<44"
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
pcsc = ["pyscard (>=1.9,<3)"]
 | 
			
		||||
 | 
			
		||||
[package.source]
 | 
			
		||||
type = "legacy"
 | 
			
		||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
 | 
			
		||||
reference = "tsinghua"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "flower"
 | 
			
		||||
version = "2.0.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -6682,6 +6704,22 @@ type = "legacy"
 | 
			
		|||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
 | 
			
		||||
reference = "tsinghua"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ua-parser"
 | 
			
		||||
version = "0.18.0"
 | 
			
		||||
description = "Python port of Browserscope's user agent parser"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "ua-parser-0.18.0.tar.gz", hash = "sha256:db51f1b59bfaa82ed9e2a1d99a54d3e4153dddf99ac1435d51828165422e624e"},
 | 
			
		||||
    {file = "ua_parser-0.18.0-py2.py3-none-any.whl", hash = "sha256:9d94ac3a80bcb0166823956a779186c746b50ea4c9fd9bf30fdb758553c38950"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.source]
 | 
			
		||||
type = "legacy"
 | 
			
		||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
 | 
			
		||||
reference = "tsinghua"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ucloud-sdk-python3"
 | 
			
		||||
version = "0.11.50"
 | 
			
		||||
| 
						 | 
				
			
			@ -6759,6 +6797,25 @@ type = "legacy"
 | 
			
		|||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
 | 
			
		||||
reference = "tsinghua"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "user-agents"
 | 
			
		||||
version = "2.2.0"
 | 
			
		||||
description = "A library to identify devices (phones, tablets) and their capabilities by parsing browser user agent strings."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "user-agents-2.2.0.tar.gz", hash = "sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26"},
 | 
			
		||||
    {file = "user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
ua-parser = ">=0.10.0"
 | 
			
		||||
 | 
			
		||||
[package.source]
 | 
			
		||||
type = "legacy"
 | 
			
		||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
 | 
			
		||||
reference = "tsinghua"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "uvicorn"
 | 
			
		||||
version = "0.22.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -7223,4 +7280,4 @@ reference = "tsinghua"
 | 
			
		|||
[metadata]
 | 
			
		||||
lock-version = "2.0"
 | 
			
		||||
python-versions = "^3.11"
 | 
			
		||||
content-hash = "f5bef179ee92691490abd486fd53596156e8af343ae8461a306c4fd8c174e348"
 | 
			
		||||
content-hash = "64818ed074beae46501d024a8d24d80cc3c38dc699a4befb3ec7c33abf6a82ce"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -139,6 +139,9 @@ netifaces = "^0.11.0"
 | 
			
		|||
daphne = "4.0.0"
 | 
			
		||||
channels = "^4.0.0"
 | 
			
		||||
channels-redis = "4.1.0"
 | 
			
		||||
fido2 = "^1.1.2"
 | 
			
		||||
ua-parser = "^0.18.0"
 | 
			
		||||
user-agents = "^2.2.0"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[tool.poetry.group.xpack.dependencies]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue