This commit is contained in:
parent
9bba2f0e92
commit
3ab3c21a09
@ -61,6 +61,7 @@ type AuditActor struct {
|
||||
SessionID string
|
||||
RecentAuth bool
|
||||
RecoveryCodeConfigured bool
|
||||
ExternalEmailConfigured bool
|
||||
}
|
||||
|
||||
type StepUpConfirmation struct {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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, "无权修改该账号的系统通知授权")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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, "无权管理该用户的安全信息")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user