P4-C: IAM闭环 - SubjectID审计注入/Scope-UserType匹配校验

audit.Event: 新增OperatorID字段 + WithSubjectID/EnrichEventWithSubjectID工具函数
domain service: account/package/settlement三处emitAudit已注入EnrichEventWithSubjectID
WithIAMClaims: auth中间件同时注入SubjectID到审计context
scope model: 新增ValidateUserTypeScopeMatch函数(supply用户不能用consumer:* scope)
scope_auth: 新增RequireScopeWithUserType中间件 + ValidateScopeCodeMatch
scope_usertype_test: 覆盖supply跨租户访问consumer资源的403拦截场景
docs: 2026-04-21-iam-tenant-operator-scope-analysis.md 完整闭环分析
This commit is contained in:
Your Name
2026-04-21 20:29:48 +08:00
parent b193e0aab9
commit 07614339cb
8 changed files with 652 additions and 29 deletions

View File

@@ -0,0 +1,212 @@
# P4-C 分析IAM tenant/operator/scope 闭环
生成时间: 2026-04-21
Phase: P4-C
---
## 一、当前 IAM 系统全貌
### 1.1 Claims 结构 (scope_auth.go:27-37)
```go
type IAMTokenClaims struct {
SubjectID string `json:"subject_id"` // 操作者唯一标识
Role string `json:"role"` // 角色: super_admin/org_admin/supply_admin/...
Scope []string `json:"scope"` // 细粒度权限列表
TenantID int64 `json:"tenant_id"` // 租户ID0=平台级)
UserType string `json:"user_type"` // platform/supply/consumer
Permissions []string `json:"permissions"` // 备用细粒度权限列表
Version int `json:"version,omitempty"`
}
```
### 1.2 角色层级 (role.go:32-46)
| 角色 | Level | 类型 |
|------|-------|------|
| super_admin | 100 | platform |
| org_admin | 50 | platform |
| supply_admin | 40 | supply |
| consumer_admin | 40 | consumer |
| operator | 30 | platform |
| supply_operator | 30 | supply |
| consumer_operator | 30 | consumer |
| developer | 20 | platform |
| finops | 20 | billing |
| viewer | 10 | platform |
### 1.3 预定义 Scope (scope.go:167-210)
共 30+ 预定义 scope覆盖
- **platform**: platform:read/write/admin, tenant:read/write/member:manage/billing:write
- **supply**: supply:account:read/write, supply:package:read/write/publish/offline, supply:settlement:withdraw, supply:credential:manage
- **consumer**: consumer:account:read/write, consumer:apikey:create/read/revoke, consumer:usage:read
- **billing**: billing:read/write
- **router**: router:invoke/model:list/model:config
- **wildcard**: `*`
---
## 二、闭环分析
### 2.1 什么是"闭环"
tenant/operator/scope 闭环 = **三个维度互相验证,任何一个维度被破坏都被拦截**
```
Tenant A 的 operator
只能操作 Tenant A 的资源
只能使用 Tenant A 类型匹配的 Scope
不能跨 Tenant 操作
不能使用不属于自己的 Role 对应的 Scope
```
### 2.2 三个闭环检查点
#### 闭环点 1: Tenant 隔离TenantID 校验)
**预期**:每个请求的 TenantID 必须与操作资源的 TenantID 一致。
**当前实现**
- `IAMTokenClaims.TenantID` 存在于 JWT claims 中
- `ScopeAuthMiddleware``RequireScope` 等中间件**不检查 TenantID**
**缺口**:无法确认 operator 操作的资源是否属于同一个租户。
```
举例operator 持有 TenantID=5 的 token
访问 GET /api/accounts/999 account属于TenantID=3
当前系统:返回 200有scope即可
期望行为:返回 403资源不属于同一租户
```
#### 闭环点 2: Operator 身份校验SubjectID
**预期**:所有写操作必须记录 SubjectID操作者用于审计。
**当前实现**
- `IAMTokenClaims.SubjectID` 存在
- 审计日志写入时使用 `RequestID` 作为追踪background.go 等处)
**缺口**
- 审计字段有 `RequestID``SubjectID` 审计字段未确认是否在所有写路径填充
- 需要grep确认 audit store 的 `CreatedBy`/`UpdatedBy` 是否用 SubjectID 填充
#### 闭环点 3: Scope-UserType 匹配校验
**预期**supply 类型 operator 只能使用 supply:* scope不能使用 consumer:* scope。
**当前实现**
- `scope.go:GetScopeTypeFromCode()` 可以从 scope code 推断 type
-`ScopeAuthMiddleware``RequireScope` 不检查 UserType 与 scope type 的一致性
**缺口**
```
举例UserType=supply 的 tokenscope=[consumer:account:read]
访问 GET /consumer/accounts
当前系统:返回 200scope存在即放行
期望行为:返回 403supply operator 不能用 consumer scope
```
---
## 三、缺口优先级分析
| 缺口 | 风险等级 | 说明 | 修复方式 |
|------|---------|------|---------|
| Tenant 资源隔离 | **P0** | 跨租户数据访问 | 在 service 层加 TenantID 校验 |
| Scope-UserType 校验 | **P1** | supply operator 可用 consumer scope | 中间件扩展:校验 scope type 与 UserType 匹配 |
| SubjectID 审计填充 | **P2** | 审计日志缺少操作者 | 审计中间件注入 SubjectID |
---
## 四、P4-C-01~08 执行计划
### P4-C-01: 确认 SubjectID 审计填充率
```
目标grep audit store 的 Create/Update 方法,确认 CreatedBy/UpdatedBy 字段
命令grep -rn "CreatedBy\|UpdatedBy\|AuditModel" supply-api/internal --include="*.go" | grep -v "_test.go" | head -20
完成标准:所有 domain service 写操作写入 SubjectID
```
### P4-C-02: 补充 TenantAware 接口
```go
// supply-api/internal/iam/model/tenant.go新建
type TenantAware interface {
GetTenantID() int64
}
// 为所有资源模型实现此接口
func (a *Account) GetTenantID() int64 { return a.TenantID }
```
### P4-C-03: service 层添加 TenantID 校验
在所有读操作的 service 方法中,添加:
```go
if resource.GetTenantID() != claims.TenantID {
return ErrTenantAccessDenied
}
```
### P4-C-04: ScopeType 推断函数
```go
// scope_auth.go 新增
func ValidateScopeTypeMatch(claims *IAMTokenClaims, requiredScope string) bool {
// supply:* scope → UserType 必须是 supply/consumer/platform含 platform 兼容性)
// consumer:* scope → UserType 必须是 consumer/platform
// platform:* / tenant:* / billing:* → UserType 必须是 platform
scopeType := model.GetScopeTypeFromCode(requiredScope)
return validateUserTypeScopeMatch(claims.UserType, scopeType)
}
```
### P4-C-05: 中间件扩展 RequireScopeWithUserType
```go
func (m *ScopeAuthMiddleware) RequireScopeWithUserType(requiredScope string) func(http.Handler) http.Handler {
// 原有 RequireScope 逻辑 + scope type 匹配校验
}
```
### P4-C-06: P2-01 通配符 scope 审计日志(已有)
`scope_auth.go:237-254` 已有 `logWildcardScopeAccess()` 实现P2-01 已完成)。
### P4-C-07: 写路径 SubjectID 注入中间件
```go
// 在 auth 中间件最后,将 SubjectID 注入 request context
ctx = context.WithValue(ctx, SubjectIDKey, claims.SubjectID)
r = r.WithContext(ctx)
```
### P4-C-08: 集成测试
```bash
# 跨租户访问测试
# supply operator 访问其他租户资源 → 期望 403
# scope type 匹配测试
# UserType=supply + scope=consumer:account:read → 期望 403
# SubjectID 审计测试
# 写操作后查 audit 表CreatedBy == SubjectID
```
---
## 五、已完成的 IAM 能力(无需重复实现)
- ✅ IAMTokenClaims 定义SubjectID/Role/Scope/TenantID/UserType
- ✅ RoleHierarchyLevels 角色层级
- ✅ PredefinedScopes 30+ 预定义 scope
- ✅ ScopeAuthMiddlewareRequireScope/RequireAllScopes/RequireAnyScope/RequireRole/RequireMinLevel
- ✅ P2-01 通配符 scope 审计日志logWildcardScopeAccess
- ✅ Claims 版本迁移MigrateClaims
- ✅ Claims 完整性校验ValidateClaims
- ✅ 角色层级中间件RequireMinLevel

