refactor(access): 统一分层白名单权限语义

取消跨主体专属占用,按租户、用户组、用户、当前 API Key 和 scope 分层求交,并在任务落库前统一校验候选。\n\n增加旧 allow 规则归档清理迁移、脱敏审计工具和回滚运行手册,补齐主体隔离、deny 优先及列表与运行时一致性测试。
This commit is contained in:
2026-08-03 15:43:49 +08:00
parent c9393af43a
commit 7376d6fab6
20 changed files with 1490 additions and 513 deletions
+125 -161
View File
@@ -43,43 +43,25 @@ func (s *Store) enabledPlatformModels(ctx context.Context) ([]PlatformModel, []P
}
func (s *Store) filterPlatformModelsByLayeredAccess(ctx context.Context, user *auth.User, models []PlatformModel) ([]PlatformModel, error) {
rules, err := s.listActiveAccessRulesForResources(ctx, platformModelAccessResources(models))
layers := accessRuleLayers(user, true)
rules, err := s.listActiveAccessRulesForLayers(ctx, layers)
if err != nil {
return nil, err
}
baselineRules, apiKeyRules := splitLayeredAccessRules(rules)
baseline := filterPlatformModelsByRuleSet(models, baselineRules, baselineAccessRuleSubjects(user), permissionLevel(user))
if user == nil || strings.TrimSpace(user.APIKeyID) == "" {
return baseline, nil
filtered := filterPlatformModelsByAccessLayers(models, rules, layers, permissionLevel(user))
if user != nil && strings.TrimSpace(user.APIKeyID) != "" {
filtered = filterPlatformModelsByAPIKeyScopes(filtered, user.APIKeyScopes)
}
apiKeyUsers, err := s.apiKeyAccessRuleUsers(ctx, apiKeyRules)
if err != nil {
return nil, err
}
return filterPlatformModelsByLayeredRuleSet(user, baseline, baselineRules, apiKeyRules, apiKeyUsers), nil
}
func filterPlatformModelsByLayeredRuleSet(user *auth.User, baseline []PlatformModel, baselineRules []AccessRule, apiKeyRules []AccessRule, apiKeyUsers map[string]*auth.User) []PlatformModel {
if user == nil || strings.TrimSpace(user.APIKeyID) == "" {
return baseline
}
keyFiltered := make([]PlatformModel, 0, len(baseline))
for _, model := range baseline {
effectiveRules := effectiveAPIKeyRulesForPlatformModel(apiKeyRules, baselineRules, apiKeyUsers, model)
if platformModelAllowedByAccessRules(model, effectiveRules, apiKeyAccessRuleSubjects(user), permissionLevel(user)) {
keyFiltered = append(keyFiltered, model)
}
}
return filterPlatformModelsByAPIKeyScopes(keyFiltered, user.APIKeyScopes)
return filtered, nil
}
func (s *Store) filterPlatformModelsByBaselineAccess(ctx context.Context, user *auth.User, models []PlatformModel) ([]PlatformModel, error) {
rules, err := s.listActiveAccessRulesForResources(ctx, platformModelAccessResources(models))
layers := accessRuleLayers(user, false)
rules, err := s.listActiveAccessRulesForLayers(ctx, layers)
if err != nil {
return nil, err
}
baselineRules, _ := splitLayeredAccessRules(rules)
return filterPlatformModelsByRuleSet(models, baselineRules, baselineAccessRuleSubjects(user), permissionLevel(user)), nil
return filterPlatformModelsByAccessLayers(models, rules, layers, permissionLevel(user)), nil
}
func (s *Store) filterRuntimeCandidatesByLayeredAccess(ctx context.Context, user *auth.User, candidates []RuntimeModelCandidate) ([]RuntimeModelCandidate, error) {
@@ -90,31 +72,20 @@ func (s *Store) filterRuntimeCandidatesByLayeredAccess(ctx context.Context, user
if err != nil {
return nil, err
}
rules, err := s.listActiveAccessRulesForResources(ctx, candidateAccessResources(candidates))
layers := accessRuleLayers(accessUser, true)
rules, err := s.listActiveAccessRulesForLayers(ctx, layers)
if err != nil {
return nil, err
}
baselineRules, apiKeyRules := splitLayeredAccessRules(rules)
baseline := filterCandidatesByRuleSet(candidates, baselineRules, baselineAccessRuleSubjects(accessUser), permissionLevel(accessUser))
if accessUser == nil || strings.TrimSpace(accessUser.APIKeyID) == "" {
return baseline, nil
}
apiKeyUsers, err := s.apiKeyAccessRuleUsers(ctx, apiKeyRules)
if err != nil {
return nil, err
}
keyFiltered := make([]RuntimeModelCandidate, 0, len(baseline))
for _, candidate := range baseline {
effectiveRules := effectiveAPIKeyRulesForCandidate(apiKeyRules, baselineRules, apiKeyUsers, candidate)
if candidateAllowedByAccessRules(candidate, effectiveRules, apiKeyAccessRuleSubjects(accessUser), permissionLevel(accessUser)) {
keyFiltered = append(keyFiltered, candidate)
}
}
filtered := make([]RuntimeModelCandidate, 0, len(keyFiltered))
for _, candidate := range keyFiltered {
if modelaccess.ScopeAllowsModelType(accessUser.APIKeyScopes, candidate.ModelType) {
filtered = append(filtered, candidate)
filtered := filterCandidatesByAccessLayers(candidates, rules, layers, permissionLevel(accessUser))
if accessUser != nil && strings.TrimSpace(accessUser.APIKeyID) != "" {
scoped := make([]RuntimeModelCandidate, 0, len(filtered))
for _, candidate := range filtered {
if modelaccess.ScopeAllowsModelType(accessUser.APIKeyScopes, candidate.ModelType) {
scoped = append(scoped, candidate)
}
}
filtered = scoped
}
return filtered, nil
}
@@ -135,12 +106,10 @@ func (s *Store) ListAPIKeyAssignablePlatformModelsForKey(ctx context.Context, us
if err != nil {
return nil, nil, err
}
rules, err := s.listActiveAccessRulesForResources(ctx, platformModelAccessResources(enabledModels))
baseline, err := s.filterPlatformModelsByBaselineAccess(ctx, accessUser, enabledModels)
if err != nil {
return nil, nil, err
}
baselineRules, _ := splitLayeredAccessRules(rules)
baseline := filterPlatformModelsByRuleSet(enabledModels, baselineRules, baselineAccessRuleSubjects(accessUser), permissionLevel(accessUser))
scoped := filterPlatformModelsByAPIKeyScopes(baseline, accessUser.APIKeyScopes)
ownedRules, err := s.ListAPIKeyAccessRules(ctx, user)
if err != nil {
@@ -185,129 +154,140 @@ WHERE k.id = $1::uuid
return &next, nil
}
func effectiveAPIKeyRulesForPlatformModel(rules []AccessRule, baselineRules []AccessRule, users map[string]*auth.User, model PlatformModel) []AccessRule {
effective := make([]AccessRule, 0, len(rules))
for _, rule := range rules {
accessUser := users[rule.SubjectID]
if accessUser == nil || !accessRuleMatchesPlatformModel(rule, model) ||
!platformModelAllowedByAccessRules(model, baselineRules, baselineAccessRuleSubjects(accessUser), permissionLevel(accessUser)) ||
len(modelaccess.FilterModelTypes(accessUser.APIKeyScopes, model.ModelType)) == 0 {
continue
}
effective = append(effective, rule)
}
return effective
type accessRuleLayer struct {
subjectType string
subjectIDs map[string]bool
}
func effectiveAPIKeyRulesForCandidate(rules []AccessRule, baselineRules []AccessRule, users map[string]*auth.User, candidate RuntimeModelCandidate) []AccessRule {
effective := make([]AccessRule, 0, len(rules))
for _, rule := range rules {
accessUser := users[rule.SubjectID]
if accessUser == nil || !accessRuleMatchesCandidate(rule, candidate) ||
!candidateAllowedByAccessRules(candidate, baselineRules, baselineAccessRuleSubjects(accessUser), permissionLevel(accessUser)) ||
!modelaccess.ScopeAllowsModelType(accessUser.APIKeyScopes, candidate.ModelType) {
var accessRuleLayerOrder = []string{"tenant", "user_group", "user", "api_key"}
func accessRuleLayers(user *auth.User, includeAPIKey bool) []accessRuleLayer {
subjects := accessRuleSubjects(user)
layers := make([]accessRuleLayer, 0, len(accessRuleLayerOrder))
for _, subjectType := range accessRuleLayerOrder {
if subjectType == "api_key" && !includeAPIKey {
continue
}
effective = append(effective, rule)
prefix := subjectType + ":"
ids := map[string]bool{}
for subject := range subjects {
if strings.HasPrefix(subject, prefix) {
ids[strings.TrimPrefix(subject, prefix)] = true
}
}
if len(ids) > 0 {
layers = append(layers, accessRuleLayer{subjectType: subjectType, subjectIDs: ids})
}
}
return effective
return layers
}
func (s *Store) apiKeyAccessRuleUsers(ctx context.Context, rules []AccessRule) (map[string]*auth.User, error) {
ids := make([]string, 0, len(rules))
seen := map[string]bool{}
for _, rule := range rules {
if rule.SubjectType != "api_key" || rule.SubjectID == "" || seen[rule.SubjectID] {
continue
func (s *Store) listActiveAccessRulesForLayers(ctx context.Context, layers []accessRuleLayer) ([]AccessRule, error) {
subjects := make([]string, 0)
for _, layer := range layers {
for id := range layer.subjectIDs {
subjects = append(subjects, layer.subjectType+":"+id)
}
seen[rule.SubjectID] = true
ids = append(ids, rule.SubjectID)
}
if len(ids) == 0 {
return map[string]*auth.User{}, nil
if len(subjects) == 0 {
return nil, nil
}
rows, err := s.pool.Query(ctx, `
SELECT k.id::text, k.scopes, u.id::text,
COALESCE(u.gateway_tenant_id::text, ''), COALESCE(u.tenant_id, ''), COALESCE(u.tenant_key, ''),
u.roles, COALESCE(k.user_group_id::text, u.default_user_group_id::text, ''), COALESCE(g.group_key, '')
FROM gateway_api_keys k
JOIN gateway_users u ON u.id = k.gateway_user_id
LEFT JOIN gateway_user_groups g ON g.id = COALESCE(k.user_group_id, u.default_user_group_id)
WHERE k.id = ANY($1::uuid[])
AND k.status = 'active'
AND k.deleted_at IS NULL
AND (k.expires_at IS NULL OR k.expires_at > now())
AND u.status = 'active'
AND u.deleted_at IS NULL`, ids)
SELECT `+accessRuleColumns+`
FROM gateway_access_rules
WHERE status = 'active'
AND (subject_type || ':' || subject_id::text) = ANY($1)
ORDER BY subject_type ASC, priority ASC, created_at ASC`, subjects)
if err != nil {
return nil, err
}
defer rows.Close()
users := map[string]*auth.User{}
rules := make([]AccessRule, 0)
for rows.Next() {
var apiKeyID string
var scopesBytes []byte
var rolesBytes []byte
var gatewayUserID string
var gatewayTenantID string
var tenantID string
var tenantKey string
var userGroupID string
var userGroupKey string
if err := rows.Scan(&apiKeyID, &scopesBytes, &gatewayUserID, &gatewayTenantID, &tenantID, &tenantKey, &rolesBytes, &userGroupID, &userGroupKey); err != nil {
item, err := scanAccessRule(rows)
if err != nil {
return nil, err
}
groupKeys := []string(nil)
if userGroupKey != "" {
groupKeys = []string{userGroupKey}
}
users[apiKeyID] = &auth.User{
GatewayUserID: gatewayUserID,
GatewayTenantID: gatewayTenantID,
TenantID: tenantID,
TenantKey: tenantKey,
Roles: decodeStringArray(rolesBytes),
UserGroupID: userGroupID,
UserGroupKey: userGroupKey,
UserGroupKeys: groupKeys,
APIKeyID: apiKeyID,
APIKeyScopes: decodeStringArray(scopesBytes),
}
rules = append(rules, item)
}
return users, rows.Err()
return rules, rows.Err()
}
func splitLayeredAccessRules(rules []AccessRule) ([]AccessRule, []AccessRule) {
baseline := make([]AccessRule, 0, len(rules))
apiKeys := make([]AccessRule, 0, len(rules))
func filterPlatformModelsByAccessLayers(models []PlatformModel, rules []AccessRule, layers []accessRuleLayer, level int) []PlatformModel {
filtered := append([]PlatformModel(nil), models...)
for _, layer := range layers {
layerRules := accessRulesForLayer(rules, layer)
if len(layerRules) == 0 {
continue
}
next := make([]PlatformModel, 0, len(filtered))
for _, model := range filtered {
if platformModelAllowedBySubjectLayer(model, layerRules, level) {
next = append(next, model)
}
}
filtered = next
}
return filtered
}
func filterCandidatesByAccessLayers(candidates []RuntimeModelCandidate, rules []AccessRule, layers []accessRuleLayer, level int) []RuntimeModelCandidate {
filtered := append([]RuntimeModelCandidate(nil), candidates...)
for _, layer := range layers {
layerRules := accessRulesForLayer(rules, layer)
if len(layerRules) == 0 {
continue
}
next := make([]RuntimeModelCandidate, 0, len(filtered))
for _, candidate := range filtered {
if candidateAllowedBySubjectLayer(candidate, layerRules, level) {
next = append(next, candidate)
}
}
filtered = next
}
return filtered
}
func accessRulesForLayer(rules []AccessRule, layer accessRuleLayer) []AccessRule {
filtered := make([]AccessRule, 0)
for _, rule := range rules {
if rule.SubjectType == "api_key" {
apiKeys = append(apiKeys, rule)
} else {
baseline = append(baseline, rule)
}
}
return baseline, apiKeys
}
func filterPlatformModelsByRuleSet(models []PlatformModel, rules []AccessRule, subjects map[string]bool, level int) []PlatformModel {
filtered := make([]PlatformModel, 0, len(models))
for _, model := range models {
if platformModelAllowedByAccessRules(model, rules, subjects, level) {
filtered = append(filtered, model)
if rule.SubjectType == layer.subjectType && layer.subjectIDs[rule.SubjectID] {
filtered = append(filtered, rule)
}
}
return filtered
}
func filterCandidatesByRuleSet(candidates []RuntimeModelCandidate, rules []AccessRule, subjects map[string]bool, level int) []RuntimeModelCandidate {
filtered := make([]RuntimeModelCandidate, 0, len(candidates))
for _, candidate := range candidates {
if candidateAllowedByAccessRules(candidate, rules, subjects, level) {
filtered = append(filtered, candidate)
func platformModelAllowedBySubjectLayer(model PlatformModel, rules []AccessRule, level int) bool {
return allowedBySubjectLayer(rules, level, func(rule AccessRule) bool {
return accessRuleMatchesPlatformModel(rule, model)
})
}
func candidateAllowedBySubjectLayer(candidate RuntimeModelCandidate, rules []AccessRule, level int) bool {
return allowedBySubjectLayer(rules, level, func(rule AccessRule) bool {
return accessRuleMatchesCandidate(rule, candidate)
})
}
func allowedBySubjectLayer(rules []AccessRule, level int, matches func(AccessRule) bool) bool {
hasAllow := false
matchedAllow := false
for _, rule := range rules {
switch rule.Effect {
case "allow":
hasAllow = true
if level >= rule.MinPermissionLevel && matches(rule) {
matchedAllow = true
}
case "deny":
if matches(rule) {
return false
}
}
}
return filtered
return !hasAllow || matchedAllow
}
func filterPlatformModelsByAPIKeyScopes(models []PlatformModel, scopes []string) []PlatformModel {
@@ -393,22 +373,6 @@ func stringValues(value any) []string {
}
}
func baselineAccessRuleSubjects(user *auth.User) map[string]bool {
subjects := accessRuleSubjects(user)
if user != nil && user.APIKeyID != "" {
delete(subjects, "api_key:"+user.APIKeyID)
}
return subjects
}
func apiKeyAccessRuleSubjects(user *auth.User) map[string]bool {
subjects := map[string]bool{}
if user != nil && strings.TrimSpace(user.APIKeyID) != "" {
subjects["api_key:"+strings.TrimSpace(user.APIKeyID)] = true
}
return subjects
}
func permissionLevel(user *auth.User) int {
if user == nil {
return 0