From f501f971bbcc67dfbdc55bfeb6055b7b2659f3cb Mon Sep 17 00:00:00 2001 From: smail Date: Sat, 3 Oct 2026 01:41:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=89=E5=85=A8=E7=A0=81?= =?UTF-8?q?=E4=B8=8E=E5=A4=96=E9=83=A8=E9=82=AE=E7=AE=B1=E4=BA=8C=E9=80=89?= =?UTF-8?q?=E4=B8=80=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/services/access/access_test.go | 21 +++++++++++++++++++++ backend/services/access/authorize.go | 9 +++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/backend/services/access/access_test.go b/backend/services/access/access_test.go index 86d7328..220ae74 100644 --- a/backend/services/access/access_test.go +++ b/backend/services/access/access_test.go @@ -2,6 +2,27 @@ package access import "testing" +func TestHasRecoveryOptionAcceptsEitherConfiguredMethod(t *testing.T) { + tests := []struct { + name string + code bool + email bool + want bool + }{ + {name: "neither", want: false}, + {name: "security code only", code: true, want: true}, + {name: "external email only", email: true, want: true}, + {name: "both", code: true, email: true, want: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := HasRecoveryOption(test.code, test.email); got != test.want { + t.Fatalf("HasRecoveryOption(%t, %t) = %t, want %t", test.code, test.email, got, test.want) + } + }) + } +} + func TestRecoveryOptionAuthorization(t *testing.T) { for _, capability := range []Capability{CapabilityPasswordReset, CapabilityUserSecurityManage, CapabilityNotificationManage} { for _, ready := range []struct { diff --git a/backend/services/access/authorize.go b/backend/services/access/authorize.go index 9e06c78..ba3efa2 100644 --- a/backend/services/access/authorize.go +++ b/backend/services/access/authorize.go @@ -40,6 +40,11 @@ type Request struct { DeploymentChannel bool } +// HasRecoveryOption 判断账号是否至少配置了一种可用恢复方式。 +func HasRecoveryOption(recoveryCodeConfigured, externalEmailConfigured bool) bool { + return recoveryCodeConfigured || externalEmailConfigured +} + func Authorize(request Request) error { if !request.ActorRole.Valid() { return deny(CodeInvalidRole) @@ -58,7 +63,7 @@ func Authorize(request Request) error { return deny(CodeRecentAuthRequired) } // 保留原策略与错误码名称;普通角色的恢复保障允许安全码或已验证外部邮箱任选其一。 - if policy.RequiresRecoveryCode && request.ActorRole != RoleSuperAdmin && !request.RecoveryCodeConfigured && !request.ExternalEmailConfigured && !request.DeploymentChannel { + if policy.RequiresRecoveryCode && request.ActorRole != RoleSuperAdmin && !HasRecoveryOption(request.RecoveryCodeConfigured, request.ExternalEmailConfigured) && !request.DeploymentChannel { return deny(CodeRecoveryCodeRequired) } return authorizeResource(request) @@ -101,7 +106,7 @@ func authorizeResource(request Request) error { if !canManageAccountTarget(request.ActorRole, request.TargetRole) || !request.NewRole.Valid() || request.NewRole == RoleSuperAdmin { return deny(CodeTargetRoleDenied) } - if !request.TargetRecoveryConfigured && !request.TargetExternalEmailConfigured { + if !HasRecoveryOption(request.TargetRecoveryConfigured, request.TargetExternalEmailConfigured) { return deny(CodeRecoveryCodeRequired) } case CapabilityPluginRestart: