fix(access): 统一 API Key 模型权限与列表契约
将全局启用、用户组基线、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 真实配置验收通过。
This commit is contained in:
@@ -244,7 +244,7 @@ SELECT EXISTS (
|
||||
if !exists {
|
||||
return nil, pgx.ErrNoRows
|
||||
}
|
||||
if err := s.ensureAPIKeyAccessRuleResourcesAllowed(ctx, user, input.UpsertResources); err != nil {
|
||||
if err := s.ensureAPIKeyAccessRuleResourcesAllowed(ctx, user, input.SubjectID, input.UpsertResources); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.BatchAccessRules(ctx, input); err != nil {
|
||||
@@ -254,32 +254,7 @@ SELECT EXISTS (
|
||||
}
|
||||
|
||||
func (s *Store) filterCandidatesByAccessRules(ctx context.Context, user *auth.User, candidates []RuntimeModelCandidate) ([]RuntimeModelCandidate, error) {
|
||||
if len(candidates) == 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
resources := candidateAccessResources(candidates)
|
||||
if len(resources) == 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
rules, err := s.listActiveAccessRulesForResources(ctx, resources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
subjects := accessRuleSubjects(user)
|
||||
level := 0
|
||||
if user != nil {
|
||||
level = auth.PermissionLevel(user.Roles)
|
||||
}
|
||||
filtered := candidates[:0]
|
||||
for _, candidate := range candidates {
|
||||
if candidateAllowedByAccessRules(candidate, rules, subjects, level) {
|
||||
filtered = append(filtered, candidate)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
return s.filterRuntimeCandidatesByLayeredAccess(ctx, user, candidates)
|
||||
}
|
||||
|
||||
func (s *Store) ListAccessiblePlatformModels(ctx context.Context, user *auth.User) ([]PlatformModel, error) {
|
||||
@@ -302,35 +277,22 @@ func (s *Store) listPlatformModelsForAccessRules(ctx context.Context, user *auth
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models, err := s.ListModels(ctx)
|
||||
models, _, err := s.enabledPlatformModels(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
platforms, err := s.ListPlatforms(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if excludedSubjectTypes["api_key"] {
|
||||
return s.filterPlatformModelsByBaselineAccess(ctx, accessUser, models)
|
||||
}
|
||||
enabledPlatforms := map[string]bool{}
|
||||
for _, platform := range platforms {
|
||||
if platform.Status == "enabled" {
|
||||
enabledPlatforms[platform.ID] = true
|
||||
}
|
||||
}
|
||||
enabled := make([]PlatformModel, 0, len(models))
|
||||
for _, model := range models {
|
||||
if model.Enabled && enabledPlatforms[model.PlatformID] {
|
||||
enabled = append(enabled, model)
|
||||
}
|
||||
}
|
||||
return s.filterPlatformModelsByAccessRules(ctx, accessUser, enabled, excludedSubjectTypes)
|
||||
return s.filterPlatformModelsByLayeredAccess(ctx, accessUser, models)
|
||||
}
|
||||
|
||||
func (s *Store) ensureAPIKeyAccessRuleResourcesAllowed(ctx context.Context, user *auth.User, resources []AccessRuleResourceInput) error {
|
||||
func (s *Store) ensureAPIKeyAccessRuleResourcesAllowed(ctx context.Context, user *auth.User, apiKeyID string, resources []AccessRuleResourceInput) error {
|
||||
resources = dedupeAccessRuleResources(resources)
|
||||
if len(resources) == 0 {
|
||||
return nil
|
||||
}
|
||||
allowed, err := s.accessibleAccessRuleResources(ctx, user)
|
||||
allowed, err := s.accessibleAccessRuleResources(ctx, user, apiKeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -342,8 +304,8 @@ func (s *Store) ensureAPIKeyAccessRuleResourcesAllowed(ctx context.Context, user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) accessibleAccessRuleResources(ctx context.Context, user *auth.User) (map[string]bool, error) {
|
||||
models, err := s.ListAPIKeyAssignablePlatformModels(ctx, user)
|
||||
func (s *Store) accessibleAccessRuleResources(ctx context.Context, user *auth.User, apiKeyID string) (map[string]bool, error) {
|
||||
models, _, err := s.ListAPIKeyAssignablePlatformModelsForKey(ctx, user, apiKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -368,25 +330,28 @@ func (s *Store) resolveCurrentAccessUser(ctx context.Context, user *auth.User) (
|
||||
}
|
||||
next := *user
|
||||
var userGroupID string
|
||||
var userGroupKey string
|
||||
var err error
|
||||
if strings.TrimSpace(user.APIKeyID) != "" {
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(k.user_group_id::text, u.default_user_group_id::text, '')
|
||||
SELECT COALESCE(k.user_group_id::text, u.default_user_group_id::text, ''), COALESCE(g.group_key, '')
|
||||
FROM gateway_users u
|
||||
JOIN gateway_api_keys k ON k.gateway_user_id = u.id
|
||||
LEFT JOIN gateway_user_groups g ON g.id = COALESCE(k.user_group_id, u.default_user_group_id)
|
||||
WHERE u.id = $1::uuid
|
||||
AND k.id = $2::uuid
|
||||
AND u.status = 'active'
|
||||
AND u.deleted_at IS NULL
|
||||
AND k.status = 'active'
|
||||
AND k.deleted_at IS NULL`, gatewayUserID, user.APIKeyID).Scan(&userGroupID)
|
||||
AND k.deleted_at IS NULL`, gatewayUserID, user.APIKeyID).Scan(&userGroupID, &userGroupKey)
|
||||
} else {
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(default_user_group_id::text, '')
|
||||
FROM gateway_users
|
||||
WHERE id = $1::uuid
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL`, gatewayUserID).Scan(&userGroupID)
|
||||
SELECT COALESCE(u.default_user_group_id::text, ''), COALESCE(g.group_key, '')
|
||||
FROM gateway_users u
|
||||
LEFT JOIN gateway_user_groups g ON g.id = u.default_user_group_id
|
||||
WHERE u.id = $1::uuid
|
||||
AND u.status = 'active'
|
||||
AND u.deleted_at IS NULL`, gatewayUserID).Scan(&userGroupID, &userGroupKey)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -395,6 +360,11 @@ WHERE id = $1::uuid
|
||||
return nil, err
|
||||
}
|
||||
next.UserGroupID = userGroupID
|
||||
next.UserGroupKey = userGroupKey
|
||||
next.UserGroupKeys = nil
|
||||
if userGroupKey != "" {
|
||||
next.UserGroupKeys = []string{userGroupKey}
|
||||
}
|
||||
return &next, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user