From dbd35e11ff6293386f0160ba44c321e4e511a9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 2 May 2026 19:45:10 +0400 Subject: [PATCH] =?UTF-8?q?feat(auth):=20demo-=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BD=20=E2=89=A56=20=D1=81=D0=B8=D0=BC=D0=B2,=20MultiAuthenti?= =?UTF-8?q?cator,=20UUID=20v5,=20v1.3.26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- console/cmd/server/main.go | 10 +++-- console/deploy/console.yaml | 2 +- console/internal/auth/auth.go | 72 +++++++++++++++++++++++++++++++++++ console/ui/index.html | 12 +++--- console/ui/js/auth.js | 10 ++++- 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/console/cmd/server/main.go b/console/cmd/server/main.go index e995ce1..63d4b22 100644 --- a/console/cmd/server/main.go +++ b/console/cmd/server/main.go @@ -39,11 +39,15 @@ func main() { testMode := os.Getenv("FISSION_TEST_MODE") == "true" - var authenticator auth.Authenticator + var jwtAuth auth.Authenticator if testMode { - authenticator = &auth.TestAuthenticator{} + jwtAuth = &auth.TestAuthenticator{} } else { - authenticator = auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil) + jwtAuth = auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil) + } + authenticator := &auth.MultiAuthenticator{ + JWT: jwtAuth, + Demo: &auth.DemoAuthenticator{}, } srv := api.NewServer(api.Config{ diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml index 8ec10f2..428a3b2 100644 --- a/console/deploy/console.yaml +++ b/console/deploy/console.yaml @@ -52,7 +52,7 @@ spec: serviceAccountName: fission-console containers: - name: console - image: naeel/fission-console:v1.3.25 + image: naeel/fission-console:v1.3.26 imagePullPolicy: Always ports: - containerPort: 8090 diff --git a/console/internal/auth/auth.go b/console/internal/auth/auth.go index 889814a..1b5a606 100644 --- a/console/internal/auth/auth.go +++ b/console/internal/auth/auth.go @@ -6,6 +6,7 @@ package auth import ( "context" + "crypto/sha1" "crypto/sha256" "encoding/base64" "encoding/hex" @@ -172,3 +173,74 @@ func identityFromJWT(token string) (UserIdentity, error) { email, _ := claims["email"].(string) return UserIdentity{Sub: sub, Email: email}, nil } + +// isJWT возвращает true если строка похожа на JWT (три непустые части через точку). +func isJWT(s string) bool { + parts := strings.SplitN(s, ".", 4) + return len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" +} + +// uuidV5 генерирует детерминированный UUID v5 (RFC 4122) из строки. +// Namespace: произвольный фиксированный UUID для demo-логинов. +func uuidV5(name string) string { + // Фиксированный namespace UUID для demo-логинов + ns := []byte{0xf4, 0x7a, 0xc1, 0x0b, 0x58, 0xcc, 0x43, 0x72, 0xa5, 0x67, 0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79} + h := sha1.New() + _, _ = h.Write(ns) + _, _ = h.Write([]byte(name)) + sum := h.Sum(nil) + sum[6] = (sum[6] & 0x0f) | 0x50 // version 5 + sum[8] = (sum[8] & 0x3f) | 0x80 // RFC 4122 variant + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + sum[0:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16]) +} + +// demoEmail формирует маскированный email для демо-логина. +// Пример: "mylogin" → "demo-my***in" +func demoEmail(login string) string { + r := []rune(login) + n := len(r) + if n <= 4 { + return "demo-" + string(r) + } + return "demo-" + string(r[:2]) + strings.Repeat("*", n-4) + string(r[n-2:]) +} + +// --- DemoAuthenticator --- + +// DemoAuthenticator — аутентификация по произвольному логину (≥6 символов). +// Не обращается ни к каким внешним сервисам. +// Sub = UUID v5 от логина (детерминированный), Email = маскированный логин. +type DemoAuthenticator struct{} + +// Authenticate принимает логин ≥6 символов и возвращает UserIdentity. +func (d *DemoAuthenticator) Authenticate(_ context.Context, login, _ string) (UserIdentity, error) { + login = strings.TrimSpace(login) + if len([]rune(login)) < 6 { + return UserIdentity{}, fmt.Errorf("login must be at least 6 characters") + } + return UserIdentity{Sub: uuidV5(login), Email: demoEmail(login)}, nil +} + +// --- MultiAuthenticator --- + +// MultiAuthenticator роутит аутентификацию: +// - JWT (три части через точку) → JWT authenticator (DeckAuthenticator или TestAuthenticator) +// - Иначе → DemoAuthenticator +// Оба пути равноправны и возвращают одинаковый UserIdentity. +type MultiAuthenticator struct { + JWT Authenticator + Demo *DemoAuthenticator +} + +// Authenticate определяет тип входных данных и делегирует нужному authenticator. +func (m *MultiAuthenticator) Authenticate(ctx context.Context, token, env string) (UserIdentity, error) { + token = strings.TrimSpace(token) + if token == "" { + return UserIdentity{}, fmt.Errorf("token or login required") + } + if isJWT(token) { + return m.JWT.Authenticate(ctx, token, env) + } + return m.Demo.Authenticate(ctx, token, env) +} diff --git a/console/ui/index.html b/console/ui/index.html index 6c89929..935da79 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -46,12 +46,12 @@
- + -
Токен: Личный кабинет → Профиль пользователя → Токены
+ placeholder="Введите токен или любой логин (не менее 6 символов)..."> +
Токен из ЛК: Личный кабинет → Профиль пользователя → Токены
Или введите любой логин не менее 6 символов для демо-доступа.
@@ -100,7 +100,7 @@
NUBES
FISSION CONSOLE
-
v1.3.25
+
v1.3.26
@@ -381,7 +381,7 @@
- v1.3.25 + v1.3.26
diff --git a/console/ui/js/auth.js b/console/ui/js/auth.js index f07aeb4..9b8fdb7 100644 --- a/console/ui/js/auth.js +++ b/console/ui/js/auth.js @@ -13,7 +13,9 @@ async function doLogin() { var errEl = document.getElementById('l-error'); var token = (document.getElementById('l-token').value || '').trim(); var env = document.getElementById('l-env').value; - if (!token) { errEl.textContent = 'Введите токен'; errEl.style.display = 'block'; return; } + if (!token) { errEl.textContent = 'Введите токен или логин'; errEl.style.display = 'block'; return; } + var isJWT = token.split('.').length === 3 && !token.includes(' '); + if (!isJWT && token.length < 6) { errEl.textContent = 'Логин — не менее 6 символов'; errEl.style.display = 'block'; return; } btn.disabled = true; errEl.style.display = 'none'; try { @@ -26,8 +28,11 @@ async function doLogin() { const d = await res.json().catch(function () { return {}; }); throw new Error(d.error || 'Ошибка входа'); } + const data = await res.json(); localStorage.setItem('auth_token', token); localStorage.setItem('auth_env', env); + if (data.email) localStorage.setItem('auth_email', data.email); + else localStorage.removeItem('auth_email'); setUserAvatar(token); hideLoginOverlay(); // Проверяем статус NS — если не ready, показываем init overlay @@ -58,7 +63,7 @@ function jwtEmail(token) { function setUserAvatar(token) { var el = document.getElementById('user-avatar'); if (!el) return; - var email = token ? jwtEmail(token) : null; + var email = localStorage.getItem('auth_email') || (token ? jwtEmail(token) : null); if (email) { el.textContent = email; el.title = email; @@ -82,6 +87,7 @@ function setUserAvatar(token) { function doLogout() { localStorage.removeItem('auth_token'); localStorage.removeItem('auth_env'); + localStorage.removeItem('auth_email'); try { document.getElementById('l-token').value = ''; } catch (_) { } setUserAvatar(null); showLoginOverlay();