将全局启用、用户组基线、API Key 专属或排除规则及 scope 按固定顺序求值,避免 Key 越过所属用户组权限,并让运行时候选与模型列表共用同一权限链。 新增 Key 级可分配模型与失效规则诊断接口、OpenAI 兼容 /v1/models 及 rich 列表迁移路径;前端权限弹窗改为按当前 Key 实时加载并支持清理失效规则。 验证:Go 全量测试与 go vet 通过;Web 22 个测试文件共 142 项通过;pnpm lint、pnpm openapi、pnpm build、Compose 配置、gofmt、ShellCheck 和 git diff --check 通过;独立 PostgreSQL 真实配置验收通过。
151 lines
7.9 KiB
Go
151 lines
7.9 KiB
Go
package store
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
|
)
|
|
|
|
func TestLayeredAccessDoesNotLetAPIKeyExpandBaseline(t *testing.T) {
|
|
model := PlatformModel{ID: "model-1", PlatformID: "platform-1", BaseModelID: "base-1"}
|
|
groupUser := &auth.User{GatewayUserID: "user-1", UserGroupID: "group-1", APIKeyID: "key-1"}
|
|
baselineRules := []AccessRule{{
|
|
SubjectType: "user_group", SubjectID: "group-1", ResourceType: "platform_model", ResourceID: "model-1", Effect: "deny",
|
|
}}
|
|
keyRules := []AccessRule{{
|
|
SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow",
|
|
}}
|
|
baseline := filterPlatformModelsByRuleSet([]PlatformModel{model}, baselineRules, baselineAccessRuleSubjects(groupUser), 0)
|
|
actual := filterPlatformModelsByRuleSet(baseline, keyRules, apiKeyAccessRuleSubjects(groupUser), 0)
|
|
if len(actual) != 0 {
|
|
t.Fatalf("api key allow expanded denied baseline: %+v", actual)
|
|
}
|
|
}
|
|
|
|
func TestAPIKeyAllowControlsOnlyMatchingKeys(t *testing.T) {
|
|
model := PlatformModel{ID: "model-1", PlatformID: "platform-1"}
|
|
rules := []AccessRule{{
|
|
SubjectType: "api_key", SubjectID: "key-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow",
|
|
}}
|
|
for _, test := range []struct {
|
|
keyID string
|
|
want int
|
|
}{{"key-a", 1}, {"key-b", 0}} {
|
|
user := &auth.User{APIKeyID: test.keyID}
|
|
actual := filterPlatformModelsByRuleSet([]PlatformModel{model}, rules, apiKeyAccessRuleSubjects(user), 0)
|
|
if len(actual) != test.want {
|
|
t.Fatalf("key %s received %d models, want %d", test.keyID, len(actual), test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLayeredAccessDenyWinsAndNoRulesInherit(t *testing.T) {
|
|
model := PlatformModel{ID: "model-1", PlatformID: "platform-1", ModelType: StringList{"text_generate"}}
|
|
keyUser := &auth.User{APIKeyID: "key-a", APIKeyScopes: []string{"chat"}}
|
|
keyUsers := map[string]*auth.User{"key-a": keyUser}
|
|
if got := filterPlatformModelsByLayeredRuleSet(keyUser, []PlatformModel{model}, nil, nil, keyUsers); len(got) != 1 {
|
|
t.Fatalf("key without rules did not inherit baseline: %+v", got)
|
|
}
|
|
rules := []AccessRule{
|
|
{SubjectType: "api_key", SubjectID: "key-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow"},
|
|
{SubjectType: "api_key", SubjectID: "key-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "deny"},
|
|
}
|
|
if got := filterPlatformModelsByLayeredRuleSet(keyUser, []PlatformModel{model}, nil, rules, keyUsers); len(got) != 0 {
|
|
t.Fatalf("matching deny did not override allow: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDirectUserIgnoresAPIKeyRules(t *testing.T) {
|
|
model := PlatformModel{ID: "model-1", PlatformID: "platform-1", ModelType: StringList{"text_generate"}}
|
|
rules := []AccessRule{{
|
|
SubjectType: "api_key", SubjectID: "key-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow",
|
|
}}
|
|
keyUsers := map[string]*auth.User{"key-a": {APIKeyID: "key-a", APIKeyScopes: []string{"chat"}}}
|
|
if got := filterPlatformModelsByLayeredRuleSet(&auth.User{GatewayUserID: "user-1"}, []PlatformModel{model}, nil, rules, keyUsers); len(got) != 1 {
|
|
t.Fatalf("direct user was constrained by API key exclusive rule: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestIneffectiveAPIKeyRuleDoesNotControlAnotherKey(t *testing.T) {
|
|
model := PlatformModel{ID: "model-1", PlatformID: "platform-1", ModelType: StringList{"text_generate"}}
|
|
staleRule := AccessRule{SubjectType: "api_key", SubjectID: "key-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow"}
|
|
groupDeny := AccessRule{SubjectType: "user_group", SubjectID: "group-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "deny"}
|
|
keyUsers := map[string]*auth.User{
|
|
"key-a": {APIKeyID: "key-a", UserGroupID: "group-a", APIKeyScopes: []string{"chat"}},
|
|
}
|
|
keyB := &auth.User{APIKeyID: "key-b", UserGroupID: "group-b", APIKeyScopes: []string{"chat"}}
|
|
if got := filterPlatformModelsByLayeredRuleSet(keyB, []PlatformModel{model}, []AccessRule{groupDeny}, []AccessRule{staleRule}, keyUsers); len(got) != 1 {
|
|
t.Fatalf("stale key-a rule blocked authorized key-b: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestPlatformRuleOnlyControlsModelsItsKeyCanAccess(t *testing.T) {
|
|
allowed := PlatformModel{ID: "allowed", PlatformID: "platform-1", ModelType: StringList{"image_generate"}}
|
|
denied := PlatformModel{ID: "denied", PlatformID: "platform-1", ModelType: StringList{"text_generate"}}
|
|
rule := AccessRule{SubjectType: "api_key", SubjectID: "key-a", ResourceType: "platform", ResourceID: "platform-1", Effect: "allow"}
|
|
groupDeny := AccessRule{SubjectType: "user_group", SubjectID: "group-a", ResourceType: "platform_model", ResourceID: "denied", Effect: "deny"}
|
|
ruleUser := &auth.User{APIKeyID: "key-a", UserGroupID: "group-a", APIKeyScopes: []string{"image"}}
|
|
users := map[string]*auth.User{"key-a": ruleUser}
|
|
if got := effectiveAPIKeyRulesForPlatformModel([]AccessRule{rule}, []AccessRule{groupDeny}, users, allowed); len(got) != 1 {
|
|
t.Fatalf("platform rule should control the allowed image model: %+v", got)
|
|
}
|
|
if got := effectiveAPIKeyRulesForPlatformModel([]AccessRule{rule}, []AccessRule{groupDeny}, users, denied); len(got) != 0 {
|
|
t.Fatalf("platform rule controlled a group-denied or scope-denied model: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFilterPlatformModelsByAPIKeyScopesPrunesCapabilities(t *testing.T) {
|
|
models := []PlatformModel{{
|
|
ID: "model-1",
|
|
ModelType: StringList{"text_generate", "image_generate"},
|
|
Capabilities: map[string]any{
|
|
"text_generate": map[string]any{"max_context_tokens": 128000},
|
|
"image_generate": map[string]any{"aspect_ratio_allowed": []any{"1:1"}},
|
|
"originalTypes": []any{"text_generate", "image_generate"},
|
|
"shared": true,
|
|
},
|
|
}}
|
|
actual := filterPlatformModelsByAPIKeyScopes(models, []string{"image"})
|
|
if len(actual) != 1 || !reflect.DeepEqual(actual[0].ModelType, StringList{"image_generate"}) {
|
|
t.Fatalf("scope-filtered models = %+v", actual)
|
|
}
|
|
if _, exists := actual[0].Capabilities["text_generate"]; exists {
|
|
t.Fatalf("text capability leaked into image scope: %+v", actual[0].Capabilities)
|
|
}
|
|
if _, exists := actual[0].Capabilities["image_generate"]; !exists {
|
|
t.Fatalf("image capability was removed: %+v", actual[0].Capabilities)
|
|
}
|
|
if !reflect.DeepEqual(actual[0].Capabilities["originalTypes"], []string{"image_generate"}) {
|
|
t.Fatalf("originalTypes not pruned: %+v", actual[0].Capabilities["originalTypes"])
|
|
}
|
|
if actual[0].Capabilities["shared"] != true {
|
|
t.Fatalf("shared capability metadata was removed: %+v", actual[0].Capabilities)
|
|
}
|
|
}
|
|
|
|
func TestDiagnoseAPIKeyRulesExplainsEachInactiveLayer(t *testing.T) {
|
|
rules := []AccessRule{
|
|
{ID: "gone", SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "gone", Effect: "allow", Status: "active"},
|
|
{ID: "revoked", SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "revoked", Effect: "allow", Status: "active"},
|
|
{ID: "scope", SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "scope", Effect: "deny", Status: "active"},
|
|
{ID: "effective", SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "effective", Effect: "allow", Status: "active"},
|
|
}
|
|
all := []PlatformModel{{ID: "revoked"}, {ID: "scope"}, {ID: "effective"}}
|
|
enabled := append([]PlatformModel(nil), all...)
|
|
baseline := []PlatformModel{{ID: "scope"}, {ID: "effective"}}
|
|
scoped := []PlatformModel{{ID: "effective"}}
|
|
diagnostics := diagnoseAPIKeyRules("key-1", rules, all, enabled, baseline, scoped, nil)
|
|
want := map[string]string{
|
|
"gone": "resource_unavailable", "revoked": "owner_access_revoked", "scope": "scope_not_allowed", "effective": "",
|
|
}
|
|
for _, diagnostic := range diagnostics {
|
|
if diagnostic.Reason != want[diagnostic.RuleID] {
|
|
t.Fatalf("diagnostic %s reason = %q, want %q", diagnostic.RuleID, diagnostic.Reason, want[diagnostic.RuleID])
|
|
}
|
|
if diagnostic.Effective != (diagnostic.RuleID == "effective") {
|
|
t.Fatalf("diagnostic %s effective = %v", diagnostic.RuleID, diagnostic.Effective)
|
|
}
|
|
}
|
|
}
|