mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
- 提升用户为管理员 / 创建管理员账号纳入敏感操作:handler 级 EnforceStepUp 门控 (admin API key 拒绝、未启用 TOTP 拒绝、无 grant 返回 STEP_UP_REQUIRED), 目标已是管理员的日常编辑不触发 - 管理员启用/停用 2FA 一律使用密码验证(默认通知邮箱常收不到验证码), verification-method 按用户角色返回;普通用户行为不变 - 用户编辑/创建弹窗接入 useStepUp:命中 STEP_UP_REQUIRED 弹 TOTP 验证并自动重试 - 审计日志清理入口与其他敏感操作对齐:未启用 2FA 时直接提示先启用 TOTP, 不再弹出无法完成的验证码输入框(后端强制现场 TOTP 语义不变) - 审计日志页重构:DataTable 布局、详情弹窗分区展示、时间范围改为 ops 同款 下拉(预设窗口 + 自定义起止支持时分)
101 lines
3.6 KiB
Go
101 lines
3.6 KiB
Go
package middleware
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// StepUpAuthMiddleware 敏感操作 step-up 2FA 门控中间件类型。
|
||
type StepUpAuthMiddleware gin.HandlerFunc
|
||
|
||
// stepUpGrantChecker 抽象 TOTP step-up 授权检查能力(由 TotpService 实现)。
|
||
type stepUpGrantChecker interface {
|
||
HasStepUpGrant(ctx context.Context, userID int64, sessionKey string) (bool, error)
|
||
}
|
||
|
||
// stepUpUserReader 抽象用户读取能力(检查 TOTP 是否启用)。
|
||
type stepUpUserReader interface {
|
||
GetByID(ctx context.Context, id int64) (*service.User, error)
|
||
}
|
||
|
||
// StepUpSessionKey 计算 step-up 授权的会话键:
|
||
// 优先绑定当前会话(refresh token family),无会话 ID 的旧 token 退化为用户级键。
|
||
func StepUpSessionKey(c *gin.Context, userID int64) string {
|
||
if sid := c.GetString(ContextKeySessionID); sid != "" {
|
||
return sid
|
||
}
|
||
return fmt.Sprintf("u%d", userID)
|
||
}
|
||
|
||
// NewStepUpAuthMiddleware 创建敏感操作 step-up 2FA 门控中间件。
|
||
//
|
||
// 通过条件(全部满足):
|
||
// 1. 必须是 JWT 认证的真人会话——admin API key(机器凭证)一律拒绝
|
||
// 2. 当前用户已启用 TOTP(未启用则拒绝并提示先启用 2FA)
|
||
// 3. 当前会话在有效期内完成过 TOTP step-up 验证(POST /api/v1/user/totp/step-up)
|
||
//
|
||
// 失败响应使用可区分的错误码,前端据此弹出 TOTP 验证对话框后重试。
|
||
func NewStepUpAuthMiddleware(totpService *service.TotpService, userService *service.UserService) StepUpAuthMiddleware {
|
||
return StepUpAuthMiddleware(stepUpAuth(totpService, userService))
|
||
}
|
||
|
||
func stepUpAuth(grantChecker stepUpGrantChecker, userReader stepUpUserReader) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
if !enforceStepUp(c, grantChecker, userReader) {
|
||
return
|
||
}
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// EnforceStepUp 对当前请求执行与 StepUpAuthMiddleware 相同语义的 step-up 门控,
|
||
// 供 handler 在需要按请求内容条件触发时调用(如仅当把用户角色提升为管理员时)。
|
||
// 校验失败时写入错误响应并中止请求,返回 false;通过返回 true。
|
||
func EnforceStepUp(c *gin.Context, totpService *service.TotpService, userService *service.UserService) bool {
|
||
return enforceStepUp(c, totpService, userService)
|
||
}
|
||
|
||
func enforceStepUp(c *gin.Context, grantChecker stepUpGrantChecker, userReader stepUpUserReader) bool {
|
||
if c.GetString("auth_method") == service.AuditAuthMethodAdminAPIKey {
|
||
AbortWithError(c, 403, "STEP_UP_ADMIN_API_KEY_FORBIDDEN",
|
||
"Admin API key cannot access this endpoint; a two-factor verified admin session is required")
|
||
return false
|
||
}
|
||
|
||
subject, ok := GetAuthSubjectFromContext(c)
|
||
if !ok || subject.UserID <= 0 {
|
||
AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
|
||
return false
|
||
}
|
||
|
||
user, err := userReader.GetByID(c.Request.Context(), subject.UserID)
|
||
if err != nil {
|
||
AbortWithError(c, 500, "INTERNAL_ERROR", "Failed to load user")
|
||
return false
|
||
}
|
||
if !user.TotpEnabled {
|
||
AbortWithError(c, 403, "STEP_UP_TOTP_NOT_ENABLED",
|
||
"This operation requires two-factor authentication; please enable TOTP first")
|
||
return false
|
||
}
|
||
|
||
sessionKey := StepUpSessionKey(c, subject.UserID)
|
||
granted, err := grantChecker.HasStepUpGrant(c.Request.Context(), subject.UserID, sessionKey)
|
||
if err != nil {
|
||
// 安全门控故障时选择 fail-closed。
|
||
AbortWithError(c, 503, "STEP_UP_UNAVAILABLE", "Step-up verification service unavailable")
|
||
return false
|
||
}
|
||
if !granted {
|
||
AbortWithError(c, 403, "STEP_UP_REQUIRED",
|
||
"This operation requires recent two-factor verification")
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|