检查点:保存安全方式二选一授权调整以补充回归覆盖
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-02 18:33:48 +08:00
parent 9bba2f0e92
commit 3ab3c21a09
12 changed files with 40 additions and 20 deletions

View File

@ -61,6 +61,7 @@ type AuditActor struct {
SessionID string
RecentAuth bool
RecoveryCodeConfigured bool
ExternalEmailConfigured bool
}
type StepUpConfirmation struct {

View File

@ -166,6 +166,7 @@ func DefaultConfigAuthorization(_ context.Context, actor AuditActor, operation C
Capability: capability,
RecentAuth: actor.RecentAuth,
RecoveryCodeConfigured: actor.RecoveryCodeConfigured,
ExternalEmailConfigured: actor.ExternalEmailConfigured,
}
if operation == ConfigOperationRollback && request.ActorRole == access.RoleSuperAdmin {
if !request.RecentAuth {

View File

@ -22,7 +22,9 @@ func authorizeCapabilityWithNewRole(ctx *context.Context, capability access.Capa
OwnResource: ownResource,
RecentAuth: ctx.RecentAuth,
RecoveryCodeConfigured: ctx.RecoveryCodeConfigured,
ExternalEmailConfigured: ctx.ExternalEmailConfigured,
TargetRecoveryConfigured: ctx.TargetRecoveryConfigured,
TargetExternalEmailConfigured: ctx.TargetExternalEmailConfigured,
NewRole: newRole,
PluginInstanceAuthorized: ctx.PluginInstanceAuthorized,
AuditScopeAuthorized: ctx.AuditScopeAuthorized,

View File

@ -372,7 +372,7 @@ func configSectionFromPath(path string) (config.Section, bool) {
}
func configActor(ctx *appcontext.Context, req *http.Request) config.AuditActor {
actor := config.AuditActor{UserID: strconv.Itoa(ctx.UserID), Role: string(ctx.EffectiveRole()), RecentAuth: ctx.RecentAuth, RecoveryCodeConfigured: ctx.RecoveryCodeConfigured}
actor := config.AuditActor{UserID: strconv.Itoa(ctx.UserID), Role: string(ctx.EffectiveRole()), RecentAuth: ctx.RecentAuth, RecoveryCodeConfigured: ctx.RecoveryCodeConfigured, ExternalEmailConfigured: ctx.ExternalEmailConfigured}
actor.Name = ctx.UserAccount
if sessionManager := session.Instance; sessionManager != nil {
if cookie, err := req.Cookie(sessionManager.Cookie.Name); err == nil {

View File

@ -164,13 +164,24 @@ func InvalidateSecurityRuntimeUser(userID int, account string) {
}
func LoadSecurityContext(ctx *context.Context) {
if ctx == nil || ctx.UserID <= 0 {
if ctx == nil {
return
}
// 每次请求重新读取,停用邮箱或用完安全码后不能沿用旧的授权状态。
ctx.RecoveryCodeConfigured = false
ctx.ExternalEmailConfigured = false
if ctx.UserID <= 0 {
return
}
if ctx.EffectiveRole() == access.RoleSuperAdmin {
ctx.RecoveryCodeConfigured = false
return
}
if db.Instance != nil {
configured, err := db.Instance.Where("user_id = ? AND status = ?", ctx.UserID, models.ExternalEmailStatusVerified).Exist(&models.ExternalEmail{})
if err == nil {
ctx.ExternalEmailConfigured = configured
}
}
service := currentSecurityRuntime().credentials
if service == nil && db.Instance != nil {
service, _ = credentials.NewService(db.Instance)

View File

@ -52,7 +52,7 @@ func loadNotificationUser(ctx *context.Context, w http.ResponseWriter, userID in
if access.IsDenied(err, access.CodeRecentAuthRequired) {
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请先确认当前管理员密码", string(access.CodeRecentAuthRequired))
} else if access.IsDenied(err, access.CodeRecoveryCodeRequired) {
writeSecurityError(w, response.NoAccessPrivileges, "请先为当前管理员配置安全码")
writeSecurityError(w, response.NoAccessPrivileges, "请先为当前管理员配置有效安全码或验证外部邮箱")
} else {
writeSecurityError(w, response.NoAccessPrivileges, "无权修改该账号的系统通知授权")
}

View File

@ -421,7 +421,7 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
}
if reqData.Password != "" {
if err := authorizeCapability(ctx, access.CapabilityPasswordReset, targetRole, false); err != nil {
writeUserAuthorizationError(w, err, "密码重置需要为当前管理员配置有效安全码")
writeUserAuthorizationError(w, err, "请先为当前管理员配置有效安全码或验证外部邮箱")
return
}
}
@ -436,17 +436,17 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
return
}
ctx.TargetRecoveryConfigured = false
if credentialService := credentialRuntime(); credentialService != nil {
configured, checkErr := credentialService.HasValidRecoveryCode(user.ID)
if checkErr != nil {
log.WithContext(ctx).WithError(checkErr).Error("load target recovery code status failed")
response.NewErrorResponse(response.ServerError, "读取目标用户恢复码状态失败", "").FPrint(w)
return
}
ctx.TargetRecoveryConfigured = configured
ctx.TargetExternalEmailConfigured = false
recoveryStatus, emailStatus, checkErr := loadUserSecurityStatus([]models.User{user})
if checkErr != nil {
log.WithContext(ctx).WithError(checkErr).Error("load target recovery options failed")
response.NewErrorResponse(response.ServerError, "读取目标用户安全方式失败", "").FPrint(w)
return
}
ctx.TargetRecoveryConfigured = recoveryStatus[user.ID]
ctx.TargetExternalEmailConfigured = emailStatus[user.ID]
if err := authorizeCapabilityWithNewRole(ctx, access.CapabilityRoleManage, targetRole, newRole, false); err != nil {
writeUserAuthorizationError(w, err, "更改权限级别前,请先为该用户配置有效安全码")
writeUserAuthorizationError(w, err, "更改权限级别前,请先为该用户配置有效安全码或验证外部邮箱")
return
}
user.ApplyRole(newRole)

View File

@ -57,7 +57,7 @@ func loadManagedSecurityUser(ctx *context.Context, w http.ResponseWriter, userID
return nil, false
}
if access.IsDenied(err, access.CodeRecoveryCodeRequired) {
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请先为当前管理员配置有效安全码", string(access.CodeRecoveryCodeRequired))
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请先为当前管理员配置有效安全码或验证外部邮箱", string(access.CodeRecoveryCodeRequired))
return nil, false
}
writeSecurityError(w, response.NoAccessPrivileges, "无权管理该用户的安全信息")

View File

@ -32,7 +32,9 @@ type Request struct {
OwnResource bool
RecentAuth bool
RecoveryCodeConfigured bool
ExternalEmailConfigured bool
TargetRecoveryConfigured bool
TargetExternalEmailConfigured bool
PluginInstanceAuthorized bool
AuditScopeAuthorized bool
DeploymentChannel bool
@ -55,7 +57,8 @@ func Authorize(request Request) error {
if policy.RequiresRecentAuth && !request.RecentAuth {
return deny(CodeRecentAuthRequired)
}
if policy.RequiresRecoveryCode && request.ActorRole != RoleSuperAdmin && !request.RecoveryCodeConfigured && !request.DeploymentChannel {
// 保留原策略与错误码名称;普通角色的恢复保障允许安全码或已验证外部邮箱任选其一。
if policy.RequiresRecoveryCode && request.ActorRole != RoleSuperAdmin && !request.RecoveryCodeConfigured && !request.ExternalEmailConfigured && !request.DeploymentChannel {
return deny(CodeRecoveryCodeRequired)
}
return authorizeResource(request)
@ -98,7 +101,7 @@ func authorizeResource(request Request) error {
if !canManageAccountTarget(request.ActorRole, request.TargetRole) || !request.NewRole.Valid() || request.NewRole == RoleSuperAdmin {
return deny(CodeTargetRoleDenied)
}
if !request.TargetRecoveryConfigured {
if !request.TargetRecoveryConfigured && !request.TargetExternalEmailConfigured {
return deny(CodeRecoveryCodeRequired)
}
case CapabilityPluginRestart:

View File

@ -25,7 +25,9 @@ type Context struct {
SecuritySetupAcknowledged int
RecentAuth bool
RecoveryCodeConfigured bool
ExternalEmailConfigured bool
TargetRecoveryConfigured bool
TargetExternalEmailConfigured bool
PluginInstanceAuthorized bool
AuditScopeAuthorized bool
DeploymentChannel bool

View File

@ -92,7 +92,7 @@
<div class="section-actions"><el-button v-if="recoveryConfigured && !generatedCodes.length" @click="revealRecoveryCodes">验证后查看</el-button><el-button type="primary" @click="generateRecoveryCodes">生成新恢复码</el-button><el-button v-if="recoveryConfigured" type="danger" plain @click="revokeRecoveryCodes">撤销</el-button></div>
</div>
<el-alert v-if="recoveryConfigured" title="当前已配置有效离线恢复码" type="success" :closable="false" show-icon />
<el-alert v-else title="当前未配置有效离线恢复码,高风险操作可能无法进行。" type="warning" :closable="false" show-icon />
<el-alert v-else title="尚未配置安全码;已验证外部邮箱也可满足安全配置要求。" type="warning" :closable="false" show-icon />
<div v-if="generatedCodes.length" class="recovery-code-panel">
<div class="one-time-tip">安全码已通过安全验证显示,仅在当前页面短暂显示,请立即复制并妥善保存。</div>
<code v-for="code in generatedCodes" :key="code" class="recovery-code">{{ code }}</code>

View File

@ -219,8 +219,8 @@
<el-tag v-else :type="roleMeta(editUserInfo.role).type">
{{ roleMeta(editUserInfo.role).label }}
</el-tag>
<span v-if="roleChanged && !selectedUser?.RecoveryCodeConfigured" class="role-requirement">
更改级别前,请先在下方配置该用户的安全码。
<span v-if="roleChanged && !selectedUser?.RecoveryCodeConfigured && !selectedUser?.ExternalEmailConfigured" class="role-requirement">
更改级别前,请先为该用户配置安全码或验证外部邮箱。
</span>
</el-form-item>