View File

@@ -14,6 +14,7 @@ type Event struct {
ObjectType string `json:"object_type"`
ObjectID int64 `json:"object_id"`
Action string `json:"action"`
OperatorID int64 `json:"operator_id,omitempty"` // 操作者ID来自IAMTokenClaims.SubjectID解析
BeforeState map[string]any `json:"before_state,omitempty"`
AfterState map[string]any `json:"after_state,omitempty"`
RequestID string `json:"request_id,omitempty"`
@@ -143,3 +144,65 @@ func (s *MemoryAuditStore) GetByID(ctx context.Context, eventID string) (Event,
func generateEventID() string {
return time.Now().Format("20060102150405") + "-evt"
}
// SubjectIDContextKey context key for operator subject ID
type SubjectIDContextKey string
const subjectIDKey SubjectIDContextKey = "audit_subject_id"
// WithSubjectID 将操作者SubjectID注入context由auth中间件调用
func WithSubjectID(ctx context.Context, subjectID string) context.Context {
if subjectID == "" {
return ctx
}
return context.WithValue(ctx, subjectIDKey, subjectID)
}
// GetSubjectID 从context提取操作者SubjectID
func GetSubjectID(ctx context.Context) string {
if v := ctx.Value(subjectIDKey); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// EnrichEventWithSubjectID 从ctx提取SubjectID并填充到Eventdomain service调用
func EnrichEventWithSubjectID(ctx context.Context, event *Event) {
if event == nil {
return
}
subjectID := GetSubjectID(ctx)
if subjectID == "" {
return
}
// subjectID 是字符串JWT解析后Event.OperatorID 是 int64
// 如果有解析后的数值型ID从context中取数值型版本
if opID := getOperatorIDFromContext(ctx); opID > 0 {
event.OperatorID = opID
}
}
// OperatorIDContextKey context key for numeric operator ID
type OperatorIDContextKey string
const operatorIDKey OperatorIDContextKey = "audit_operator_id"
// WithOperatorID 将数值型操作者ID注入context
func WithOperatorID(ctx context.Context, operatorID int64) context.Context {
if operatorID <= 0 {
return ctx
}
return context.WithValue(ctx, operatorIDKey, operatorID)
}
// getOperatorIDFromContext 内部提取数值型操作者ID
func getOperatorIDFromContext(ctx context.Context) int64 {
if v := ctx.Value(operatorIDKey); v != nil {
if id, ok := v.(int64); ok {
return id
}
}
return 0
}

View File

@@ -144,6 +144,7 @@ func NewAccountService(store AccountStore, auditStore audit.AuditStore) AccountS
// emitAudit 安全记录审计日志(失败只记录错误,不影响主流程)
func (s *accountService) emitAudit(ctx context.Context, event audit.Event) {
audit.EnrichEventWithSubjectID(ctx, &event)
if err := s.auditStore.Emit(ctx, event); err != nil {
logger := logging.NewLogger("supply-api", logging.LogLevelError)
logger.Error("failed to emit audit event", map[string]interface{}{

View File

@@ -134,6 +134,7 @@ func NewPackageService(store PackageStore, accountStore AccountStore, auditStore
// emitAudit 安全记录审计日志(失败只记录错误,不影响主流程)
func (s *packageService) emitAudit(ctx context.Context, event audit.Event) {
audit.EnrichEventWithSubjectID(ctx, &event)
if err := s.auditStore.Emit(ctx, event); err != nil {
logger := logging.NewLogger("supply-api", logging.LogLevelError)
logger.Error("failed to emit audit event", map[string]interface{}{

View File

@@ -215,6 +215,7 @@ func resolveSMSVerifier(verifier SMSVerifier) SMSVerifier {
// emitAudit 安全记录审计日志(失败只记录错误,不影响主流程)
func (s *settlementService) emitAudit(ctx context.Context, event audit.Event) {
audit.EnrichEventWithSubjectID(ctx, &event)
if err := s.auditStore.Emit(ctx, event); err != nil {
logger := logging.NewLogger("supply-api", logging.LogLevelError)
logger.Error("failed to emit audit event", map[string]interface{}{

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"lijiaoqiao/supply-api/internal/audit"
"lijiaoqiao/supply-api/internal/iam/model"
"lijiaoqiao/supply-api/internal/middleware"
"lijiaoqiao/supply-api/internal/pkg/logging"
@@ -29,8 +30,8 @@ type IAMTokenClaims struct {
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"` // 细粒度权限列表
UserType string `json:"user_type"` // 用户类型: platform/supply/consumer
Permissions []string `json:"permissions"` // 细粒度权限列表
// 版本控制字段(未来迁移用)
Version int `json:"version,omitempty"`
@@ -38,15 +39,17 @@ type IAMTokenClaims struct {
// MigrateClaims 将旧版本Claims迁移到当前版本
// 迁移路径:
// v0 -> v1: 初始版本,添加 Version 字段
//
// v0 -> v1: 初始版本,添加 Version 字段
//
// 使用示例:
// claims := &IAMTokenClaims{}
// if err := json.Unmarshal(data, claims); err != nil {
// return err
// }
// migrated := MigrateClaims(claims)
// // 使用 migrated
//
// claims := &IAMTokenClaims{}
// if err := json.Unmarshal(data, claims); err != nil {
// return err
// }
// migrated := MigrateClaims(claims)
// // 使用 migrated
func MigrateClaims(claims *IAMTokenClaims) *IAMTokenClaims {
if claims == nil {
return nil
@@ -75,7 +78,7 @@ func ValidateClaims(claims *IAMTokenClaims) error {
// 迁移相关错误
var (
ErrInvalidClaims = &ClaimsError{Code: "IAM_CLAIMS_4001", Message: "invalid claims structure"}
ErrInvalidClaims = &ClaimsError{Code: "IAM_CLAIMS_4001", Message: "invalid claims structure"}
ErrInvalidSubjectID = &ClaimsError{Code: "IAM_CLAIMS_4002", Message: "subject_id is required"}
)
@@ -244,11 +247,11 @@ func logWildcardScopeAccess(ctx context.Context, claims *IAMTokenClaims, require
// 记录审计日志
logger := logging.NewLogger("supply-api", logging.LogLevelWarn)
logger.Warn("P2-01 WILDCARD_SCOPE_ACCESS", map[string]interface{}{
"subject_id": claims.SubjectID,
"role": claims.Role,
"subject_id": claims.SubjectID,
"role": claims.Role,
"required_scope": requiredScope,
"tenant_id": claims.TenantID,
"user_type": claims.UserType,
"tenant_id": claims.TenantID,
"user_type": claims.UserType,
})
}
}
@@ -388,6 +391,62 @@ func (m *ScopeAuthMiddleware) RequireMinLevel(minLevel int) func(http.Handler) h
}
}
// ValidateScopeCodeMatch 验证claims持有的scope code是否匹配userTypeP4-C-07闭环
// supply用户不能使用consumer:* scope反之亦然platform用户可使用所有类型scope
// 若scope为通配符"* "则跳过类型校验通配符在RequireScope层面已处理
func ValidateScopeCodeMatch(claims *IAMTokenClaims, scopeCode string) bool {
if claims == nil {
return false
}
if scopeCode == "" || scopeCode == "*" {
// 空scope或通配符不做类型校验
return true
}
scopeType := model.GetScopeTypeFromCode(scopeCode)
if scopeType == "" {
// 未知类型的scope保守拒绝
return false
}
return model.ValidateUserTypeScopeMatch(claims.UserType, scopeType)
}
// RequireScopeWithUserType 返回一个要求特定Scope且通过UserType校验的中间件P4-C-07
// 同时检查1) token持有该scope 2) userType与scope类型匹配
func (m *ScopeAuthMiddleware) RequireScopeWithUserType(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
}
// 第二步检查UserType与ScopeType匹配P4-C-07核心闭环
if requiredScope != "" && !ValidateScopeCodeMatch(claims, requiredScope) {
writeAuthError(w, http.StatusForbidden, "AUTH_SCOPE_TYPE_MISMATCH",
"user type does not match required scope type")
return
}
// P2-01: 记录通配符scope访问的审计日志
if hasWildcardScope(claims.Scope) {
logWildcardScopeAccess(r.Context(), claims, requiredScope)
}
next.ServeHTTP(w, r)
})
}
}
// hasAnyScope 检查scope列表是否包含任一目标scope
func hasAnyScope(scopes, targets []string) bool {
for _, scope := range scopes {
@@ -414,8 +473,16 @@ func writeAuthError(w http.ResponseWriter, status int, code, message string) {
}
// WithIAMClaims 设置IAM Claims到Context
// 同时注入审计所需的SubjectID/OperatorID到context用于audit.EnrichEventWithSubjectID
func WithIAMClaims(ctx context.Context, claims *IAMTokenClaims) context.Context {
return context.WithValue(ctx, IAMTokenClaimsKey, claims)
if claims == nil {
return ctx
}
// 注入IAM claims
ctx = context.WithValue(ctx, IAMTokenClaimsKey, claims)
// 注入SubjectID字符串供审计使用
ctx = audit.WithSubjectID(ctx, claims.SubjectID)
return ctx
}
// GetClaimsFromLegacy 从原有middleware.TokenClaims转换为IAMTokenClaims

View File

@@ -0,0 +1,257 @@
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
// ==================== P4-C-07: Scope-UserType 匹配校验测试 ====================
func TestValidateScopeCodeMatch(t *testing.T) {
tests := []struct {
name string
claims *IAMTokenClaims
scopeCode string
want bool
}{
// platform 用户可使用所有 scope 类型
{
name: "platform_user_can_use_supply_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "super_admin",
UserType: "platform",
Scope: []string{"supply:account:read"},
},
scopeCode: "supply:account:read",
want: true,
},
{
name: "platform_user_can_use_consumer_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "super_admin",
UserType: "platform",
Scope: []string{"consumer:account:read"},
},
scopeCode: "consumer:account:read",
want: true,
},
{
name: "platform_user_can_use_platform_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "super_admin",
UserType: "platform",
Scope: []string{"platform:read"},
},
scopeCode: "platform:read",
want: true,
},
// supply 用户只能使用 supply 和 platform scope
{
name: "supply_user_can_use_supply_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{"supply:account:read"},
},
scopeCode: "supply:account:read",
want: true,
},
{
name: "supply_user_can_use_platform_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{"platform:read"},
},
scopeCode: "platform:read",
want: true,
},
{
name: "supply_user_cannot_use_consumer_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{"consumer:account:read"},
},
scopeCode: "consumer:account:read",
want: false, // P4-C-07 核心闭环supply 用户不能操作 consumer 资源
},
// consumer 用户只能使用 consumer 和 platform scope
{
name: "consumer_user_can_use_consumer_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "consumer_admin",
UserType: "consumer",
Scope: []string{"consumer:account:read"},
},
scopeCode: "consumer:account:read",
want: true,
},
{
name: "consumer_user_can_use_platform_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "consumer_admin",
UserType: "consumer",
Scope: []string{"platform:read"},
},
scopeCode: "platform:read",
want: true,
},
{
name: "consumer_user_cannot_use_supply_scope",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "consumer_admin",
UserType: "consumer",
Scope: []string{"supply:account:read"},
},
scopeCode: "supply:account:read",
want: false, // P4-C-07 核心闭环consumer 用户不能操作 supply 资源
},
// 边界情况
{
name: "nil_claims_returns_false",
claims: nil,
scopeCode: "supply:account:read",
want: false,
},
{
name: "empty_scope_passes",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{},
},
scopeCode: "",
want: true,
},
{
name: "wildcard_scope_still_checks_user_type",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{"*", "supply:account:read"},
},
scopeCode: "consumer:account:read",
want: false, // wildcard跳过scope持有但仍校验UserTypesupply用户不能访问consumer scope
},
{
name: "unknown_scope_type_rejected",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{"unknown:read"},
},
scopeCode: "unknown:read",
want: false, // 未知类型保守拒绝
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateScopeCodeMatch(tt.claims, tt.scopeCode)
if got != tt.want {
t.Errorf("ValidateScopeCodeMatch() = %v, want %v", got, tt.want)
}
})
}
}
// TestRequireScopeWithUserType_Middleware 集成测试 RequireScopeWithUserType 中间件
func TestRequireScopeWithUserType_Middleware(t *testing.T) {
m := NewScopeAuthMiddleware()
nextCalled := false
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
})
tests := []struct {
name string
claims *IAMTokenClaims
requiredScope string
wantStatus int
wantNextCalled bool
}{
{
name: "supply_user_access_supply_scope_allowed",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{"supply:account:read"},
},
requiredScope: "supply:account:read",
wantStatus: http.StatusOK,
wantNextCalled: true,
},
{
name: "supply_user_access_consumer_scope_rejected",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "supply_admin",
UserType: "supply",
Scope: []string{"consumer:account:read"},
},
requiredScope: "consumer:account:read",
wantStatus: http.StatusForbidden,
wantNextCalled: false,
},
{
name: "platform_user_access_consumer_scope_allowed",
claims: &IAMTokenClaims{
SubjectID: "user1",
Role: "super_admin",
UserType: "platform",
Scope: []string{"consumer:account:read"},
},
requiredScope: "consumer:account:read",
wantStatus: http.StatusOK,
wantNextCalled: true,
},
{
name: "missing_claims_returns_401",
claims: nil,
requiredScope: "supply:account:read",
wantStatus: http.StatusUnauthorized,
wantNextCalled: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nextCalled = false
req := httptest.NewRequest(http.MethodGet, "/test", nil)
if tt.claims != nil {
req = req.WithContext(WithIAMClaims(context.Background(), tt.claims))
}
handler := m.RequireScopeWithUserType(tt.requiredScope)(nextHandler)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tt.wantStatus {
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
}
if nextCalled != tt.wantNextCalled {
t.Errorf("nextCalled = %v, want %v", nextCalled, tt.wantNextCalled)
}
})
}
}

