Files
lijiaoqiao/supply-api/internal/iam/middleware/scope_auth.go
Your Name b2d32be14f fix(P2): 修复4个P2轻微问题
P2-01: 通配符scope安全风险 (scope_auth.go)
- 添加hasWildcardScope()函数检测通配符scope
- 添加logWildcardScopeAccess()函数记录审计日志
- 在RequireScope/RequireAllScopes/RequireAnyScope中间件中调用审计日志

P2-02: isSamePayload比较字段不完整 (audit_service.go)
- 添加ActionDetail字段比较
- 添加ResultMessage字段比较
- 添加Extensions字段比较
- 添加compareExtensions()辅助函数

P2-03: regexp.MustCompile可能panic (sanitizer.go)
- 添加compileRegex()安全编译函数替代MustCompile
- 处理编译错误,避免panic

P2-04: StrategyRoundRobin未实现 (router.go)
- 添加selectByRoundRobin()方法
- 添加roundRobinCounter原子计数器
- 使用atomic.AddUint64实现线程安全的轮询

P2-05: 错误信息泄露内部细节 - 已在MED-09中处理,跳过
2026-04-03 09:39:32 +08:00

380 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"context"
"encoding/json"
"log"
"net/http"
"lijiaoqiao/supply-api/internal/middleware"
)
// IAM token claims context key
type iamContextKey string
const (
// IAMTokenClaimsKey 用于在context中存储token claims
IAMTokenClaimsKey iamContextKey = "iam_token_claims"
)
// IAMTokenClaims IAM扩展Token Claims
type IAMTokenClaims struct {
SubjectID string `json:"subject_id"`
Role string `json:"role"`
Scope []string `json:"scope"`
TenantID int64 `json:"tenant_id"`
UserType string `json:"user_type"` // 用户类型: platform/supply/consumer
Permissions []string `json:"permissions"` // 细粒度权限列表
}
// 角色层级定义
var roleHierarchyLevels = map[string]int{
"super_admin": 100,
"org_admin": 50,
"supply_admin": 40,
"consumer_admin": 40,
"operator": 30,
"developer": 20,
"finops": 20,
"supply_operator": 30,
"supply_finops": 20,
"supply_viewer": 10,
"consumer_operator": 30,
"consumer_viewer": 10,
"viewer": 10,
}
// ScopeAuthMiddleware Scope权限验证中间件
type ScopeAuthMiddleware struct {
// 路由-Scope映射
routeScopePolicies map[string][]string
// 角色层级已废弃使用包级变量roleHierarchyLevels
roleHierarchy map[string]int
}
// NewScopeAuthMiddleware 创建Scope权限验证中间件
func NewScopeAuthMiddleware() *ScopeAuthMiddleware {
return &ScopeAuthMiddleware{
routeScopePolicies: make(map[string][]string),
roleHierarchy: roleHierarchyLevels,
}
}
// SetRouteScopePolicy 设置路由的Scope要求
func (m *ScopeAuthMiddleware) SetRouteScopePolicy(route string, scopes []string) {
m.routeScopePolicies[route] = scopes
}
// CheckScope 检查是否拥有指定Scope
func CheckScope(ctx context.Context, requiredScope string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
// 空scope应该拒绝访问
if requiredScope == "" {
return false
}
return hasScope(claims.Scope, requiredScope)
}
// CheckAllScopes 检查是否拥有所有指定Scope
func CheckAllScopes(ctx context.Context, requiredScopes []string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
// 空列表直接通过
if len(requiredScopes) == 0 {
return true
}
for _, scope := range requiredScopes {
if !hasScope(claims.Scope, scope) {
return false
}
}
return true
}
// CheckAnyScope 检查是否拥有任一指定Scope
func CheckAnyScope(ctx context.Context, requiredScopes []string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
// 空列表直接通过
if len(requiredScopes) == 0 {
return true
}
for _, scope := range requiredScopes {
if hasScope(claims.Scope, scope) {
return true
}
}
return false
}
// HasRole 检查是否拥有指定角色
func HasRole(ctx context.Context, requiredRole string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
return claims.Role == requiredRole
}
// HasRoleLevel 检查角色层级是否满足要求
func HasRoleLevel(ctx context.Context, minLevel int) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
level := GetRoleLevel(claims.Role)
return level >= minLevel
}
// GetRoleLevel 获取角色层级数值
func GetRoleLevel(role string) int {
if level, ok := roleHierarchyLevels[role]; ok {
return level
}
return 0
}
// GetIAMTokenClaims 获取IAM Token Claims
func GetIAMTokenClaims(ctx context.Context) *IAMTokenClaims {
if claims, ok := ctx.Value(IAMTokenClaimsKey).(*IAMTokenClaims); ok {
return claims
}
return nil
}
// getIAMTokenClaims 内部获取IAM Token Claims
func getIAMTokenClaims(ctx context.Context) *IAMTokenClaims {
if claims, ok := ctx.Value(IAMTokenClaimsKey).(*IAMTokenClaims); ok {
return claims
}
return nil
}
// hasScope 检查scope列表是否包含目标scope
func hasScope(scopes []string, target string) bool {
for _, scope := range scopes {
if scope == target || scope == "*" {
return true
}
}
return false
}
// hasWildcardScope 检查scope列表是否包含通配符scope
func hasWildcardScope(scopes []string) bool {
for _, scope := range scopes {
if scope == "*" {
return true
}
}
return false
}
// logWildcardScopeAccess 记录通配符scope访问的审计日志
// P2-01: 通配符scope是安全风险应记录审计日志
func logWildcardScopeAccess(ctx context.Context, claims *IAMTokenClaims, requiredScope string) {
if claims == nil {
return
}
// 检查是否使用了通配符scope
if hasWildcardScope(claims.Scope) {
// 记录审计日志
log.Printf("[AUDIT] P2-01 WILDCARD_SCOPE_ACCESS: subject_id=%s, role=%s, required_scope=%s, tenant_id=%d, user_type=%s",
claims.SubjectID, claims.Role, requiredScope, claims.TenantID, claims.UserType)
}
}
// RequireScope 返回一个要求特定Scope的中间件
func (m *ScopeAuthMiddleware) RequireScope(requiredScope string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
// 检查scope
if requiredScope != "" && !hasScope(claims.Scope, requiredScope) {
writeAuthError(w, http.StatusForbidden, "AUTH_SCOPE_DENIED",
"required scope is not granted")
return
}
// P2-01: 记录通配符scope访问的审计日志
if hasWildcardScope(claims.Scope) {
logWildcardScopeAccess(r.Context(), claims, requiredScope)
}
next.ServeHTTP(w, r)
})
}
}
// RequireAllScopes 返回一个要求所有指定Scope的中间件
func (m *ScopeAuthMiddleware) RequireAllScopes(requiredScopes []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
for _, scope := range requiredScopes {
if !hasScope(claims.Scope, scope) {
writeAuthError(w, http.StatusForbidden, "AUTH_SCOPE_DENIED",
"required scope is not granted")
return
}
}
// P2-01: 记录通配符scope访问的审计日志
if hasWildcardScope(claims.Scope) {
logWildcardScopeAccess(r.Context(), claims, "")
}
next.ServeHTTP(w, r)
})
}
}
// RequireAnyScope 返回一个要求任一指定Scope的中间件
func (m *ScopeAuthMiddleware) RequireAnyScope(requiredScopes []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
// 空列表应该拒绝访问
if len(requiredScopes) == 0 || !hasAnyScope(claims.Scope, requiredScopes) {
writeAuthError(w, http.StatusForbidden, "AUTH_SCOPE_DENIED",
"none of the required scopes are granted")
return
}
// P2-01: 记录通配符scope访问的审计日志
if hasWildcardScope(claims.Scope) {
logWildcardScopeAccess(r.Context(), claims, "")
}
next.ServeHTTP(w, r)
})
}
}
// RequireRole 返回一个要求特定角色的中间件
func (m *ScopeAuthMiddleware) RequireRole(requiredRole string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
if claims.Role != requiredRole {
writeAuthError(w, http.StatusForbidden, "AUTH_ROLE_DENIED",
"required role is not granted")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireMinLevel 返回一个要求最小角色层级的中间件
func (m *ScopeAuthMiddleware) RequireMinLevel(minLevel int) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
level := GetRoleLevel(claims.Role)
if level < minLevel {
writeAuthError(w, http.StatusForbidden, "AUTH_ROLE_LEVEL_DENIED",
"insufficient role level")
return
}
next.ServeHTTP(w, r)
})
}
}
// hasAnyScope 检查scope列表是否包含任一目标scope
func hasAnyScope(scopes, targets []string) bool {
for _, scope := range scopes {
for _, target := range targets {
if scope == target || scope == "*" {
return true
}
}
}
return false
}
// writeAuthError 写入鉴权错误
func writeAuthError(w http.ResponseWriter, status int, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := map[string]interface{}{
"error": map[string]string{
"code": code,
"message": message,
},
}
json.NewEncoder(w).Encode(resp)
}
// WithIAMClaims 设置IAM Claims到Context
func WithIAMClaims(ctx context.Context, claims *IAMTokenClaims) context.Context {
return context.WithValue(ctx, IAMTokenClaimsKey, claims)
}
// GetClaimsFromLegacy 从原有middleware.TokenClaims转换为IAMTokenClaims
func GetClaimsFromLegacy(legacy *middleware.TokenClaims) *IAMTokenClaims {
if legacy == nil {
return nil
}
return &IAMTokenClaims{
SubjectID: legacy.SubjectID,
Role: legacy.Role,
Scope: legacy.Scope,
TenantID: legacy.TenantID,
}
}