This commit is contained in:
parent
9bba2f0e92
commit
3ab3c21a09
@ -61,6 +61,7 @@ type AuditActor struct {
|
|||||||
SessionID string
|
SessionID string
|
||||||
RecentAuth bool
|
RecentAuth bool
|
||||||
RecoveryCodeConfigured bool
|
RecoveryCodeConfigured bool
|
||||||
|
ExternalEmailConfigured bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type StepUpConfirmation struct {
|
type StepUpConfirmation struct {
|
||||||
|
|||||||
@ -166,6 +166,7 @@ func DefaultConfigAuthorization(_ context.Context, actor AuditActor, operation C
|
|||||||
Capability: capability,
|
Capability: capability,
|
||||||
RecentAuth: actor.RecentAuth,
|
RecentAuth: actor.RecentAuth,
|
||||||
RecoveryCodeConfigured: actor.RecoveryCodeConfigured,
|
RecoveryCodeConfigured: actor.RecoveryCodeConfigured,
|
||||||
|
ExternalEmailConfigured: actor.ExternalEmailConfigured,
|
||||||
}
|
}
|
||||||
if operation == ConfigOperationRollback && request.ActorRole == access.RoleSuperAdmin {
|
if operation == ConfigOperationRollback && request.ActorRole == access.RoleSuperAdmin {
|
||||||
if !request.RecentAuth {
|
if !request.RecentAuth {
|
||||||
|
|||||||
@ -22,7 +22,9 @@ func authorizeCapabilityWithNewRole(ctx *context.Context, capability access.Capa
|
|||||||
OwnResource: ownResource,
|
OwnResource: ownResource,
|
||||||
RecentAuth: ctx.RecentAuth,
|
RecentAuth: ctx.RecentAuth,
|
||||||
RecoveryCodeConfigured: ctx.RecoveryCodeConfigured,
|
RecoveryCodeConfigured: ctx.RecoveryCodeConfigured,
|
||||||
|
ExternalEmailConfigured: ctx.ExternalEmailConfigured,
|
||||||
TargetRecoveryConfigured: ctx.TargetRecoveryConfigured,
|
TargetRecoveryConfigured: ctx.TargetRecoveryConfigured,
|
||||||
|
TargetExternalEmailConfigured: ctx.TargetExternalEmailConfigured,
|
||||||
NewRole: newRole,
|
NewRole: newRole,
|
||||||
PluginInstanceAuthorized: ctx.PluginInstanceAuthorized,
|
PluginInstanceAuthorized: ctx.PluginInstanceAuthorized,
|
||||||
AuditScopeAuthorized: ctx.AuditScopeAuthorized,
|
AuditScopeAuthorized: ctx.AuditScopeAuthorized,
|
||||||
|
|||||||
@ -372,7 +372,7 @@ func configSectionFromPath(path string) (config.Section, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func configActor(ctx *appcontext.Context, req *http.Request) config.AuditActor {
|
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
|
actor.Name = ctx.UserAccount
|
||||||
if sessionManager := session.Instance; sessionManager != nil {
|
if sessionManager := session.Instance; sessionManager != nil {
|
||||||
if cookie, err := req.Cookie(sessionManager.Cookie.Name); err == 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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if ctx.EffectiveRole() == access.RoleSuperAdmin {
|
if ctx.EffectiveRole() == access.RoleSuperAdmin {
|
||||||
ctx.RecoveryCodeConfigured = false
|
|
||||||
return
|
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
|
service := currentSecurityRuntime().credentials
|
||||||
if service == nil && db.Instance != nil {
|
if service == nil && db.Instance != nil {
|
||||||
service, _ = credentials.NewService(db.Instance)
|
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) {
|
if access.IsDenied(err, access.CodeRecentAuthRequired) {
|
||||||
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请先确认当前管理员密码", string(access.CodeRecentAuthRequired))
|
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请先确认当前管理员密码", string(access.CodeRecentAuthRequired))
|
||||||
} else if access.IsDenied(err, access.CodeRecoveryCodeRequired) {
|
} else if access.IsDenied(err, access.CodeRecoveryCodeRequired) {
|
||||||
writeSecurityError(w, response.NoAccessPrivileges, "请先为当前管理员配置安全码")
|
writeSecurityError(w, response.NoAccessPrivileges, "请先为当前管理员配置有效安全码或验证外部邮箱")
|
||||||
} else {
|
} else {
|
||||||
writeSecurityError(w, response.NoAccessPrivileges, "无权修改该账号的系统通知授权")
|
writeSecurityError(w, response.NoAccessPrivileges, "无权修改该账号的系统通知授权")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -421,7 +421,7 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
if reqData.Password != "" {
|
if reqData.Password != "" {
|
||||||
if err := authorizeCapability(ctx, access.CapabilityPasswordReset, targetRole, false); err != nil {
|
if err := authorizeCapability(ctx, access.CapabilityPasswordReset, targetRole, false); err != nil {
|
||||||
writeUserAuthorizationError(w, err, "密码重置需要为当前管理员配置有效安全码")
|
writeUserAuthorizationError(w, err, "请先为当前管理员配置有效安全码或验证外部邮箱")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -436,17 +436,17 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.TargetRecoveryConfigured = false
|
ctx.TargetRecoveryConfigured = false
|
||||||
if credentialService := credentialRuntime(); credentialService != nil {
|
ctx.TargetExternalEmailConfigured = false
|
||||||
configured, checkErr := credentialService.HasValidRecoveryCode(user.ID)
|
recoveryStatus, emailStatus, checkErr := loadUserSecurityStatus([]models.User{user})
|
||||||
if checkErr != nil {
|
if checkErr != nil {
|
||||||
log.WithContext(ctx).WithError(checkErr).Error("load target recovery code status failed")
|
log.WithContext(ctx).WithError(checkErr).Error("load target recovery options failed")
|
||||||
response.NewErrorResponse(response.ServerError, "读取目标用户恢复码状态失败", "").FPrint(w)
|
response.NewErrorResponse(response.ServerError, "读取目标用户安全方式失败", "").FPrint(w)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
ctx.TargetRecoveryConfigured = configured
|
|
||||||
}
|
}
|
||||||
|
ctx.TargetRecoveryConfigured = recoveryStatus[user.ID]
|
||||||
|
ctx.TargetExternalEmailConfigured = emailStatus[user.ID]
|
||||||
if err := authorizeCapabilityWithNewRole(ctx, access.CapabilityRoleManage, targetRole, newRole, false); err != nil {
|
if err := authorizeCapabilityWithNewRole(ctx, access.CapabilityRoleManage, targetRole, newRole, false); err != nil {
|
||||||
writeUserAuthorizationError(w, err, "更改权限级别前,请先为该用户配置有效安全码")
|
writeUserAuthorizationError(w, err, "更改权限级别前,请先为该用户配置有效安全码或验证外部邮箱")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.ApplyRole(newRole)
|
user.ApplyRole(newRole)
|
||||||
|
|||||||
@ -57,7 +57,7 @@ func loadManagedSecurityUser(ctx *context.Context, w http.ResponseWriter, userID
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
if access.IsDenied(err, access.CodeRecoveryCodeRequired) {
|
if access.IsDenied(err, access.CodeRecoveryCodeRequired) {
|
||||||
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请先为当前管理员配置有效安全码", string(access.CodeRecoveryCodeRequired))
|
writeSecurityErrorWithCode(w, response.NoAccessPrivileges, "请先为当前管理员配置有效安全码或验证外部邮箱", string(access.CodeRecoveryCodeRequired))
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
writeSecurityError(w, response.NoAccessPrivileges, "无权管理该用户的安全信息")
|
writeSecurityError(w, response.NoAccessPrivileges, "无权管理该用户的安全信息")
|
||||||
|
|||||||
@ -32,7 +32,9 @@ type Request struct {
|
|||||||
OwnResource bool
|
OwnResource bool
|
||||||
RecentAuth bool
|
RecentAuth bool
|
||||||
RecoveryCodeConfigured bool
|
RecoveryCodeConfigured bool
|
||||||
|
ExternalEmailConfigured bool
|
||||||
TargetRecoveryConfigured bool
|
TargetRecoveryConfigured bool
|
||||||
|
TargetExternalEmailConfigured bool
|
||||||
PluginInstanceAuthorized bool
|
PluginInstanceAuthorized bool
|
||||||
AuditScopeAuthorized bool
|
AuditScopeAuthorized bool
|
||||||
DeploymentChannel bool
|
DeploymentChannel bool
|
||||||
@ -55,7 +57,8 @@ func Authorize(request Request) error {
|
|||||||
if policy.RequiresRecentAuth && !request.RecentAuth {
|
if policy.RequiresRecentAuth && !request.RecentAuth {
|
||||||
return deny(CodeRecentAuthRequired)
|
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 deny(CodeRecoveryCodeRequired)
|
||||||
}
|
}
|
||||||
return authorizeResource(request)
|
return authorizeResource(request)
|
||||||
@ -98,7 +101,7 @@ func authorizeResource(request Request) error {
|
|||||||
if !canManageAccountTarget(request.ActorRole, request.TargetRole) || !request.NewRole.Valid() || request.NewRole == RoleSuperAdmin {
|
if !canManageAccountTarget(request.ActorRole, request.TargetRole) || !request.NewRole.Valid() || request.NewRole == RoleSuperAdmin {
|
||||||
return deny(CodeTargetRoleDenied)
|
return deny(CodeTargetRoleDenied)
|
||||||
}
|
}
|
||||||
if !request.TargetRecoveryConfigured {
|
if !request.TargetRecoveryConfigured && !request.TargetExternalEmailConfigured {
|
||||||
return deny(CodeRecoveryCodeRequired)
|
return deny(CodeRecoveryCodeRequired)
|
||||||
}
|
}
|
||||||
case CapabilityPluginRestart:
|
case CapabilityPluginRestart:
|
||||||
|
|||||||
@ -25,7 +25,9 @@ type Context struct {
|
|||||||
SecuritySetupAcknowledged int
|
SecuritySetupAcknowledged int
|
||||||
RecentAuth bool
|
RecentAuth bool
|
||||||
RecoveryCodeConfigured bool
|
RecoveryCodeConfigured bool
|
||||||
|
ExternalEmailConfigured bool
|
||||||
TargetRecoveryConfigured bool
|
TargetRecoveryConfigured bool
|
||||||
|
TargetExternalEmailConfigured bool
|
||||||
PluginInstanceAuthorized bool
|
PluginInstanceAuthorized bool
|
||||||
AuditScopeAuthorized bool
|
AuditScopeAuthorized bool
|
||||||
DeploymentChannel 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 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>
|
</div>
|
||||||
<el-alert v-if="recoveryConfigured" title="当前已配置有效离线恢复码" type="success" :closable="false" show-icon />
|
<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 v-if="generatedCodes.length" class="recovery-code-panel">
|
||||||
<div class="one-time-tip">安全码已通过安全验证显示,仅在当前页面短暂显示,请立即复制并妥善保存。</div>
|
<div class="one-time-tip">安全码已通过安全验证显示,仅在当前页面短暂显示,请立即复制并妥善保存。</div>
|
||||||
<code v-for="code in generatedCodes" :key="code" class="recovery-code">{{ code }}</code>
|
<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">
|
<el-tag v-else :type="roleMeta(editUserInfo.role).type">
|
||||||
{{ roleMeta(editUserInfo.role).label }}
|
{{ roleMeta(editUserInfo.role).label }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-if="roleChanged && !selectedUser?.RecoveryCodeConfigured" class="role-requirement">
|
<span v-if="roleChanged && !selectedUser?.RecoveryCodeConfigured && !selectedUser?.ExternalEmailConfigured" class="role-requirement">
|
||||||
更改级别前,请先在下方配置该用户的安全码。
|
更改级别前,请先为该用户配置安全码或验证外部邮箱。
|
||||||
</span>
|
</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user