From 0040d784964a0f71f883b2e176b3e753ba755532 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Tue, 10 Feb 2026 15:38:03 +0800 Subject: [PATCH] refactor(sdk): simplify provider lifecycle and registration logic --- cmd/server/main.go | 2 +- docs/sdk-access.md | 128 +++++------ docs/sdk-access_CN.md | 124 +++++----- internal/access/config_access/provider.go | 77 +++++-- internal/access/reconcile.go | 211 +++--------------- .../api/handlers/management/config_lists.go | 5 +- internal/api/server.go | 10 +- internal/config/config.go | 27 +-- internal/config/sdk_config.go | 65 ------ sdk/access/errors.go | 96 +++++++- sdk/access/manager.go | 21 +- sdk/access/registry.go | 94 ++++---- sdk/access/types.go | 47 ++++ sdk/cliproxy/builder.go | 8 +- sdk/config/config.go | 10 +- 15 files changed, 391 insertions(+), 534 deletions(-) create mode 100644 sdk/access/types.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 5bf4ba6a..dec30484 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -445,7 +445,7 @@ func main() { } // Register built-in access providers before constructing services. - configaccess.Register() + configaccess.Register(&cfg.SDKConfig) // Handle different command modes based on the provided flags. diff --git a/docs/sdk-access.md b/docs/sdk-access.md index e4e69629..343c851b 100644 --- a/docs/sdk-access.md +++ b/docs/sdk-access.md @@ -7,80 +7,71 @@ The `github.com/router-for-me/CLIProxyAPI/v6/sdk/access` package centralizes inb ```go import ( sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" ) ``` Add the module with `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access`. +## Provider Registry + +Providers are registered globally and then attached to a `Manager` as a snapshot: + +- `RegisterProvider(type, provider)` installs a pre-initialized provider instance. +- Registration order is preserved the first time each `type` is seen. +- `RegisteredProviders()` returns the providers in that order. + ## Manager Lifecycle ```go manager := sdkaccess.NewManager() -providers, err := sdkaccess.BuildProviders(cfg) -if err != nil { - return err -} -manager.SetProviders(providers) +manager.SetProviders(sdkaccess.RegisteredProviders()) ``` * `NewManager` constructs an empty manager. * `SetProviders` replaces the provider slice using a defensive copy. * `Providers` retrieves a snapshot that can be iterated safely from other goroutines. -* `BuildProviders` translates `config.Config` access declarations into runnable providers. When the config omits explicit providers but defines inline API keys, the helper auto-installs the built-in `config-api-key` provider. + +If the manager itself is `nil` or no providers are configured, the call returns `nil, nil`, allowing callers to treat access control as disabled. ## Authenticating Requests ```go -result, err := manager.Authenticate(ctx, req) +result, authErr := manager.Authenticate(ctx, req) switch { -case err == nil: +case authErr == nil: // Authentication succeeded; result describes the provider and principal. -case errors.Is(err, sdkaccess.ErrNoCredentials): +case sdkaccess.IsAuthErrorCode(authErr, sdkaccess.AuthErrorCodeNoCredentials): // No recognizable credentials were supplied. -case errors.Is(err, sdkaccess.ErrInvalidCredential): +case sdkaccess.IsAuthErrorCode(authErr, sdkaccess.AuthErrorCodeInvalidCredential): // Supplied credentials were present but rejected. default: - // Transport-level failure was returned by a provider. + // Internal/transport failure was returned by a provider. } ``` -`Manager.Authenticate` walks the configured providers in order. It returns on the first success, skips providers that surface `ErrNotHandled`, and tracks whether any provider reported `ErrNoCredentials` or `ErrInvalidCredential` for downstream error reporting. - -If the manager itself is `nil` or no providers are registered, the call returns `nil, nil`, allowing callers to treat access control as disabled without branching on errors. +`Manager.Authenticate` walks the configured providers in order. It returns on the first success, skips providers that return `AuthErrorCodeNotHandled`, and aggregates `AuthErrorCodeNoCredentials` / `AuthErrorCodeInvalidCredential` for a final result. Each `Result` includes the provider identifier, the resolved principal, and optional metadata (for example, which header carried the credential). -## Configuration Layout +## Built-in `config-api-key` Provider -The manager expects access providers under the `auth.providers` key inside `config.yaml`: +The proxy includes one built-in access provider: + +- `config-api-key`: Validates API keys declared under top-level `api-keys`. + - Credential sources: `Authorization: Bearer`, `X-Goog-Api-Key`, `X-Api-Key`, `?key=`, `?auth_token=` + - Metadata: `Result.Metadata["source"]` is set to the matched source label. + +In the CLI server and `sdk/cliproxy`, this provider is registered automatically based on the loaded configuration. ```yaml -auth: - providers: - - name: inline-api - type: config-api-key - api-keys: - - sk-test-123 - - sk-prod-456 +api-keys: + - sk-test-123 + - sk-prod-456 ``` -Fields map directly to `config.AccessProvider`: `name` labels the provider, `type` selects the registered factory, `sdk` can name an external module, `api-keys` seeds inline credentials, and `config` passes provider-specific options. +## Loading Providers from External Go Modules -### Loading providers from external SDK modules - -To consume a provider shipped in another Go module, point the `sdk` field at the module path and import it for its registration side effect: - -```yaml -auth: - providers: - - name: partner-auth - type: partner-token - sdk: github.com/acme/xplatform/sdk/access/providers/partner - config: - region: us-west-2 - audience: cli-proxy -``` +To consume a provider shipped in another Go module, import it for its registration side effect: ```go import ( @@ -89,19 +80,11 @@ import ( ) ``` -The blank identifier import ensures `init` runs so `sdkaccess.RegisterProvider` executes before `BuildProviders` is called. - -## Built-in Providers - -The SDK ships with one provider out of the box: - -- `config-api-key`: Validates API keys declared inline or under top-level `api-keys`. It accepts the key from `Authorization: Bearer`, `X-Goog-Api-Key`, `X-Api-Key`, or the `?key=` query string and reports `ErrInvalidCredential` when no match is found. - -Additional providers can be delivered by third-party packages. When a provider package is imported, it registers itself with `sdkaccess.RegisterProvider`. +The blank identifier import ensures `init` runs so `sdkaccess.RegisterProvider` executes before you call `RegisteredProviders()` (or before `cliproxy.NewBuilder().Build()`). ### Metadata and auditing -`Result.Metadata` carries provider-specific context. The built-in `config-api-key` provider, for example, stores the credential source (`authorization`, `x-goog-api-key`, `x-api-key`, or `query-key`). Populate this map in custom providers to enrich logs and downstream auditing. +`Result.Metadata` carries provider-specific context. The built-in `config-api-key` provider, for example, stores the credential source (`authorization`, `x-goog-api-key`, `x-api-key`, `query-key`, `query-auth-token`). Populate this map in custom providers to enrich logs and downstream auditing. ## Writing Custom Providers @@ -110,13 +93,13 @@ type customProvider struct{} func (p *customProvider) Identifier() string { return "my-provider" } -func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) { +func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, *sdkaccess.AuthError) { token := r.Header.Get("X-Custom") if token == "" { - return nil, sdkaccess.ErrNoCredentials + return nil, sdkaccess.NewNotHandledError() } if token != "expected" { - return nil, sdkaccess.ErrInvalidCredential + return nil, sdkaccess.NewInvalidCredentialError() } return &sdkaccess.Result{ Provider: p.Identifier(), @@ -126,51 +109,46 @@ func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sd } func init() { - sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) { - return &customProvider{}, nil - }) + sdkaccess.RegisterProvider("custom", &customProvider{}) } ``` -A provider must implement `Identifier()` and `Authenticate()`. To expose it to configuration, call `RegisterProvider` inside `init`. Provider factories receive the specific `AccessProvider` block plus the full root configuration for contextual needs. +A provider must implement `Identifier()` and `Authenticate()`. To make it available to the access manager, call `RegisterProvider` inside `init` with an initialized provider instance. ## Error Semantics -- `ErrNoCredentials`: no credentials were present or recognized by any provider. -- `ErrInvalidCredential`: at least one provider processed the credentials but rejected them. -- `ErrNotHandled`: instructs the manager to fall through to the next provider without affecting aggregate error reporting. +- `NewNoCredentialsError()` (`AuthErrorCodeNoCredentials`): no credentials were present or recognized. (HTTP 401) +- `NewInvalidCredentialError()` (`AuthErrorCodeInvalidCredential`): credentials were present but rejected. (HTTP 401) +- `NewNotHandledError()` (`AuthErrorCodeNotHandled`): fall through to the next provider. +- `NewInternalAuthError(message, cause)` (`AuthErrorCodeInternal`): transport/system failure. (HTTP 500) -Return custom errors to surface transport failures; they propagate immediately to the caller instead of being masked. +Errors propagate immediately to the caller unless they are classified as `not_handled` / `no_credentials` / `invalid_credential` and can be aggregated by the manager. ## Integration with cliproxy Service -`sdk/cliproxy` wires `@sdk/access` automatically when you build a CLI service via `cliproxy.NewBuilder`. Supplying a preconfigured manager allows you to extend or override the default providers: +`sdk/cliproxy` wires `@sdk/access` automatically when you build a CLI service via `cliproxy.NewBuilder`. Supplying a manager lets you reuse the same instance in your host process: ```go coreCfg, _ := config.LoadConfig("config.yaml") -providers, _ := sdkaccess.BuildProviders(coreCfg) -manager := sdkaccess.NewManager() -manager.SetProviders(providers) +accessManager := sdkaccess.NewManager() svc, _ := cliproxy.NewBuilder(). WithConfig(coreCfg). - WithAccessManager(manager). + WithConfigPath("config.yaml"). + WithRequestAccessManager(accessManager). Build() ``` -The service reuses the manager for every inbound request, ensuring consistent authentication across embedded deployments and the canonical CLI binary. +Register any custom providers (typically via blank imports) before calling `Build()` so they are present in the global registry snapshot. -### Hot reloading providers +### Hot reloading -When configuration changes, rebuild providers and swap them into the manager: +When configuration changes, refresh any config-backed providers and then reset the manager's provider chain: ```go -providers, err := sdkaccess.BuildProviders(newCfg) -if err != nil { - log.Errorf("reload auth providers failed: %v", err) - return -} -accessManager.SetProviders(providers) +// configaccess is github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access +configaccess.Register(&newCfg.SDKConfig) +accessManager.SetProviders(sdkaccess.RegisteredProviders()) ``` -This mirrors the behaviour in `cliproxy.Service.refreshAccessProviders` and `api.Server.applyAccessConfig`, enabling runtime updates without restarting the process. +This mirrors the behaviour in `internal/access.ApplyAccessProviders`, enabling runtime updates without restarting the process. diff --git a/docs/sdk-access_CN.md b/docs/sdk-access_CN.md index b3f26497..38aafe11 100644 --- a/docs/sdk-access_CN.md +++ b/docs/sdk-access_CN.md @@ -7,80 +7,71 @@ ```go import ( sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" ) ``` 通过 `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 添加依赖。 +## Provider Registry + +访问提供者是全局注册,然后以快照形式挂到 `Manager` 上: + +- `RegisterProvider(type, provider)` 注册一个已经初始化好的 provider 实例。 +- 每个 `type` 第一次出现时会记录其注册顺序。 +- `RegisteredProviders()` 会按该顺序返回 provider 列表。 + ## 管理器生命周期 ```go manager := sdkaccess.NewManager() -providers, err := sdkaccess.BuildProviders(cfg) -if err != nil { - return err -} -manager.SetProviders(providers) +manager.SetProviders(sdkaccess.RegisteredProviders()) ``` - `NewManager` 创建空管理器。 - `SetProviders` 替换提供者切片并做防御性拷贝。 - `Providers` 返回适合并发读取的快照。 -- `BuildProviders` 将 `config.Config` 中的访问配置转换成可运行的提供者。当配置没有显式声明但包含顶层 `api-keys` 时,会自动挂载内建的 `config-api-key` 提供者。 + +如果管理器本身为 `nil` 或未配置任何 provider,调用会返回 `nil, nil`,可视为关闭访问控制。 ## 认证请求 ```go -result, err := manager.Authenticate(ctx, req) +result, authErr := manager.Authenticate(ctx, req) switch { -case err == nil: +case authErr == nil: // Authentication succeeded; result carries provider and principal. -case errors.Is(err, sdkaccess.ErrNoCredentials): +case sdkaccess.IsAuthErrorCode(authErr, sdkaccess.AuthErrorCodeNoCredentials): // No recognizable credentials were supplied. -case errors.Is(err, sdkaccess.ErrInvalidCredential): +case sdkaccess.IsAuthErrorCode(authErr, sdkaccess.AuthErrorCodeInvalidCredential): // Credentials were present but rejected. default: // Provider surfaced a transport-level failure. } ``` -`Manager.Authenticate` 按配置顺序遍历提供者。遇到成功立即返回,`ErrNotHandled` 会继续尝试下一个;若发现 `ErrNoCredentials` 或 `ErrInvalidCredential`,会在遍历结束后汇总给调用方。 - -若管理器本身为 `nil` 或尚未注册提供者,调用会返回 `nil, nil`,让调用方无需针对错误做额外分支即可关闭访问控制。 +`Manager.Authenticate` 会按顺序遍历 provider:遇到成功立即返回,`AuthErrorCodeNotHandled` 会继续尝试下一个;`AuthErrorCodeNoCredentials` / `AuthErrorCodeInvalidCredential` 会在遍历结束后汇总给调用方。 `Result` 提供认证提供者标识、解析出的主体以及可选元数据(例如凭证来源)。 -## 配置结构 +## 内建 `config-api-key` Provider -在 `config.yaml` 的 `auth.providers` 下定义访问提供者: +代理内置一个访问提供者: + +- `config-api-key`:校验 `config.yaml` 顶层的 `api-keys`。 + - 凭证来源:`Authorization: Bearer`、`X-Goog-Api-Key`、`X-Api-Key`、`?key=`、`?auth_token=` + - 元数据:`Result.Metadata["source"]` 会写入匹配到的来源标识 + +在 CLI 服务端与 `sdk/cliproxy` 中,该 provider 会根据加载到的配置自动注册。 ```yaml -auth: - providers: - - name: inline-api - type: config-api-key - api-keys: - - sk-test-123 - - sk-prod-456 +api-keys: + - sk-test-123 + - sk-prod-456 ``` -条目映射到 `config.AccessProvider`:`name` 指定实例名,`type` 选择注册的工厂,`sdk` 可引用第三方模块,`api-keys` 提供内联凭证,`config` 用于传递特定选项。 +## 引入外部 Go 模块提供者 -### 引入外部 SDK 提供者 - -若要消费其它 Go 模块输出的访问提供者,可在配置里填写 `sdk` 字段并在代码中引入该包,利用其 `init` 注册过程: - -```yaml -auth: - providers: - - name: partner-auth - type: partner-token - sdk: github.com/acme/xplatform/sdk/access/providers/partner - config: - region: us-west-2 - audience: cli-proxy -``` +若要消费其它 Go 模块输出的访问提供者,直接用空白标识符导入以触发其 `init` 注册即可: ```go import ( @@ -89,19 +80,11 @@ import ( ) ``` -通过空白标识符导入即可确保 `init` 调用,先于 `BuildProviders` 完成 `sdkaccess.RegisterProvider`。 - -## 内建提供者 - -当前 SDK 默认内置: - -- `config-api-key`:校验配置中的 API Key。它从 `Authorization: Bearer`、`X-Goog-Api-Key`、`X-Api-Key` 以及查询参数 `?key=` 提取凭证,不匹配时抛出 `ErrInvalidCredential`。 - -导入第三方包即可通过 `sdkaccess.RegisterProvider` 注册更多类型。 +空白导入可确保 `init` 先执行,从而在你调用 `RegisteredProviders()`(或 `cliproxy.NewBuilder().Build()`)之前完成 `sdkaccess.RegisterProvider`。 ### 元数据与审计 -`Result.Metadata` 用于携带提供者特定的上下文信息。内建的 `config-api-key` 会记录凭证来源(`authorization`、`x-goog-api-key`、`x-api-key` 或 `query-key`)。自定义提供者同样可以填充该 Map,以便丰富日志与审计场景。 +`Result.Metadata` 用于携带提供者特定的上下文信息。内建的 `config-api-key` 会记录凭证来源(`authorization`、`x-goog-api-key`、`x-api-key`、`query-key`、`query-auth-token`)。自定义提供者同样可以填充该 Map,以便丰富日志与审计场景。 ## 编写自定义提供者 @@ -110,13 +93,13 @@ type customProvider struct{} func (p *customProvider) Identifier() string { return "my-provider" } -func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) { +func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, *sdkaccess.AuthError) { token := r.Header.Get("X-Custom") if token == "" { - return nil, sdkaccess.ErrNoCredentials + return nil, sdkaccess.NewNotHandledError() } if token != "expected" { - return nil, sdkaccess.ErrInvalidCredential + return nil, sdkaccess.NewInvalidCredentialError() } return &sdkaccess.Result{ Provider: p.Identifier(), @@ -126,51 +109,46 @@ func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sd } func init() { - sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) { - return &customProvider{}, nil - }) + sdkaccess.RegisterProvider("custom", &customProvider{}) } ``` -自定义提供者需要实现 `Identifier()` 与 `Authenticate()`。在 `init` 中调用 `RegisterProvider` 暴露给配置层,工厂函数既能读取当前条目,也能访问完整根配置。 +自定义提供者需要实现 `Identifier()` 与 `Authenticate()`。在 `init` 中用已初始化实例调用 `RegisterProvider` 注册到全局 registry。 ## 错误语义 -- `ErrNoCredentials`:任何提供者都未识别到凭证。 -- `ErrInvalidCredential`:至少一个提供者处理了凭证但判定无效。 -- `ErrNotHandled`:告诉管理器跳到下一个提供者,不影响最终错误统计。 +- `NewNoCredentialsError()`(`AuthErrorCodeNoCredentials`):未提供或未识别到凭证。(HTTP 401) +- `NewInvalidCredentialError()`(`AuthErrorCodeInvalidCredential`):凭证存在但校验失败。(HTTP 401) +- `NewNotHandledError()`(`AuthErrorCodeNotHandled`):告诉管理器跳到下一个 provider。 +- `NewInternalAuthError(message, cause)`(`AuthErrorCodeInternal`):网络/系统错误。(HTTP 500) -自定义错误(例如网络异常)会马上冒泡返回。 +除可汇总的 `not_handled` / `no_credentials` / `invalid_credential` 外,其它错误会立即冒泡返回。 ## 与 cliproxy 集成 -使用 `sdk/cliproxy` 构建服务时会自动接入 `@sdk/access`。如果需要扩展内置行为,可传入自定义管理器: +使用 `sdk/cliproxy` 构建服务时会自动接入 `@sdk/access`。如果希望在宿主进程里复用同一个 `Manager` 实例,可传入自定义管理器: ```go coreCfg, _ := config.LoadConfig("config.yaml") -providers, _ := sdkaccess.BuildProviders(coreCfg) -manager := sdkaccess.NewManager() -manager.SetProviders(providers) +accessManager := sdkaccess.NewManager() svc, _ := cliproxy.NewBuilder(). WithConfig(coreCfg). - WithAccessManager(manager). + WithConfigPath("config.yaml"). + WithRequestAccessManager(accessManager). Build() ``` -服务会复用该管理器处理每一个入站请求,实现与 CLI 二进制一致的访问控制体验。 +请在调用 `Build()` 之前完成自定义 provider 的注册(通常通过空白导入触发 `init`),以确保它们被包含在全局 registry 的快照中。 ### 动态热更新提供者 -当配置发生变化时,可以重新构建提供者并替换当前列表: +当配置发生变化时,刷新依赖配置的 provider,然后重置 manager 的 provider 链: ```go -providers, err := sdkaccess.BuildProviders(newCfg) -if err != nil { - log.Errorf("reload auth providers failed: %v", err) - return -} -accessManager.SetProviders(providers) +// configaccess is github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access +configaccess.Register(&newCfg.SDKConfig) +accessManager.SetProviders(sdkaccess.RegisteredProviders()) ``` -这一流程与 `cliproxy.Service.refreshAccessProviders` 和 `api.Server.applyAccessConfig` 保持一致,避免为更新访问策略而重启进程。 +这一流程与 `internal/access.ApplyAccessProviders` 保持一致,避免为更新访问策略而重启进程。 diff --git a/internal/access/config_access/provider.go b/internal/access/config_access/provider.go index 70824524..84e8abcb 100644 --- a/internal/access/config_access/provider.go +++ b/internal/access/config_access/provider.go @@ -4,19 +4,28 @@ import ( "context" "net/http" "strings" - "sync" sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" ) -var registerOnce sync.Once - // Register ensures the config-access provider is available to the access manager. -func Register() { - registerOnce.Do(func() { - sdkaccess.RegisterProvider(sdkconfig.AccessProviderTypeConfigAPIKey, newProvider) - }) +func Register(cfg *sdkconfig.SDKConfig) { + if cfg == nil { + sdkaccess.UnregisterProvider(sdkaccess.AccessProviderTypeConfigAPIKey) + return + } + + keys := normalizeKeys(cfg.APIKeys) + if len(keys) == 0 { + sdkaccess.UnregisterProvider(sdkaccess.AccessProviderTypeConfigAPIKey) + return + } + + sdkaccess.RegisterProvider( + sdkaccess.AccessProviderTypeConfigAPIKey, + newProvider(sdkaccess.DefaultAccessProviderName, keys), + ) } type provider struct { @@ -24,34 +33,31 @@ type provider struct { keys map[string]struct{} } -func newProvider(cfg *sdkconfig.AccessProvider, _ *sdkconfig.SDKConfig) (sdkaccess.Provider, error) { - name := cfg.Name - if name == "" { - name = sdkconfig.DefaultAccessProviderName +func newProvider(name string, keys []string) *provider { + providerName := strings.TrimSpace(name) + if providerName == "" { + providerName = sdkaccess.DefaultAccessProviderName } - keys := make(map[string]struct{}, len(cfg.APIKeys)) - for _, key := range cfg.APIKeys { - if key == "" { - continue - } - keys[key] = struct{}{} + keySet := make(map[string]struct{}, len(keys)) + for _, key := range keys { + keySet[key] = struct{}{} } - return &provider{name: name, keys: keys}, nil + return &provider{name: providerName, keys: keySet} } func (p *provider) Identifier() string { if p == nil || p.name == "" { - return sdkconfig.DefaultAccessProviderName + return sdkaccess.DefaultAccessProviderName } return p.name } -func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess.Result, error) { +func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess.Result, *sdkaccess.AuthError) { if p == nil { - return nil, sdkaccess.ErrNotHandled + return nil, sdkaccess.NewNotHandledError() } if len(p.keys) == 0 { - return nil, sdkaccess.ErrNotHandled + return nil, sdkaccess.NewNotHandledError() } authHeader := r.Header.Get("Authorization") authHeaderGoogle := r.Header.Get("X-Goog-Api-Key") @@ -63,7 +69,7 @@ func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess. queryAuthToken = r.URL.Query().Get("auth_token") } if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && queryKey == "" && queryAuthToken == "" { - return nil, sdkaccess.ErrNoCredentials + return nil, sdkaccess.NewNoCredentialsError() } apiKey := extractBearerToken(authHeader) @@ -94,7 +100,7 @@ func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess. } } - return nil, sdkaccess.ErrInvalidCredential + return nil, sdkaccess.NewInvalidCredentialError() } func extractBearerToken(header string) string { @@ -110,3 +116,26 @@ func extractBearerToken(header string) string { } return strings.TrimSpace(parts[1]) } + +func normalizeKeys(keys []string) []string { + if len(keys) == 0 { + return nil + } + normalized := make([]string, 0, len(keys)) + seen := make(map[string]struct{}, len(keys)) + for _, key := range keys { + trimmedKey := strings.TrimSpace(key) + if trimmedKey == "" { + continue + } + if _, exists := seen[trimmedKey]; exists { + continue + } + seen[trimmedKey] = struct{}{} + normalized = append(normalized, trimmedKey) + } + if len(normalized) == 0 { + return nil + } + return normalized +} diff --git a/internal/access/reconcile.go b/internal/access/reconcile.go index 267d2fe0..36601f99 100644 --- a/internal/access/reconcile.go +++ b/internal/access/reconcile.go @@ -6,9 +6,9 @@ import ( "sort" "strings" + configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" - sdkConfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" log "github.com/sirupsen/logrus" ) @@ -17,26 +17,26 @@ import ( // ordered provider slice along with the identifiers of providers that were added, updated, or // removed compared to the previous configuration. func ReconcileProviders(oldCfg, newCfg *config.Config, existing []sdkaccess.Provider) (result []sdkaccess.Provider, added, updated, removed []string, err error) { + _ = oldCfg if newCfg == nil { return nil, nil, nil, nil, nil } + result = sdkaccess.RegisteredProviders() + existingMap := make(map[string]sdkaccess.Provider, len(existing)) for _, provider := range existing { - if provider == nil { + providerID := identifierFromProvider(provider) + if providerID == "" { continue } - existingMap[provider.Identifier()] = provider + existingMap[providerID] = provider } - oldCfgMap := accessProviderMap(oldCfg) - newEntries := collectProviderEntries(newCfg) - - result = make([]sdkaccess.Provider, 0, len(newEntries)) - finalIDs := make(map[string]struct{}, len(newEntries)) + finalIDs := make(map[string]struct{}, len(result)) isInlineProvider := func(id string) bool { - return strings.EqualFold(id, sdkConfig.DefaultAccessProviderName) + return strings.EqualFold(id, sdkaccess.DefaultAccessProviderName) } appendChange := func(list *[]string, id string) { if isInlineProvider(id) { @@ -45,85 +45,28 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []sdkaccess.Prov *list = append(*list, id) } - for _, providerCfg := range newEntries { - key := providerIdentifier(providerCfg) - if key == "" { + for _, provider := range result { + providerID := identifierFromProvider(provider) + if providerID == "" { continue } + finalIDs[providerID] = struct{}{} - forceRebuild := strings.EqualFold(strings.TrimSpace(providerCfg.Type), sdkConfig.AccessProviderTypeConfigAPIKey) - if oldCfgProvider, ok := oldCfgMap[key]; ok { - isAliased := oldCfgProvider == providerCfg - if !forceRebuild && !isAliased && providerConfigEqual(oldCfgProvider, providerCfg) { - if existingProvider, okExisting := existingMap[key]; okExisting { - result = append(result, existingProvider) - finalIDs[key] = struct{}{} - continue - } - } + existingProvider, exists := existingMap[providerID] + if !exists { + appendChange(&added, providerID) + continue } - - provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig) - if buildErr != nil { - return nil, nil, nil, nil, buildErr - } - if _, ok := oldCfgMap[key]; ok { - if _, existed := existingMap[key]; existed { - appendChange(&updated, key) - } else { - appendChange(&added, key) - } - } else { - appendChange(&added, key) - } - result = append(result, provider) - finalIDs[key] = struct{}{} - } - - if len(result) == 0 { - if inline := sdkConfig.MakeInlineAPIKeyProvider(newCfg.APIKeys); inline != nil { - key := providerIdentifier(inline) - if key != "" { - if oldCfgProvider, ok := oldCfgMap[key]; ok { - if providerConfigEqual(oldCfgProvider, inline) { - if existingProvider, okExisting := existingMap[key]; okExisting { - result = append(result, existingProvider) - finalIDs[key] = struct{}{} - goto inlineDone - } - } - } - provider, buildErr := sdkaccess.BuildProvider(inline, &newCfg.SDKConfig) - if buildErr != nil { - return nil, nil, nil, nil, buildErr - } - if _, existed := existingMap[key]; existed { - appendChange(&updated, key) - } else if _, hadOld := oldCfgMap[key]; hadOld { - appendChange(&updated, key) - } else { - appendChange(&added, key) - } - result = append(result, provider) - finalIDs[key] = struct{}{} - } - } - inlineDone: - } - - removedSet := make(map[string]struct{}) - for id := range existingMap { - if _, ok := finalIDs[id]; !ok { - if isInlineProvider(id) { - continue - } - removedSet[id] = struct{}{} + if !providerInstanceEqual(existingProvider, provider) { + appendChange(&updated, providerID) } } - removed = make([]string, 0, len(removedSet)) - for id := range removedSet { - removed = append(removed, id) + for providerID := range existingMap { + if _, exists := finalIDs[providerID]; exists { + continue + } + appendChange(&removed, providerID) } sort.Strings(added) @@ -142,6 +85,7 @@ func ApplyAccessProviders(manager *sdkaccess.Manager, oldCfg, newCfg *config.Con } existing := manager.Providers() + configaccess.Register(&newCfg.SDKConfig) providers, added, updated, removed, err := ReconcileProviders(oldCfg, newCfg, existing) if err != nil { log.Errorf("failed to reconcile request auth providers: %v", err) @@ -160,111 +104,24 @@ func ApplyAccessProviders(manager *sdkaccess.Manager, oldCfg, newCfg *config.Con return false, nil } -func accessProviderMap(cfg *config.Config) map[string]*sdkConfig.AccessProvider { - result := make(map[string]*sdkConfig.AccessProvider) - if cfg == nil { - return result - } - for i := range cfg.Access.Providers { - providerCfg := &cfg.Access.Providers[i] - if providerCfg.Type == "" { - continue - } - key := providerIdentifier(providerCfg) - if key == "" { - continue - } - result[key] = providerCfg - } - if len(result) == 0 && len(cfg.APIKeys) > 0 { - if provider := sdkConfig.MakeInlineAPIKeyProvider(cfg.APIKeys); provider != nil { - if key := providerIdentifier(provider); key != "" { - result[key] = provider - } - } - } - return result -} - -func collectProviderEntries(cfg *config.Config) []*sdkConfig.AccessProvider { - entries := make([]*sdkConfig.AccessProvider, 0, len(cfg.Access.Providers)) - for i := range cfg.Access.Providers { - providerCfg := &cfg.Access.Providers[i] - if providerCfg.Type == "" { - continue - } - if key := providerIdentifier(providerCfg); key != "" { - entries = append(entries, providerCfg) - } - } - if len(entries) == 0 && len(cfg.APIKeys) > 0 { - if inline := sdkConfig.MakeInlineAPIKeyProvider(cfg.APIKeys); inline != nil { - entries = append(entries, inline) - } - } - return entries -} - -func providerIdentifier(provider *sdkConfig.AccessProvider) string { +func identifierFromProvider(provider sdkaccess.Provider) string { if provider == nil { return "" } - if name := strings.TrimSpace(provider.Name); name != "" { - return name - } - typ := strings.TrimSpace(provider.Type) - if typ == "" { - return "" - } - if strings.EqualFold(typ, sdkConfig.AccessProviderTypeConfigAPIKey) { - return sdkConfig.DefaultAccessProviderName - } - return typ + return strings.TrimSpace(provider.Identifier()) } -func providerConfigEqual(a, b *sdkConfig.AccessProvider) bool { +func providerInstanceEqual(a, b sdkaccess.Provider) bool { if a == nil || b == nil { return a == nil && b == nil } - if !strings.EqualFold(strings.TrimSpace(a.Type), strings.TrimSpace(b.Type)) { + if reflect.TypeOf(a) != reflect.TypeOf(b) { return false } - if strings.TrimSpace(a.SDK) != strings.TrimSpace(b.SDK) { - return false + valueA := reflect.ValueOf(a) + valueB := reflect.ValueOf(b) + if valueA.Kind() == reflect.Pointer && valueB.Kind() == reflect.Pointer { + return valueA.Pointer() == valueB.Pointer() } - if !stringSetEqual(a.APIKeys, b.APIKeys) { - return false - } - if len(a.Config) != len(b.Config) { - return false - } - if len(a.Config) > 0 && !reflect.DeepEqual(a.Config, b.Config) { - return false - } - return true -} - -func stringSetEqual(a, b []string) bool { - if len(a) != len(b) { - return false - } - if len(a) == 0 { - return true - } - seen := make(map[string]int, len(a)) - for _, val := range a { - seen[val]++ - } - for _, val := range b { - count := seen[val] - if count == 0 { - return false - } - if count == 1 { - delete(seen, val) - } else { - seen[val] = count - 1 - } - } - return len(seen) == 0 + return reflect.DeepEqual(a, b) } diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 4e0e0284..66e89992 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -109,14 +109,13 @@ func (h *Handler) GetAPIKeys(c *gin.Context) { c.JSON(200, gin.H{"api-keys": h.c func (h *Handler) PutAPIKeys(c *gin.Context) { h.putStringList(c, func(v []string) { h.cfg.APIKeys = append([]string(nil), v...) - h.cfg.Access.Providers = nil }, nil) } func (h *Handler) PatchAPIKeys(c *gin.Context) { - h.patchStringList(c, &h.cfg.APIKeys, func() { h.cfg.Access.Providers = nil }) + h.patchStringList(c, &h.cfg.APIKeys, func() {}) } func (h *Handler) DeleteAPIKeys(c *gin.Context) { - h.deleteFromStringList(c, &h.cfg.APIKeys, func() { h.cfg.Access.Providers = nil }) + h.deleteFromStringList(c, &h.cfg.APIKeys, func() {}) } // gemini-api-key: []GeminiKey diff --git a/internal/api/server.go b/internal/api/server.go index 3eb09366..4cbcbba2 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -1033,14 +1033,10 @@ func AuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc { return } - switch { - case errors.Is(err, sdkaccess.ErrNoCredentials): - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing API key"}) - case errors.Is(err, sdkaccess.ErrInvalidCredential): - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"}) - default: + statusCode := err.HTTPStatusCode() + if statusCode >= http.StatusInternalServerError { log.Errorf("authentication middleware error: %v", err) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Authentication service error"}) } + c.AbortWithStatusJSON(statusCode, gin.H{"error": err.Message}) } } diff --git a/internal/config/config.go b/internal/config/config.go index fec58fe5..c78b2582 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -589,9 +589,6 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { cfg.ErrorLogsMaxFiles = 10 } - // Sync request authentication providers with inline API keys for backwards compatibility. - syncInlineAccessProvider(&cfg) - // Sanitize Gemini API key configuration and migrate legacy entries. cfg.SanitizeGeminiKeys() @@ -825,18 +822,6 @@ func normalizeModelPrefix(prefix string) string { return trimmed } -func syncInlineAccessProvider(cfg *Config) { - if cfg == nil { - return - } - if len(cfg.APIKeys) == 0 { - if provider := cfg.ConfigAPIKeyProvider(); provider != nil && len(provider.APIKeys) > 0 { - cfg.APIKeys = append([]string(nil), provider.APIKeys...) - } - } - cfg.Access.Providers = nil -} - // looksLikeBcrypt returns true if the provided string appears to be a bcrypt hash. func looksLikeBcrypt(s string) bool { return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$") @@ -924,7 +909,7 @@ func hashSecret(secret string) (string, error) { // SaveConfigPreserveComments writes the config back to YAML while preserving existing comments // and key ordering by loading the original file into a yaml.Node tree and updating values in-place. func SaveConfigPreserveComments(configFile string, cfg *Config) error { - persistCfg := sanitizeConfigForPersist(cfg) + persistCfg := cfg // Load original YAML as a node tree to preserve comments and ordering. data, err := os.ReadFile(configFile) if err != nil { @@ -992,16 +977,6 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error { return err } -func sanitizeConfigForPersist(cfg *Config) *Config { - if cfg == nil { - return nil - } - clone := *cfg - clone.SDKConfig = cfg.SDKConfig - clone.SDKConfig.Access = AccessConfig{} - return &clone -} - // SaveConfigPreserveCommentsUpdateNestedScalar updates a nested scalar key path like ["a","b"] // while preserving comments and positions. func SaveConfigPreserveCommentsUpdateNestedScalar(configFile string, path []string, value string) error { diff --git a/internal/config/sdk_config.go b/internal/config/sdk_config.go index 4d4abc37..5c3990a6 100644 --- a/internal/config/sdk_config.go +++ b/internal/config/sdk_config.go @@ -20,9 +20,6 @@ type SDKConfig struct { // APIKeys is a list of keys for authenticating clients to this proxy server. APIKeys []string `yaml:"api-keys" json:"api-keys"` - // Access holds request authentication provider configuration. - Access AccessConfig `yaml:"auth,omitempty" json:"auth,omitempty"` - // Streaming configures server-side streaming behavior (keep-alives and safe bootstrap retries). Streaming StreamingConfig `yaml:"streaming" json:"streaming"` @@ -42,65 +39,3 @@ type StreamingConfig struct { // <= 0 disables bootstrap retries. Default is 0. BootstrapRetries int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"` } - -// AccessConfig groups request authentication providers. -type AccessConfig struct { - // Providers lists configured authentication providers. - Providers []AccessProvider `yaml:"providers,omitempty" json:"providers,omitempty"` -} - -// AccessProvider describes a request authentication provider entry. -type AccessProvider struct { - // Name is the instance identifier for the provider. - Name string `yaml:"name" json:"name"` - - // Type selects the provider implementation registered via the SDK. - Type string `yaml:"type" json:"type"` - - // SDK optionally names a third-party SDK module providing this provider. - SDK string `yaml:"sdk,omitempty" json:"sdk,omitempty"` - - // APIKeys lists inline keys for providers that require them. - APIKeys []string `yaml:"api-keys,omitempty" json:"api-keys,omitempty"` - - // Config passes provider-specific options to the implementation. - Config map[string]any `yaml:"config,omitempty" json:"config,omitempty"` -} - -const ( - // AccessProviderTypeConfigAPIKey is the built-in provider validating inline API keys. - AccessProviderTypeConfigAPIKey = "config-api-key" - - // DefaultAccessProviderName is applied when no provider name is supplied. - DefaultAccessProviderName = "config-inline" -) - -// ConfigAPIKeyProvider returns the first inline API key provider if present. -func (c *SDKConfig) ConfigAPIKeyProvider() *AccessProvider { - if c == nil { - return nil - } - for i := range c.Access.Providers { - if c.Access.Providers[i].Type == AccessProviderTypeConfigAPIKey { - if c.Access.Providers[i].Name == "" { - c.Access.Providers[i].Name = DefaultAccessProviderName - } - return &c.Access.Providers[i] - } - } - return nil -} - -// MakeInlineAPIKeyProvider constructs an inline API key provider configuration. -// It returns nil when no keys are supplied. -func MakeInlineAPIKeyProvider(keys []string) *AccessProvider { - if len(keys) == 0 { - return nil - } - provider := &AccessProvider{ - Name: DefaultAccessProviderName, - Type: AccessProviderTypeConfigAPIKey, - APIKeys: append([]string(nil), keys...), - } - return provider -} diff --git a/sdk/access/errors.go b/sdk/access/errors.go index 6ea2cc1a..6f344bb0 100644 --- a/sdk/access/errors.go +++ b/sdk/access/errors.go @@ -1,12 +1,90 @@ package access -import "errors" - -var ( - // ErrNoCredentials indicates no recognizable credentials were supplied. - ErrNoCredentials = errors.New("access: no credentials provided") - // ErrInvalidCredential signals that supplied credentials were rejected by a provider. - ErrInvalidCredential = errors.New("access: invalid credential") - // ErrNotHandled tells the manager to continue trying other providers. - ErrNotHandled = errors.New("access: not handled") +import ( + "fmt" + "net/http" + "strings" ) + +// AuthErrorCode classifies authentication failures. +type AuthErrorCode string + +const ( + AuthErrorCodeNoCredentials AuthErrorCode = "no_credentials" + AuthErrorCodeInvalidCredential AuthErrorCode = "invalid_credential" + AuthErrorCodeNotHandled AuthErrorCode = "not_handled" + AuthErrorCodeInternal AuthErrorCode = "internal_error" +) + +// AuthError carries authentication failure details and HTTP status. +type AuthError struct { + Code AuthErrorCode + Message string + StatusCode int + Cause error +} + +func (e *AuthError) Error() string { + if e == nil { + return "" + } + message := strings.TrimSpace(e.Message) + if message == "" { + message = "authentication error" + } + if e.Cause != nil { + return fmt.Sprintf("%s: %v", message, e.Cause) + } + return message +} + +func (e *AuthError) Unwrap() error { + if e == nil { + return nil + } + return e.Cause +} + +// HTTPStatusCode returns a safe fallback for missing status codes. +func (e *AuthError) HTTPStatusCode() int { + if e == nil || e.StatusCode <= 0 { + return http.StatusInternalServerError + } + return e.StatusCode +} + +func newAuthError(code AuthErrorCode, message string, statusCode int, cause error) *AuthError { + return &AuthError{ + Code: code, + Message: message, + StatusCode: statusCode, + Cause: cause, + } +} + +func NewNoCredentialsError() *AuthError { + return newAuthError(AuthErrorCodeNoCredentials, "Missing API key", http.StatusUnauthorized, nil) +} + +func NewInvalidCredentialError() *AuthError { + return newAuthError(AuthErrorCodeInvalidCredential, "Invalid API key", http.StatusUnauthorized, nil) +} + +func NewNotHandledError() *AuthError { + return newAuthError(AuthErrorCodeNotHandled, "authentication provider did not handle request", 0, nil) +} + +func NewInternalAuthError(message string, cause error) *AuthError { + normalizedMessage := strings.TrimSpace(message) + if normalizedMessage == "" { + normalizedMessage = "Authentication service error" + } + return newAuthError(AuthErrorCodeInternal, normalizedMessage, http.StatusInternalServerError, cause) +} + +func IsAuthErrorCode(authErr *AuthError, code AuthErrorCode) bool { + if authErr == nil { + return false + } + return authErr.Code == code +} diff --git a/sdk/access/manager.go b/sdk/access/manager.go index fb5f8cca..2d4b0326 100644 --- a/sdk/access/manager.go +++ b/sdk/access/manager.go @@ -2,7 +2,6 @@ package access import ( "context" - "errors" "net/http" "sync" ) @@ -43,7 +42,7 @@ func (m *Manager) Providers() []Provider { } // Authenticate evaluates providers until one succeeds. -func (m *Manager) Authenticate(ctx context.Context, r *http.Request) (*Result, error) { +func (m *Manager) Authenticate(ctx context.Context, r *http.Request) (*Result, *AuthError) { if m == nil { return nil, nil } @@ -61,29 +60,29 @@ func (m *Manager) Authenticate(ctx context.Context, r *http.Request) (*Result, e if provider == nil { continue } - res, err := provider.Authenticate(ctx, r) - if err == nil { + res, authErr := provider.Authenticate(ctx, r) + if authErr == nil { return res, nil } - if errors.Is(err, ErrNotHandled) { + if IsAuthErrorCode(authErr, AuthErrorCodeNotHandled) { continue } - if errors.Is(err, ErrNoCredentials) { + if IsAuthErrorCode(authErr, AuthErrorCodeNoCredentials) { missing = true continue } - if errors.Is(err, ErrInvalidCredential) { + if IsAuthErrorCode(authErr, AuthErrorCodeInvalidCredential) { invalid = true continue } - return nil, err + return nil, authErr } if invalid { - return nil, ErrInvalidCredential + return nil, NewInvalidCredentialError() } if missing { - return nil, ErrNoCredentials + return nil, NewNoCredentialsError() } - return nil, ErrNoCredentials + return nil, NewNoCredentialsError() } diff --git a/sdk/access/registry.go b/sdk/access/registry.go index a29cdd96..cbb0d1c5 100644 --- a/sdk/access/registry.go +++ b/sdk/access/registry.go @@ -2,17 +2,15 @@ package access import ( "context" - "fmt" "net/http" + "strings" "sync" - - "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" ) // Provider validates credentials for incoming requests. type Provider interface { Identifier() string - Authenticate(ctx context.Context, r *http.Request) (*Result, error) + Authenticate(ctx context.Context, r *http.Request) (*Result, *AuthError) } // Result conveys authentication outcome. @@ -22,66 +20,64 @@ type Result struct { Metadata map[string]string } -// ProviderFactory builds a provider from configuration data. -type ProviderFactory func(cfg *config.AccessProvider, root *config.SDKConfig) (Provider, error) - var ( registryMu sync.RWMutex - registry = make(map[string]ProviderFactory) + registry = make(map[string]Provider) + order []string ) -// RegisterProvider registers a provider factory for a given type identifier. -func RegisterProvider(typ string, factory ProviderFactory) { - if typ == "" || factory == nil { +// RegisterProvider registers a pre-built provider instance for a given type identifier. +func RegisterProvider(typ string, provider Provider) { + normalizedType := strings.TrimSpace(typ) + if normalizedType == "" || provider == nil { return } + registryMu.Lock() - registry[typ] = factory + if _, exists := registry[normalizedType]; !exists { + order = append(order, normalizedType) + } + registry[normalizedType] = provider registryMu.Unlock() } -func BuildProvider(cfg *config.AccessProvider, root *config.SDKConfig) (Provider, error) { - if cfg == nil { - return nil, fmt.Errorf("access: nil provider config") +// UnregisterProvider removes a provider by type identifier. +func UnregisterProvider(typ string) { + normalizedType := strings.TrimSpace(typ) + if normalizedType == "" { + return } - registryMu.RLock() - factory, ok := registry[cfg.Type] - registryMu.RUnlock() - if !ok { - return nil, fmt.Errorf("access: provider type %q is not registered", cfg.Type) + registryMu.Lock() + if _, exists := registry[normalizedType]; !exists { + registryMu.Unlock() + return } - provider, err := factory(cfg, root) - if err != nil { - return nil, fmt.Errorf("access: failed to build provider %q: %w", cfg.Name, err) - } - return provider, nil -} - -// BuildProviders constructs providers declared in configuration. -func BuildProviders(root *config.SDKConfig) ([]Provider, error) { - if root == nil { - return nil, nil - } - providers := make([]Provider, 0, len(root.Access.Providers)) - for i := range root.Access.Providers { - providerCfg := &root.Access.Providers[i] - if providerCfg.Type == "" { + delete(registry, normalizedType) + for index := range order { + if order[index] != normalizedType { continue } - provider, err := BuildProvider(providerCfg, root) - if err != nil { - return nil, err + order = append(order[:index], order[index+1:]...) + break + } + registryMu.Unlock() +} + +// RegisteredProviders returns the global provider instances in registration order. +func RegisteredProviders() []Provider { + registryMu.RLock() + if len(order) == 0 { + registryMu.RUnlock() + return nil + } + providers := make([]Provider, 0, len(order)) + for _, providerType := range order { + provider, exists := registry[providerType] + if !exists || provider == nil { + continue } providers = append(providers, provider) } - if len(providers) == 0 { - if inline := config.MakeInlineAPIKeyProvider(root.APIKeys); inline != nil { - provider, err := BuildProvider(inline, root) - if err != nil { - return nil, err - } - providers = append(providers, provider) - } - } - return providers, nil + registryMu.RUnlock() + return providers } diff --git a/sdk/access/types.go b/sdk/access/types.go new file mode 100644 index 00000000..4ed80d04 --- /dev/null +++ b/sdk/access/types.go @@ -0,0 +1,47 @@ +package access + +// AccessConfig groups request authentication providers. +type AccessConfig struct { + // Providers lists configured authentication providers. + Providers []AccessProvider `yaml:"providers,omitempty" json:"providers,omitempty"` +} + +// AccessProvider describes a request authentication provider entry. +type AccessProvider struct { + // Name is the instance identifier for the provider. + Name string `yaml:"name" json:"name"` + + // Type selects the provider implementation registered via the SDK. + Type string `yaml:"type" json:"type"` + + // SDK optionally names a third-party SDK module providing this provider. + SDK string `yaml:"sdk,omitempty" json:"sdk,omitempty"` + + // APIKeys lists inline keys for providers that require them. + APIKeys []string `yaml:"api-keys,omitempty" json:"api-keys,omitempty"` + + // Config passes provider-specific options to the implementation. + Config map[string]any `yaml:"config,omitempty" json:"config,omitempty"` +} + +const ( + // AccessProviderTypeConfigAPIKey is the built-in provider validating inline API keys. + AccessProviderTypeConfigAPIKey = "config-api-key" + + // DefaultAccessProviderName is applied when no provider name is supplied. + DefaultAccessProviderName = "config-inline" +) + +// MakeInlineAPIKeyProvider constructs an inline API key provider configuration. +// It returns nil when no keys are supplied. +func MakeInlineAPIKeyProvider(keys []string) *AccessProvider { + if len(keys) == 0 { + return nil + } + provider := &AccessProvider{ + Name: DefaultAccessProviderName, + Type: AccessProviderTypeConfigAPIKey, + APIKeys: append([]string(nil), keys...), + } + return provider +} diff --git a/sdk/cliproxy/builder.go b/sdk/cliproxy/builder.go index 5eba18a0..60ca07f5 100644 --- a/sdk/cliproxy/builder.go +++ b/sdk/cliproxy/builder.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access" "github.com/router-for-me/CLIProxyAPI/v6/internal/api" sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" @@ -186,11 +187,8 @@ func (b *Builder) Build() (*Service, error) { accessManager = sdkaccess.NewManager() } - providers, err := sdkaccess.BuildProviders(&b.cfg.SDKConfig) - if err != nil { - return nil, err - } - accessManager.SetProviders(providers) + configaccess.Register(&b.cfg.SDKConfig) + accessManager.SetProviders(sdkaccess.RegisteredProviders()) coreManager := b.coreManager if coreManager == nil { diff --git a/sdk/config/config.go b/sdk/config/config.go index a9b5c2c3..14163418 100644 --- a/sdk/config/config.go +++ b/sdk/config/config.go @@ -7,8 +7,6 @@ package config import internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" type SDKConfig = internalconfig.SDKConfig -type AccessConfig = internalconfig.AccessConfig -type AccessProvider = internalconfig.AccessProvider type Config = internalconfig.Config @@ -34,15 +32,9 @@ type OpenAICompatibilityModel = internalconfig.OpenAICompatibilityModel type TLS = internalconfig.TLSConfig const ( - AccessProviderTypeConfigAPIKey = internalconfig.AccessProviderTypeConfigAPIKey - DefaultAccessProviderName = internalconfig.DefaultAccessProviderName - DefaultPanelGitHubRepository = internalconfig.DefaultPanelGitHubRepository + DefaultPanelGitHubRepository = internalconfig.DefaultPanelGitHubRepository ) -func MakeInlineAPIKeyProvider(keys []string) *AccessProvider { - return internalconfig.MakeInlineAPIKeyProvider(keys) -} - func LoadConfig(configFile string) (*Config, error) { return internalconfig.LoadConfig(configFile) } func LoadConfigOptional(configFile string, optional bool) (*Config, error) {