feat(P1/P2): 完成TDD开发及P1/P2设计文档

## 设计文档
- multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO)
- audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO)
- routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO)
- sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO)
- compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO)

## TDD开发成果
- IAM模块: supply-api/internal/iam/ (111个测试)
- 审计日志模块: supply-api/internal/audit/ (40+测试)
- 路由策略模块: gateway/internal/router/ (33+测试)
- 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/

## 规范文档
- parallel_agent_output_quality_standards: 并行Agent产出质量规范
- project_experience_summary: 项目经验总结 (v2)
- 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划

## 评审报告
- 5个CONDITIONAL GO设计文档评审报告
- fix_verification_report: 修复验证报告
- full_verification_report: 全面质量验证报告
- tdd_module_quality_verification: TDD模块质量验证
- tdd_execution_summary: TDD执行总结

依据: Superpowers执行框架 + TDD规范
This commit is contained in:
Your Name
2026-04-02 23:35:53 +08:00
parent ed0961d486
commit 89104bd0db
94 changed files with 24738 additions and 5 deletions

View File

@@ -0,0 +1,296 @@
package middleware
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
// TestRoleInheritance_OperatorInheritsViewer 测试运维人员继承查看者
func TestRoleInheritance_OperatorInheritsViewer(t *testing.T) {
// arrange
// operator 显式配置拥有 viewer 所有 scope + platform:write 等
operatorScopes := []string{"platform:read", "platform:write", "tenant:read", "tenant:write", "billing:read"}
viewerScopes := []string{"platform:read", "tenant:read", "billing:read"}
operatorClaims := &IAMTokenClaims{
SubjectID: "user:1",
Role: "operator",
Scope: operatorScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *operatorClaims)
// act & assert - operator 应该拥有 viewer 的所有 scope
for _, viewerScope := range viewerScopes {
assert.True(t, CheckScope(ctx, viewerScope),
"operator should inherit viewer scope: %s", viewerScope)
}
// operator 还有额外的 scope
assert.True(t, CheckScope(ctx, "platform:write"))
assert.False(t, CheckScope(ctx, "platform:admin")) // viewer 没有 platform:admin
}
// TestRoleInheritance_ExplicitOverride 测试显式配置的Scope优先
func TestRoleInheritance_ExplicitOverride(t *testing.T) {
// arrange
// org_admin 显式配置拥有 operator + finops + developer + viewer 所有 scope
orgAdminScopes := []string{
// viewer scopes
"platform:read", "tenant:read", "billing:read",
// operator scopes
"platform:write", "tenant:write",
// finops scopes
"billing:write",
// developer scopes
"router:model:list",
// org_admin 自身 scope
"platform:admin", "tenant:member:manage",
}
orgAdminClaims := &IAMTokenClaims{
SubjectID: "user:2",
Role: "org_admin",
Scope: orgAdminScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *orgAdminClaims)
// act & assert - org_admin 应该拥有所有子角色的 scope
assert.True(t, CheckScope(ctx, "platform:read")) // viewer
assert.True(t, CheckScope(ctx, "tenant:read")) // viewer
assert.True(t, CheckScope(ctx, "billing:read")) // viewer/finops
assert.True(t, CheckScope(ctx, "platform:write")) // operator
assert.True(t, CheckScope(ctx, "tenant:write")) // operator
assert.True(t, CheckScope(ctx, "billing:write")) // finops
assert.True(t, CheckScope(ctx, "router:model:list")) // developer
assert.True(t, CheckScope(ctx, "platform:admin")) // org_admin 自身
}
// TestRoleInheritance_ViewerDoesNotInherit 测试查看者不继承任何角色
func TestRoleInheritance_ViewerDoesNotInherit(t *testing.T) {
// arrange
viewerScopes := []string{"platform:read", "tenant:read", "billing:read"}
viewerClaims := &IAMTokenClaims{
SubjectID: "user:3",
Role: "viewer",
Scope: viewerScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *viewerClaims)
// act & assert - viewer 是基础角色,不继承任何角色
assert.True(t, CheckScope(ctx, "platform:read"))
assert.False(t, CheckScope(ctx, "platform:write")) // viewer 没有 write
assert.False(t, CheckScope(ctx, "platform:admin")) // viewer 没有 admin
}
// TestRoleInheritance_SupplyChain 测试供应方角色链
func TestRoleInheritance_SupplyChain(t *testing.T) {
// arrange
// supply_admin > supply_operator > supply_viewer
supplyViewerScopes := []string{"supply:account:read", "supply:package:read"}
supplyOperatorScopes := []string{"supply:account:read", "supply:account:write", "supply:package:read", "supply:package:write", "supply:package:publish"}
supplyAdminScopes := []string{"supply:account:read", "supply:account:write", "supply:package:read", "supply:package:write", "supply:package:publish", "supply:package:offline", "supply:settlement:withdraw"}
// supply_viewer 测试
viewerCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:4",
Role: "supply_viewer",
Scope: supplyViewerScopes,
TenantID: 1,
})
// act & assert
assert.True(t, CheckScope(viewerCtx, "supply:account:read"))
assert.False(t, CheckScope(viewerCtx, "supply:account:write"))
// supply_operator 测试
operatorCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:5",
Role: "supply_operator",
Scope: supplyOperatorScopes,
TenantID: 1,
})
// act & assert - operator 继承 viewer
assert.True(t, CheckScope(operatorCtx, "supply:account:read"))
assert.True(t, CheckScope(operatorCtx, "supply:account:write"))
assert.False(t, CheckScope(operatorCtx, "supply:settlement:withdraw")) // operator 没有 withdraw
// supply_admin 测试
adminCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:6",
Role: "supply_admin",
Scope: supplyAdminScopes,
TenantID: 1,
})
// act & assert - admin 继承所有
assert.True(t, CheckScope(adminCtx, "supply:account:read"))
assert.True(t, CheckScope(adminCtx, "supply:settlement:withdraw"))
}
// TestRoleInheritance_ConsumerChain 测试需求方角色链
func TestRoleInheritance_ConsumerChain(t *testing.T) {
// arrange
// consumer_admin > consumer_operator > consumer_viewer
consumerViewerScopes := []string{"consumer:account:read", "consumer:apikey:read", "consumer:usage:read"}
consumerOperatorScopes := []string{"consumer:account:read", "consumer:account:write", "consumer:apikey:read", "consumer:apikey:create", "consumer:apikey:revoke", "consumer:usage:read"}
consumerAdminScopes := []string{"consumer:account:read", "consumer:account:write", "consumer:apikey:read", "consumer:apikey:create", "consumer:apikey:revoke", "consumer:usage:read"}
// consumer_viewer 测试
viewerCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:7",
Role: "consumer_viewer",
Scope: consumerViewerScopes,
TenantID: 1,
})
// act & assert
assert.True(t, CheckScope(viewerCtx, "consumer:account:read"))
assert.True(t, CheckScope(viewerCtx, "consumer:usage:read"))
assert.False(t, CheckScope(viewerCtx, "consumer:apikey:create"))
// consumer_operator 测试
operatorCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:8",
Role: "consumer_operator",
Scope: consumerOperatorScopes,
TenantID: 1,
})
// act & assert - operator 继承 viewer
assert.True(t, CheckScope(operatorCtx, "consumer:apikey:create"))
assert.True(t, CheckScope(operatorCtx, "consumer:apikey:revoke"))
// consumer_admin 测试
adminCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:9",
Role: "consumer_admin",
Scope: consumerAdminScopes,
TenantID: 1,
})
// act & assert - admin 继承所有
assert.True(t, CheckScope(adminCtx, "consumer:account:read"))
assert.True(t, CheckScope(adminCtx, "consumer:apikey:revoke"))
}
// TestRoleInheritance_MultipleRoles 测试多角色继承(显式配置模拟)
func TestRoleInheritance_MultipleRoles(t *testing.T) {
// arrange
// 假设用户同时拥有 developer 和 finops 角色(通过 scope 累加)
combinedScopes := []string{
// viewer scopes
"platform:read", "tenant:read", "billing:read",
// developer scopes
"router:model:list", "router:invoke",
// finops scopes
"billing:write",
}
combinedClaims := &IAMTokenClaims{
SubjectID: "user:10",
Role: "developer", // 主角色
Scope: combinedScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *combinedClaims)
// act & assert
assert.True(t, CheckScope(ctx, "platform:read")) // viewer
assert.True(t, CheckScope(ctx, "billing:read")) // viewer
assert.True(t, CheckScope(ctx, "router:model:list")) // developer
assert.True(t, CheckScope(ctx, "billing:write")) // finops
}
// TestRoleInheritance_SuperAdmin 测试超级管理员
func TestRoleInheritance_SuperAdmin(t *testing.T) {
// arrange
superAdminClaims := &IAMTokenClaims{
SubjectID: "user:11",
Role: "super_admin",
Scope: []string{"*"}, // 通配符拥有所有权限
TenantID: 0,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *superAdminClaims)
// act & assert - super_admin 拥有所有 scope
assert.True(t, CheckScope(ctx, "platform:read"))
assert.True(t, CheckScope(ctx, "platform:admin"))
assert.True(t, CheckScope(ctx, "supply:account:write"))
assert.True(t, CheckScope(ctx, "consumer:apikey:create"))
assert.True(t, CheckScope(ctx, "billing:write"))
}
// TestRoleInheritance_DeveloperInheritsViewer 测试开发者继承查看者
func TestRoleInheritance_DeveloperInheritsViewer(t *testing.T) {
// arrange
developerScopes := []string{"platform:read", "tenant:read", "billing:read", "router:invoke", "router:model:list"}
developerClaims := &IAMTokenClaims{
SubjectID: "user:12",
Role: "developer",
Scope: developerScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *developerClaims)
// act & assert - developer 继承 viewer 的所有 scope
assert.True(t, CheckScope(ctx, "platform:read"))
assert.True(t, CheckScope(ctx, "tenant:read"))
assert.True(t, CheckScope(ctx, "billing:read"))
assert.True(t, CheckScope(ctx, "router:invoke")) // developer 自身 scope
assert.False(t, CheckScope(ctx, "platform:write")) // developer 没有 write
}
// TestRoleInheritance_FinopsInheritsViewer 测试财务人员继承查看者
func TestRoleInheritance_FinopsInheritsViewer(t *testing.T) {
// arrange
finopsScopes := []string{"platform:read", "tenant:read", "billing:read", "billing:write"}
finopsClaims := &IAMTokenClaims{
SubjectID: "user:13",
Role: "finops",
Scope: finopsScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *finopsClaims)
// act & assert - finops 继承 viewer 的所有 scope
assert.True(t, CheckScope(ctx, "platform:read"))
assert.True(t, CheckScope(ctx, "tenant:read"))
assert.True(t, CheckScope(ctx, "billing:read"))
assert.True(t, CheckScope(ctx, "billing:write")) // finops 自身 scope
assert.False(t, CheckScope(ctx, "platform:write")) // finops 没有 write
}
// TestRoleInheritance_DeveloperDoesNotInheritOperator 测试开发者不继承运维
func TestRoleInheritance_DeveloperDoesNotInheritOperator(t *testing.T) {
// arrange
developerScopes := []string{"platform:read", "tenant:read", "billing:read", "router:invoke", "router:model:list"}
developerClaims := &IAMTokenClaims{
SubjectID: "user:14",
Role: "developer",
Scope: developerScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *developerClaims)
// act & assert - developer 不继承 operator 的 scope
assert.False(t, CheckScope(ctx, "platform:write")) // operator 有developer 没有
assert.False(t, CheckScope(ctx, "tenant:write")) // operator 有developer 没有
}

