refactor(access): 统一分层白名单权限语义
取消跨主体专属占用,按租户、用户组、用户、当前 API Key 和 scope 分层求交,并在任务落库前统一校验候选。\n\n增加旧 allow 规则归档清理迁移、脱敏审计工具和回滚运行手册,补齐主体隔离、deny 优先及列表与运行时一致性测试。
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user