refactor(access): 统一分层白名单权限语义
取消跨主体专属占用,按租户、用户组、用户、当前 API Key 和 scope 分层求交,并在任务落库前统一校验候选。\n\n增加旧 allow 规则归档清理迁移、脱敏审计工具和回滚运行手册,补齐主体隔离、deny 优先及列表与运行时一致性测试。
This commit is contained in:
@@ -8,90 +8,136 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
models := []PlatformModel{{ID: "model-1", PlatformID: "platform-1"}, {ID: "model-2", PlatformID: "platform-1"}}
|
||||
user := &auth.User{GatewayUserID: "user-1", UserGroupID: "group-1", APIKeyID: "key-1"}
|
||||
rules := []AccessRule{
|
||||
{SubjectType: "user_group", SubjectID: "group-1", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow"},
|
||||
{SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "model-2", Effect: "allow"},
|
||||
}
|
||||
actual := filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, true), 0)
|
||||
if len(actual) != 0 {
|
||||
t.Fatalf("api key allow expanded denied baseline: %+v", actual)
|
||||
t.Fatalf("api key allow expanded its parent whitelist: %+v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIKeyAllowControlsOnlyMatchingKeys(t *testing.T) {
|
||||
model := PlatformModel{ID: "model-1", PlatformID: "platform-1"}
|
||||
func TestNoRulesInheritAllModels(t *testing.T) {
|
||||
models := []PlatformModel{{ID: "model-1"}, {ID: "model-2"}}
|
||||
user := &auth.User{GatewayUserID: "user-1", UserGroupID: "group-1", APIKeyID: "key-1"}
|
||||
actual := filterPlatformModelsByAccessLayers(models, nil, accessRuleLayers(user, true), 0)
|
||||
if !reflect.DeepEqual(actual, models) {
|
||||
t.Fatalf("models without rules = %+v, want %+v", actual, models)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIKeyAllowIsCurrentKeyWhitelistOnly(t *testing.T) {
|
||||
models := []PlatformModel{{ID: "model-1"}, {ID: "model-2"}}
|
||||
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)
|
||||
want []string
|
||||
}{{"key-a", []string{"model-1"}}, {"key-b", []string{"model-1", "model-2"}}} {
|
||||
user := &auth.User{GatewayUserID: "user-1", APIKeyID: test.keyID}
|
||||
actual := filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, true), 0)
|
||||
if got := platformModelIDs(actual); !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("key %s received %v, want %v", test.keyID, got, 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)
|
||||
func TestUserGroupAllowDoesNotAffectOtherGroups(t *testing.T) {
|
||||
models := []PlatformModel{{ID: "model-1"}, {ID: "model-2"}}
|
||||
rules := []AccessRule{{SubjectType: "user_group", SubjectID: "group-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow"}}
|
||||
for _, test := range []struct {
|
||||
groupID string
|
||||
want []string
|
||||
}{{"group-a", []string{"model-1"}}, {"group-b", []string{"model-1", "model-2"}}} {
|
||||
user := &auth.User{GatewayUserID: "user-1", UserGroupID: test.groupID}
|
||||
actual := filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, false), 0)
|
||||
if got := platformModelIDs(actual); !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("group %s received %v, want %v", test.groupID, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantGroupUserAndKeyWhitelistsIntersect(t *testing.T) {
|
||||
models := []PlatformModel{{ID: "a"}, {ID: "b"}, {ID: "c"}, {ID: "d"}}
|
||||
user := &auth.User{GatewayTenantID: "tenant-1", GatewayUserID: "user-1", UserGroupID: "group-1", APIKeyID: "key-1"}
|
||||
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"},
|
||||
{SubjectType: "tenant", SubjectID: "tenant-1", ResourceType: "platform_model", ResourceID: "a", Effect: "allow"},
|
||||
{SubjectType: "tenant", SubjectID: "tenant-1", ResourceType: "platform_model", ResourceID: "b", Effect: "allow"},
|
||||
{SubjectType: "user_group", SubjectID: "group-1", ResourceType: "platform_model", ResourceID: "b", Effect: "allow"},
|
||||
{SubjectType: "user_group", SubjectID: "group-1", ResourceType: "platform_model", ResourceID: "c", Effect: "allow"},
|
||||
{SubjectType: "user", SubjectID: "user-1", ResourceType: "platform_model", ResourceID: "b", Effect: "allow"},
|
||||
{SubjectType: "user", SubjectID: "user-1", ResourceType: "platform_model", ResourceID: "d", Effect: "allow"},
|
||||
{SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "b", Effect: "allow"},
|
||||
{SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "c", Effect: "allow"},
|
||||
}
|
||||
if got := filterPlatformModelsByLayeredRuleSet(keyUser, []PlatformModel{model}, nil, rules, keyUsers); len(got) != 0 {
|
||||
t.Fatalf("matching deny did not override allow: %+v", got)
|
||||
actual := filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, true), 0)
|
||||
if got := platformModelIDs(actual); !reflect.DeepEqual(got, []string{"b"}) {
|
||||
t.Fatalf("layered whitelist result = %v, want [b]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectUserIgnoresAPIKeyRules(t *testing.T) {
|
||||
model := PlatformModel{ID: "model-1", PlatformID: "platform-1", ModelType: StringList{"text_generate"}}
|
||||
func TestDenyWinsAndPlatformAllowIncludesChildren(t *testing.T) {
|
||||
models := []PlatformModel{
|
||||
{ID: "allowed", PlatformID: "platform-1"},
|
||||
{ID: "denied", PlatformID: "platform-1"},
|
||||
{ID: "other", PlatformID: "platform-2"},
|
||||
}
|
||||
user := &auth.User{GatewayUserID: "user-1", APIKeyID: "key-1"}
|
||||
rules := []AccessRule{
|
||||
{SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform", ResourceID: "platform-1", Effect: "allow"},
|
||||
{SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "denied", Effect: "deny"},
|
||||
}
|
||||
actual := filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, true), 0)
|
||||
if got := platformModelIDs(actual); !reflect.DeepEqual(got, []string{"allowed"}) {
|
||||
t.Fatalf("platform allow with child deny = %v, want [allowed]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameLayerAllowsUnionAndAnyDenyWins(t *testing.T) {
|
||||
models := []PlatformModel{{ID: "a"}, {ID: "b"}, {ID: "c"}}
|
||||
layers := []accessRuleLayer{{subjectType: "user_group", subjectIDs: map[string]bool{"group-a": true, "group-b": true}}}
|
||||
rules := []AccessRule{
|
||||
{SubjectType: "user_group", SubjectID: "group-a", ResourceType: "platform_model", ResourceID: "a", Effect: "allow"},
|
||||
{SubjectType: "user_group", SubjectID: "group-b", ResourceType: "platform_model", ResourceID: "b", Effect: "allow"},
|
||||
{SubjectType: "user_group", SubjectID: "group-a", ResourceType: "platform_model", ResourceID: "b", Effect: "deny"},
|
||||
}
|
||||
actual := filterPlatformModelsByAccessLayers(models, rules, layers, 0)
|
||||
if got := platformModelIDs(actual); !reflect.DeepEqual(got, []string{"a"}) {
|
||||
t.Fatalf("same-layer allow union and deny result = %v, want [a]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseModelRuleMatchesEverySource(t *testing.T) {
|
||||
models := []PlatformModel{
|
||||
{ID: "source-a", BaseModelID: "base-1"},
|
||||
{ID: "source-b", BaseModelID: "base-1"},
|
||||
{ID: "source-c", BaseModelID: "base-2"},
|
||||
}
|
||||
user := &auth.User{GatewayUserID: "user-1"}
|
||||
rules := []AccessRule{{
|
||||
SubjectType: "api_key", SubjectID: "key-a", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow",
|
||||
SubjectType: "user", SubjectID: "user-1", ResourceType: "base_model", ResourceID: "base-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)
|
||||
actual := filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, false), 0)
|
||||
if got := platformModelIDs(actual); !reflect.DeepEqual(got, []string{"source-a", "source-b"}) {
|
||||
t.Fatalf("base-model whitelist result = %v, want both base-1 sources", 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"}},
|
||||
func TestAllowPermissionLevelFailsClosed(t *testing.T) {
|
||||
models := []PlatformModel{{ID: "model-1"}, {ID: "model-2"}}
|
||||
rules := []AccessRule{{
|
||||
SubjectType: "api_key", SubjectID: "key-1", ResourceType: "platform_model", ResourceID: "model-1", Effect: "allow", MinPermissionLevel: 2,
|
||||
}}
|
||||
user := &auth.User{GatewayUserID: "user-1", APIKeyID: "key-1"}
|
||||
if got := filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, true), 1); len(got) != 0 {
|
||||
t.Fatalf("insufficient permission inherited models instead of failing closed: %+v", got)
|
||||
}
|
||||
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)
|
||||
if got := platformModelIDs(filterPlatformModelsByAccessLayers(models, rules, accessRuleLayers(user, true), 2)); !reflect.DeepEqual(got, []string{"model-1"}) {
|
||||
t.Fatalf("sufficient permission result = %v, want [model-1]", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,3 +194,11 @@ func TestDiagnoseAPIKeyRulesExplainsEachInactiveLayer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func platformModelIDs(models []PlatformModel) []string {
|
||||
ids := make([]string, 0, len(models))
|
||||
for _, model := range models {
|
||||
ids = append(ids, model.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user