View File

@@ -0,0 +1,350 @@
package middleware
import (
"context"
"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"` // 细粒度权限列表
}
// ScopeAuthMiddleware Scope权限验证中间件
type ScopeAuthMiddleware struct {
// 路由-Scope映射
routeScopePolicies map[string][]string
// 角色层级
roleHierarchy map[string]int
}
// NewScopeAuthMiddleware 创建Scope权限验证中间件
func NewScopeAuthMiddleware() *ScopeAuthMiddleware {
return &ScopeAuthMiddleware{
routeScopePolicies: make(map[string][]string),
roleHierarchy: 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,
},
}
}
// 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 true
}
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 {
hierarchy := 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,
}
if level, ok := hierarchy[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
}
// 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
}
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
}
}
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
}
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,
},
}
_ = 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,
}
}

View File

@@ -0,0 +1,439 @@
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"lijiaoqiao/supply-api/internal/middleware"
)
// TestScopeAuth_CheckScope_SuperAdminHasAllScopes 测试超级管理员拥有所有Scope
func TestScopeAuth_CheckScope_SuperAdminHasAllScopes(t *testing.T) {
// arrange
// 创建超级管理员token claims
claims := &IAMTokenClaims{
SubjectID: "user:1",
Role: "super_admin",
Scope: []string{"*"}, // 通配符Scope代表所有权限
TenantID: 0,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
hasScope := CheckScope(ctx, "platform:read")
hasScope2 := CheckScope(ctx, "supply:account:write")
hasScope3 := CheckScope(ctx, "consumer:apikey:create")
// assert
assert.True(t, hasScope, "super_admin should have platform:read")
assert.True(t, hasScope2, "super_admin should have supply:account:write")
assert.True(t, hasScope3, "super_admin should have consumer:apikey:create")
}
// TestScopeAuth_CheckScope_ViewerHasReadOnly 测试Viewer只有只读权限
func TestScopeAuth_CheckScope_ViewerHasReadOnly(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:2",
Role: "viewer",
Scope: []string{"platform:read", "tenant:read", "billing:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckScope(ctx, "platform:read"), "viewer should have platform:read")
assert.True(t, CheckScope(ctx, "tenant:read"), "viewer should have tenant:read")
assert.True(t, CheckScope(ctx, "billing:read"), "viewer should have billing:read")
assert.False(t, CheckScope(ctx, "platform:write"), "viewer should NOT have platform:write")
assert.False(t, CheckScope(ctx, "tenant:write"), "viewer should NOT have tenant:write")
assert.False(t, CheckScope(ctx, "supply:account:write"), "viewer should NOT have supply:account:write")
}
// TestScopeAuth_CheckScope_Denied 测试Scope被拒绝
func TestScopeAuth_CheckScope_Denied(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:3",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.False(t, CheckScope(ctx, "platform:write"), "viewer should NOT have platform:write")
assert.False(t, CheckScope(ctx, "supply:account:write"), "viewer should NOT have supply:account:write")
}
// TestScopeAuth_CheckScope_MissingTokenClaims 测试缺少Token Claims
func TestScopeAuth_CheckScope_MissingTokenClaims(t *testing.T) {
// arrange
ctx := context.Background() // 没有token claims
// act
hasScope := CheckScope(ctx, "platform:read")
// assert
assert.False(t, hasScope, "should return false when token claims are missing")
}
// TestScopeAuth_CheckScope_EmptyScope 测试空Scope要求
func TestScopeAuth_CheckScope_EmptyScope(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:4",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
hasEmptyScope := CheckScope(ctx, "")
// assert
assert.True(t, hasEmptyScope, "empty scope should always pass")
}
// TestScopeAuth_CheckMultipleScopes 测试检查多个Scope需要全部满足
func TestScopeAuth_CheckMultipleScopes(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:5",
Role: "operator",
Scope: []string{"platform:read", "platform:write", "tenant:read", "tenant:write"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckAllScopes(ctx, []string{"platform:read", "platform:write"}), "operator should have both read and write")
assert.True(t, CheckAllScopes(ctx, []string{"tenant:read", "tenant:write"}), "operator should have both tenant scopes")
assert.False(t, CheckAllScopes(ctx, []string{"platform:read", "platform:admin"}), "operator should NOT have platform:admin")
}
// TestScopeAuth_CheckAnyScope 测试检查多个Scope只需满足其一
func TestScopeAuth_CheckAnyScope(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:6",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckAnyScope(ctx, []string{"platform:read", "platform:write"}), "should pass with one matching scope")
assert.False(t, CheckAnyScope(ctx, []string{"platform:write", "platform:admin"}), "should fail when no scopes match")
assert.True(t, CheckAnyScope(ctx, []string{}), "empty scope list should pass")
}
// TestScopeAuth_GetIAMTokenClaims 测试从Context获取IAMTokenClaims
func TestScopeAuth_GetIAMTokenClaims(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:7",
Role: "org_admin",
Scope: []string{"platform:read", "platform:write"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.NotNil(t, retrievedClaims)
assert.Equal(t, claims.SubjectID, retrievedClaims.SubjectID)
assert.Equal(t, claims.Role, retrievedClaims.Role)
assert.Equal(t, claims.Scope, retrievedClaims.Scope)
}
// TestScopeAuth_GetIAMTokenClaims_Missing 测试获取不存在的IAMTokenClaims
func TestScopeAuth_GetIAMTokenClaims_Missing(t *testing.T) {
// arrange
ctx := context.Background()
// act
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.Nil(t, retrievedClaims)
}
// TestScopeAuth_HasRole 测试用户角色检查
func TestScopeAuth_HasRole(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:8",
Role: "operator",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, HasRole(ctx, "operator"))
assert.False(t, HasRole(ctx, "viewer"))
assert.False(t, HasRole(ctx, "admin"))
}
// TestScopeAuth_HasRole_MissingClaims 测试缺少Claims时的角色检查
func TestScopeAuth_HasRole_MissingClaims(t *testing.T) {
// arrange
ctx := context.Background()
// act & assert
assert.False(t, HasRole(ctx, "operator"))
}
// TestScopeRoleAuthzMiddleware_WithScope 测试带Scope要求的中间件
func TestScopeRoleAuthzMiddleware_WithScope(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
// 创建一个带scope验证的handler
wrappedHandler := scopeAuth.RequireScope("platform:write")(handler)
// 创建一个带有token claims的请求
claims := &IAMTokenClaims{
SubjectID: "user:9",
Role: "operator",
Scope: []string{"platform:read", "platform:write"},
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
}
// TestScopeRoleAuthzMiddleware_Denied 测试Scope不足时中间件拒绝
func TestScopeRoleAuthzMiddleware_Denied(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireScope("platform:admin")(handler)
claims := &IAMTokenClaims{
SubjectID: "user:10",
Role: "viewer",
Scope: []string{"platform:read"}, // viewer没有platform:admin
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusForbidden, rec.Code)
}
// TestScopeRoleAuthzMiddleware_MissingClaims 测试缺少Claims时中间件拒绝
func TestScopeRoleAuthzMiddleware_MissingClaims(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireScope("platform:read")(handler)
req := httptest.NewRequest("GET", "/test", nil)
// 不设置token claims
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
// TestScopeRoleAuthzMiddleware_RequireAllScopes 测试要求所有Scope的中间件
func TestScopeRoleAuthzMiddleware_RequireAllScopes(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireAllScopes([]string{"platform:read", "tenant:read"})(handler)
claims := &IAMTokenClaims{
SubjectID: "user:11",
Role: "operator",
Scope: []string{"platform:read", "platform:write", "tenant:read"},
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
}
// TestScopeRoleAuthzMiddleware_RequireAllScopes_Denied 测试要求所有Scope但不足时拒绝
func TestScopeRoleAuthzMiddleware_RequireAllScopes_Denied(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireAllScopes([]string{"platform:read", "platform:admin"})(handler)
claims := &IAMTokenClaims{
SubjectID: "user:12",
Role: "viewer",
Scope: []string{"platform:read"}, // viewer没有platform:admin
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusForbidden, rec.Code)
}
// TestScopeAuth_HasRoleLevel 测试角色层级检查
func TestScopeAuth_HasRoleLevel(t *testing.T) {
// arrange
testCases := []struct {
role string
minLevel int
expected bool
}{
{"super_admin", 50, true},
{"super_admin", 100, true},
{"org_admin", 50, true},
{"org_admin", 60, false},
{"operator", 30, true},
{"operator", 40, false},
{"viewer", 10, true},
{"viewer", 20, false},
}
for _, tc := range testCases {
claims := &IAMTokenClaims{
SubjectID: "user:test",
Role: tc.role,
Scope: []string{},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
result := HasRoleLevel(ctx, tc.minLevel)
// assert
assert.Equal(t, tc.expected, result, "role=%s, minLevel=%d", tc.role, tc.minLevel)
}
}
// TestGetRoleLevel 测试获取角色层级
func TestGetRoleLevel(t *testing.T) {
testCases := []struct {
role string
expected int
}{
{"super_admin", 100},
{"org_admin", 50},
{"supply_admin", 40},
{"operator", 30},
{"developer", 20},
{"viewer", 10},
{"unknown_role", 0},
}
for _, tc := range testCases {
// act
level := GetRoleLevel(tc.role)
// assert
assert.Equal(t, tc.expected, level, "role=%s", tc.role)
}
}
// TestScopeAuth_WithIAMClaims 测试设置IAM Claims到Context
func TestScopeAuth_WithIAMClaims(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:13",
Role: "org_admin",
Scope: []string{"platform:read"},
TenantID: 1,
}
// act
ctx := WithIAMClaims(context.Background(), claims)
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.NotNil(t, retrievedClaims)
assert.Equal(t, claims.SubjectID, retrievedClaims.SubjectID)
assert.Equal(t, claims.Role, retrievedClaims.Role)
}
// TestGetClaimsFromLegacy 测试从原有TokenClaims转换
func TestGetClaimsFromLegacy(t *testing.T) {
// arrange
legacyClaims := &middleware.TokenClaims{
SubjectID: "user:14",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
// act
iamClaims := GetClaimsFromLegacy(legacyClaims)
// assert
assert.NotNil(t, iamClaims)
assert.Equal(t, legacyClaims.SubjectID, iamClaims.SubjectID)
assert.Equal(t, legacyClaims.Role, iamClaims.Role)
assert.Equal(t, legacyClaims.Scope, iamClaims.Scope)
assert.Equal(t, legacyClaims.TenantID, iamClaims.TenantID)
}