Files
lijiaoqiao/supply-api/internal/iam/middleware/scope_auth_test.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

597 lines
18 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"
"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 := WithIAMClaims(context.Background(), 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 := WithIAMClaims(context.Background(), 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 := WithIAMClaims(context.Background(), 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 := WithIAMClaims(context.Background(), claims)
// act
hasEmptyScope := CheckScope(ctx, "")
// assert - 空scope应该拒绝访问安全修复
assert.False(t, hasEmptyScope, "empty scope should DENY access (security fix)")
}
// 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 := WithIAMClaims(context.Background(), 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 := WithIAMClaims(context.Background(), 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 := WithIAMClaims(context.Background(), 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 := WithIAMClaims(context.Background(), 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(WithIAMClaims(req.Context(), 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(WithIAMClaims(req.Context(), 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(WithIAMClaims(req.Context(), 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(WithIAMClaims(req.Context(), 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 := WithIAMClaims(context.Background(), 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)
}
// P0-01: 测试WithIAMClaims存储指针返回有效指针而非悬空指针
// 问题GetIAMTokenClaims返回指向栈帧的指针函数返回后指针无效
// 修复:改为存储和获取指针,返回有效堆内存指针
func TestP0_01_WithIAMClaims_ReturnsValidPointer(t *testing.T) {
// arrange - 创建一个claims并存储到context
originalClaims := &IAMTokenClaims{
SubjectID: "user:p0test1",
Role: "operator",
Scope: []string{"platform:read"},
TenantID: 100,
}
ctx := WithIAMClaims(context.Background(), originalClaims)
// act - 从context获取claims获取的应该是有效指针
retrievedClaims := GetIAMTokenClaims(ctx)
// assert - 返回的应该是有效指针指向与原始claims相同的内存
assert.NotNil(t, retrievedClaims, "retrieved claims should not be nil")
assert.Equal(t, originalClaims, retrievedClaims, "should return same pointer as stored")
assert.Equal(t, "user:p0test1", retrievedClaims.SubjectID, "SubjectID should match")
assert.Equal(t, "operator", retrievedClaims.Role, "Role should match")
// 验证修改原始对象后retrievedClaims能看到变化因为共享指针
originalClaims.Role = "super_admin"
assert.Equal(t, "super_admin", retrievedClaims.Role, "retrieved claims should see modification")
}
// P0-01: 测试GetIAMTokenClaims在context返回后仍然有效
func TestP0_01_GetIAMTokenClaims_PointerValidAfterReturn(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:ptrtest",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
// act - 存储到context
ctx := WithIAMClaims(context.Background(), claims)
// 在函数外获取claims模拟中间件在请求处理中访问
retrievedClaims := GetIAMTokenClaims(ctx)
// assert - 应该返回有效指针而不是nil或无效指针
assert.NotNil(t, retrievedClaims)
assert.Equal(t, claims, retrievedClaims, "should return exact same pointer")
assert.Equal(t, "user:ptrtest", retrievedClaims.SubjectID)
}
// P0-02: 测试writeAuthError写入响应体
func TestP0_02_writeAuthError_WritesResponseBody(t *testing.T) {
// arrange
rec := httptest.NewRecorder()
// act - 调用writeAuthError
writeAuthError(rec, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING", "authentication context is missing")
// assert - 响应体应该包含错误信息
body := rec.Body.String()
assert.NotEmpty(t, body, "response body should not be empty")
// 验证响应体包含错误码和消息
assert.Contains(t, body, "AUTH_CONTEXT_MISSING", "body should contain error code")
assert.Contains(t, body, "authentication context is missing", "body should contain error message")
assert.Equal(t, http.StatusUnauthorized, rec.Code, "status code should match")
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"), "content type should be JSON")
}
// P0-02: 测试writeAuthError在Forbidden状态下也写入响应体
func TestP0_02_writeAuthError_ForbiddenWritesBody(t *testing.T) {
// arrange
rec := httptest.NewRecorder()
// act
writeAuthError(rec, http.StatusForbidden, "AUTH_SCOPE_DENIED", "required scope is not granted")
// assert
body := rec.Body.String()
assert.NotEmpty(t, body, "response body should not be empty for Forbidden status")
assert.Contains(t, body, "AUTH_SCOPE_DENIED")
assert.Contains(t, body, "required scope is not granted")
}
// HIGH-01: CheckScope空scope应该拒绝访问而不应该绕过权限检查
func TestHIGH01_CheckScope_EmptyScopeShouldDenyAccess(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:high01",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := WithIAMClaims(context.Background(), claims)
// act - 空scope要求应该拒绝访问安全修复
hasEmptyScope := CheckScope(ctx, "")
// assert - 空scope应该返回false拒绝访问
assert.False(t, hasEmptyScope, "empty scope should DENY access (security fix)")
}
// MED-01: RequireAnyScope当requiredScopes为空时应该拒绝访问
func TestMED01_RequireAnyScope_EmptyScopesShouldDenyAccess(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// 传入空的requiredScopes
wrappedHandler := scopeAuth.RequireAnyScope([]string{})(handler)
claims := &IAMTokenClaims{
SubjectID: "user:med01",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(WithIAMClaims(req.Context(), claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert - 空scope列表应该拒绝访问安全修复
assert.Equal(t, http.StatusForbidden, rec.Code, "empty required scopes should DENY access (security fix)")
}
// P2-01: scope=="*"时直接返回true应记录审计日志
// 由于hasScope是内部函数我们通过中间件来验证通配符scope的行为
func TestP2_01_WildcardScope_SecurityRisk(t *testing.T) {
// 创建一个带通配符scope的claims
claims := &IAMTokenClaims{
SubjectID: "user:p2-01",
Role: "super_admin",
Scope: []string{"*"}, // 通配符scope代表所有权限
TenantID: 1,
}
ctx := WithIAMClaims(context.Background(), claims)
// 通配符scope应该能通过任何scope检查
assert.True(t, CheckScope(ctx, "platform:read"), "wildcard scope should have platform:read")
assert.True(t, CheckScope(ctx, "platform:write"), "wildcard scope should have platform:write")
assert.True(t, CheckScope(ctx, "any:custom:scope"), "wildcard scope should have any:custom:scope")
// 问题通配符scope被使用时没有记录审计日志
// 修复建议在hasScope返回true时如果scope是"*",应该记录审计日志
// 这是一个安全风险,因为无法追踪何时使用了超级权限
t.Logf("P2-01: Wildcard scope usage should be audited for security compliance")
}