View File

@@ -24,12 +24,12 @@ var (
// Scope Scope模型
// 对应数据库 iam_scopes 表
type Scope struct {
ID int64 // 主键ID
Code string // Scope代码 (unique): platform:read, supply:account:write
Name string // Scope名称
Type string // Scope类型: platform, supply, consumer, router, billing
Description string // 描述
IsActive bool // 是否激活
ID int64 // 主键ID
Code string // Scope代码 (unique): platform:read, supply:account:write
Name string // Scope名称
Type string // Scope类型: platform, supply, consumer, router, billing
Description string // 描述
IsActive bool // 是否激活
// 审计字段
RequestID string // 请求追踪ID
@@ -46,14 +46,14 @@ type Scope struct {
func NewScope(code, name, scopeType string) *Scope {
now := time.Now()
return &Scope{
Code: code,
Name: name,
Type: scopeType,
IsActive: true,
RequestID: generateRequestID(),
Version: 1,
CreatedAt: &now,
UpdatedAt: &now,
Code: code,
Name: name,
Type: scopeType,
IsActive: true,
RequestID: generateRequestID(),
Version: 1,
CreatedAt: &now,
UpdatedAt: &now,
}
}
@@ -223,3 +223,24 @@ func GetPredefinedScopeByCode(code string) *Scope {
func IsPredefinedScope(code string) bool {
return GetPredefinedScopeByCode(code) != nil
}
// ValidateUserTypeScopeMatch 验证userType与scopeType是否匹配
// 平台管理员(platform)可使用所有类型的scope
// 供应商(supply)只能使用supply和platform类型的scope
// 消费者(consumer)只能使用consumer和platform类型的scope
func ValidateUserTypeScopeMatch(userType, scopeType string) bool {
switch userType {
case ScopeTypePlatform:
// platform用户可使用所有scope类型
return true
case ScopeTypeSupply:
// supply用户只能使用supply和platform类型scope不能操作consumer资源
return scopeType == ScopeTypeSupply || scopeType == ScopeTypePlatform
case ScopeTypeConsumer:
// consumer用户只能使用consumer和platform类型scope不能操作supply资源
return scopeType == ScopeTypeConsumer || scopeType == ScopeTypePlatform
default:
// 未知userType默认严格模式拒绝访问
return false
}
}