mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 04:10:51 +08:00
refactor(sdk): simplify provider lifecycle and registration logic
This commit is contained in:
@@ -445,7 +445,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register built-in access providers before constructing services.
|
// Register built-in access providers before constructing services.
|
||||||
configaccess.Register()
|
configaccess.Register(&cfg.SDKConfig)
|
||||||
|
|
||||||
// Handle different command modes based on the provided flags.
|
// Handle different command modes based on the provided flags.
|
||||||
|
|
||||||
|
|||||||
@@ -7,80 +7,71 @@ The `github.com/router-for-me/CLIProxyAPI/v6/sdk/access` package centralizes inb
|
|||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
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`.
|
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
|
## Manager Lifecycle
|
||||||
|
|
||||||
```go
|
```go
|
||||||
manager := sdkaccess.NewManager()
|
manager := sdkaccess.NewManager()
|
||||||
providers, err := sdkaccess.BuildProviders(cfg)
|
manager.SetProviders(sdkaccess.RegisteredProviders())
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
manager.SetProviders(providers)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* `NewManager` constructs an empty manager.
|
* `NewManager` constructs an empty manager.
|
||||||
* `SetProviders` replaces the provider slice using a defensive copy.
|
* `SetProviders` replaces the provider slice using a defensive copy.
|
||||||
* `Providers` retrieves a snapshot that can be iterated safely from other goroutines.
|
* `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
|
## Authenticating Requests
|
||||||
|
|
||||||
```go
|
```go
|
||||||
result, err := manager.Authenticate(ctx, req)
|
result, authErr := manager.Authenticate(ctx, req)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case authErr == nil:
|
||||||
// Authentication succeeded; result describes the provider and principal.
|
// 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.
|
// No recognizable credentials were supplied.
|
||||||
case errors.Is(err, sdkaccess.ErrInvalidCredential):
|
case sdkaccess.IsAuthErrorCode(authErr, sdkaccess.AuthErrorCodeInvalidCredential):
|
||||||
// Supplied credentials were present but rejected.
|
// Supplied credentials were present but rejected.
|
||||||
default:
|
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.
|
`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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Each `Result` includes the provider identifier, the resolved principal, and optional metadata (for example, which header carried the credential).
|
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
|
```yaml
|
||||||
auth:
|
api-keys:
|
||||||
providers:
|
- sk-test-123
|
||||||
- name: inline-api
|
- sk-prod-456
|
||||||
type: config-api-key
|
|
||||||
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, import it for its registration side effect:
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
@@ -89,19 +80,11 @@ import (
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
The blank identifier import ensures `init` runs so `sdkaccess.RegisterProvider` executes before `BuildProviders` is called.
|
The blank identifier import ensures `init` runs so `sdkaccess.RegisterProvider` executes before you call `RegisteredProviders()` (or before `cliproxy.NewBuilder().Build()`).
|
||||||
|
|
||||||
## 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`.
|
|
||||||
|
|
||||||
### Metadata and auditing
|
### 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
|
## Writing Custom Providers
|
||||||
|
|
||||||
@@ -110,13 +93,13 @@ type customProvider struct{}
|
|||||||
|
|
||||||
func (p *customProvider) Identifier() string { return "my-provider" }
|
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")
|
token := r.Header.Get("X-Custom")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return nil, sdkaccess.ErrNoCredentials
|
return nil, sdkaccess.NewNotHandledError()
|
||||||
}
|
}
|
||||||
if token != "expected" {
|
if token != "expected" {
|
||||||
return nil, sdkaccess.ErrInvalidCredential
|
return nil, sdkaccess.NewInvalidCredentialError()
|
||||||
}
|
}
|
||||||
return &sdkaccess.Result{
|
return &sdkaccess.Result{
|
||||||
Provider: p.Identifier(),
|
Provider: p.Identifier(),
|
||||||
@@ -126,51 +109,46 @@ func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) {
|
sdkaccess.RegisterProvider("custom", &customProvider{})
|
||||||
return &customProvider{}, nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
## Error Semantics
|
||||||
|
|
||||||
- `ErrNoCredentials`: no credentials were present or recognized by any provider.
|
- `NewNoCredentialsError()` (`AuthErrorCodeNoCredentials`): no credentials were present or recognized. (HTTP 401)
|
||||||
- `ErrInvalidCredential`: at least one provider processed the credentials but rejected them.
|
- `NewInvalidCredentialError()` (`AuthErrorCodeInvalidCredential`): credentials were present but rejected. (HTTP 401)
|
||||||
- `ErrNotHandled`: instructs the manager to fall through to the next provider without affecting aggregate error reporting.
|
- `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
|
## 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
|
```go
|
||||||
coreCfg, _ := config.LoadConfig("config.yaml")
|
coreCfg, _ := config.LoadConfig("config.yaml")
|
||||||
providers, _ := sdkaccess.BuildProviders(coreCfg)
|
accessManager := sdkaccess.NewManager()
|
||||||
manager := sdkaccess.NewManager()
|
|
||||||
manager.SetProviders(providers)
|
|
||||||
|
|
||||||
svc, _ := cliproxy.NewBuilder().
|
svc, _ := cliproxy.NewBuilder().
|
||||||
WithConfig(coreCfg).
|
WithConfig(coreCfg).
|
||||||
WithAccessManager(manager).
|
WithConfigPath("config.yaml").
|
||||||
|
WithRequestAccessManager(accessManager).
|
||||||
Build()
|
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
|
```go
|
||||||
providers, err := sdkaccess.BuildProviders(newCfg)
|
// configaccess is github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access
|
||||||
if err != nil {
|
configaccess.Register(&newCfg.SDKConfig)
|
||||||
log.Errorf("reload auth providers failed: %v", err)
|
accessManager.SetProviders(sdkaccess.RegisteredProviders())
|
||||||
return
|
|
||||||
}
|
|
||||||
accessManager.SetProviders(providers)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -7,80 +7,71 @@
|
|||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
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` 添加依赖。
|
通过 `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 添加依赖。
|
||||||
|
|
||||||
|
## Provider Registry
|
||||||
|
|
||||||
|
访问提供者是全局注册,然后以快照形式挂到 `Manager` 上:
|
||||||
|
|
||||||
|
- `RegisterProvider(type, provider)` 注册一个已经初始化好的 provider 实例。
|
||||||
|
- 每个 `type` 第一次出现时会记录其注册顺序。
|
||||||
|
- `RegisteredProviders()` 会按该顺序返回 provider 列表。
|
||||||
|
|
||||||
## 管理器生命周期
|
## 管理器生命周期
|
||||||
|
|
||||||
```go
|
```go
|
||||||
manager := sdkaccess.NewManager()
|
manager := sdkaccess.NewManager()
|
||||||
providers, err := sdkaccess.BuildProviders(cfg)
|
manager.SetProviders(sdkaccess.RegisteredProviders())
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
manager.SetProviders(providers)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `NewManager` 创建空管理器。
|
- `NewManager` 创建空管理器。
|
||||||
- `SetProviders` 替换提供者切片并做防御性拷贝。
|
- `SetProviders` 替换提供者切片并做防御性拷贝。
|
||||||
- `Providers` 返回适合并发读取的快照。
|
- `Providers` 返回适合并发读取的快照。
|
||||||
- `BuildProviders` 将 `config.Config` 中的访问配置转换成可运行的提供者。当配置没有显式声明但包含顶层 `api-keys` 时,会自动挂载内建的 `config-api-key` 提供者。
|
|
||||||
|
如果管理器本身为 `nil` 或未配置任何 provider,调用会返回 `nil, nil`,可视为关闭访问控制。
|
||||||
|
|
||||||
## 认证请求
|
## 认证请求
|
||||||
|
|
||||||
```go
|
```go
|
||||||
result, err := manager.Authenticate(ctx, req)
|
result, authErr := manager.Authenticate(ctx, req)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case authErr == nil:
|
||||||
// Authentication succeeded; result carries provider and principal.
|
// Authentication succeeded; result carries provider and principal.
|
||||||
case errors.Is(err, sdkaccess.ErrNoCredentials):
|
case sdkaccess.IsAuthErrorCode(authErr, sdkaccess.AuthErrorCodeNoCredentials):
|
||||||
// No recognizable credentials were supplied.
|
// No recognizable credentials were supplied.
|
||||||
case errors.Is(err, sdkaccess.ErrInvalidCredential):
|
case sdkaccess.IsAuthErrorCode(authErr, sdkaccess.AuthErrorCodeInvalidCredential):
|
||||||
// Credentials were present but rejected.
|
// Credentials were present but rejected.
|
||||||
default:
|
default:
|
||||||
// Provider surfaced a transport-level failure.
|
// Provider surfaced a transport-level failure.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`Manager.Authenticate` 按配置顺序遍历提供者。遇到成功立即返回,`ErrNotHandled` 会继续尝试下一个;若发现 `ErrNoCredentials` 或 `ErrInvalidCredential`,会在遍历结束后汇总给调用方。
|
`Manager.Authenticate` 会按顺序遍历 provider:遇到成功立即返回,`AuthErrorCodeNotHandled` 会继续尝试下一个;`AuthErrorCodeNoCredentials` / `AuthErrorCodeInvalidCredential` 会在遍历结束后汇总给调用方。
|
||||||
|
|
||||||
若管理器本身为 `nil` 或尚未注册提供者,调用会返回 `nil, nil`,让调用方无需针对错误做额外分支即可关闭访问控制。
|
|
||||||
|
|
||||||
`Result` 提供认证提供者标识、解析出的主体以及可选元数据(例如凭证来源)。
|
`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
|
```yaml
|
||||||
auth:
|
api-keys:
|
||||||
providers:
|
- sk-test-123
|
||||||
- name: inline-api
|
- sk-prod-456
|
||||||
type: config-api-key
|
|
||||||
api-keys:
|
|
||||||
- sk-test-123
|
|
||||||
- sk-prod-456
|
|
||||||
```
|
```
|
||||||
|
|
||||||
条目映射到 `config.AccessProvider`:`name` 指定实例名,`type` 选择注册的工厂,`sdk` 可引用第三方模块,`api-keys` 提供内联凭证,`config` 用于传递特定选项。
|
## 引入外部 Go 模块提供者
|
||||||
|
|
||||||
### 引入外部 SDK 提供者
|
若要消费其它 Go 模块输出的访问提供者,直接用空白标识符导入以触发其 `init` 注册即可:
|
||||||
|
|
||||||
若要消费其它 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
|
```go
|
||||||
import (
|
import (
|
||||||
@@ -89,19 +80,11 @@ import (
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
通过空白标识符导入即可确保 `init` 调用,先于 `BuildProviders` 完成 `sdkaccess.RegisterProvider`。
|
空白导入可确保 `init` 先执行,从而在你调用 `RegisteredProviders()`(或 `cliproxy.NewBuilder().Build()`)之前完成 `sdkaccess.RegisterProvider`。
|
||||||
|
|
||||||
## 内建提供者
|
|
||||||
|
|
||||||
当前 SDK 默认内置:
|
|
||||||
|
|
||||||
- `config-api-key`:校验配置中的 API Key。它从 `Authorization: Bearer`、`X-Goog-Api-Key`、`X-Api-Key` 以及查询参数 `?key=` 提取凭证,不匹配时抛出 `ErrInvalidCredential`。
|
|
||||||
|
|
||||||
导入第三方包即可通过 `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) 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")
|
token := r.Header.Get("X-Custom")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return nil, sdkaccess.ErrNoCredentials
|
return nil, sdkaccess.NewNotHandledError()
|
||||||
}
|
}
|
||||||
if token != "expected" {
|
if token != "expected" {
|
||||||
return nil, sdkaccess.ErrInvalidCredential
|
return nil, sdkaccess.NewInvalidCredentialError()
|
||||||
}
|
}
|
||||||
return &sdkaccess.Result{
|
return &sdkaccess.Result{
|
||||||
Provider: p.Identifier(),
|
Provider: p.Identifier(),
|
||||||
@@ -126,51 +109,46 @@ func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) {
|
sdkaccess.RegisterProvider("custom", &customProvider{})
|
||||||
return &customProvider{}, nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
自定义提供者需要实现 `Identifier()` 与 `Authenticate()`。在 `init` 中调用 `RegisterProvider` 暴露给配置层,工厂函数既能读取当前条目,也能访问完整根配置。
|
自定义提供者需要实现 `Identifier()` 与 `Authenticate()`。在 `init` 中用已初始化实例调用 `RegisterProvider` 注册到全局 registry。
|
||||||
|
|
||||||
## 错误语义
|
## 错误语义
|
||||||
|
|
||||||
- `ErrNoCredentials`:任何提供者都未识别到凭证。
|
- `NewNoCredentialsError()`(`AuthErrorCodeNoCredentials`):未提供或未识别到凭证。(HTTP 401)
|
||||||
- `ErrInvalidCredential`:至少一个提供者处理了凭证但判定无效。
|
- `NewInvalidCredentialError()`(`AuthErrorCodeInvalidCredential`):凭证存在但校验失败。(HTTP 401)
|
||||||
- `ErrNotHandled`:告诉管理器跳到下一个提供者,不影响最终错误统计。
|
- `NewNotHandledError()`(`AuthErrorCodeNotHandled`):告诉管理器跳到下一个 provider。
|
||||||
|
- `NewInternalAuthError(message, cause)`(`AuthErrorCodeInternal`):网络/系统错误。(HTTP 500)
|
||||||
|
|
||||||
自定义错误(例如网络异常)会马上冒泡返回。
|
除可汇总的 `not_handled` / `no_credentials` / `invalid_credential` 外,其它错误会立即冒泡返回。
|
||||||
|
|
||||||
## 与 cliproxy 集成
|
## 与 cliproxy 集成
|
||||||
|
|
||||||
使用 `sdk/cliproxy` 构建服务时会自动接入 `@sdk/access`。如果需要扩展内置行为,可传入自定义管理器:
|
使用 `sdk/cliproxy` 构建服务时会自动接入 `@sdk/access`。如果希望在宿主进程里复用同一个 `Manager` 实例,可传入自定义管理器:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
coreCfg, _ := config.LoadConfig("config.yaml")
|
coreCfg, _ := config.LoadConfig("config.yaml")
|
||||||
providers, _ := sdkaccess.BuildProviders(coreCfg)
|
accessManager := sdkaccess.NewManager()
|
||||||
manager := sdkaccess.NewManager()
|
|
||||||
manager.SetProviders(providers)
|
|
||||||
|
|
||||||
svc, _ := cliproxy.NewBuilder().
|
svc, _ := cliproxy.NewBuilder().
|
||||||
WithConfig(coreCfg).
|
WithConfig(coreCfg).
|
||||||
WithAccessManager(manager).
|
WithConfigPath("config.yaml").
|
||||||
|
WithRequestAccessManager(accessManager).
|
||||||
Build()
|
Build()
|
||||||
```
|
```
|
||||||
|
|
||||||
服务会复用该管理器处理每一个入站请求,实现与 CLI 二进制一致的访问控制体验。
|
请在调用 `Build()` 之前完成自定义 provider 的注册(通常通过空白导入触发 `init`),以确保它们被包含在全局 registry 的快照中。
|
||||||
|
|
||||||
### 动态热更新提供者
|
### 动态热更新提供者
|
||||||
|
|
||||||
当配置发生变化时,可以重新构建提供者并替换当前列表:
|
当配置发生变化时,刷新依赖配置的 provider,然后重置 manager 的 provider 链:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
providers, err := sdkaccess.BuildProviders(newCfg)
|
// configaccess is github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access
|
||||||
if err != nil {
|
configaccess.Register(&newCfg.SDKConfig)
|
||||||
log.Errorf("reload auth providers failed: %v", err)
|
accessManager.SetProviders(sdkaccess.RegisteredProviders())
|
||||||
return
|
|
||||||
}
|
|
||||||
accessManager.SetProviders(providers)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
这一流程与 `cliproxy.Service.refreshAccessProviders` 和 `api.Server.applyAccessConfig` 保持一致,避免为更新访问策略而重启进程。
|
这一流程与 `internal/access.ApplyAccessProviders` 保持一致,避免为更新访问策略而重启进程。
|
||||||
|
|||||||
@@ -4,19 +4,28 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
||||||
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
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.
|
// Register ensures the config-access provider is available to the access manager.
|
||||||
func Register() {
|
func Register(cfg *sdkconfig.SDKConfig) {
|
||||||
registerOnce.Do(func() {
|
if cfg == nil {
|
||||||
sdkaccess.RegisterProvider(sdkconfig.AccessProviderTypeConfigAPIKey, newProvider)
|
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 {
|
type provider struct {
|
||||||
@@ -24,34 +33,31 @@ type provider struct {
|
|||||||
keys map[string]struct{}
|
keys map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProvider(cfg *sdkconfig.AccessProvider, _ *sdkconfig.SDKConfig) (sdkaccess.Provider, error) {
|
func newProvider(name string, keys []string) *provider {
|
||||||
name := cfg.Name
|
providerName := strings.TrimSpace(name)
|
||||||
if name == "" {
|
if providerName == "" {
|
||||||
name = sdkconfig.DefaultAccessProviderName
|
providerName = sdkaccess.DefaultAccessProviderName
|
||||||
}
|
}
|
||||||
keys := make(map[string]struct{}, len(cfg.APIKeys))
|
keySet := make(map[string]struct{}, len(keys))
|
||||||
for _, key := range cfg.APIKeys {
|
for _, key := range keys {
|
||||||
if key == "" {
|
keySet[key] = struct{}{}
|
||||||
continue
|
|
||||||
}
|
|
||||||
keys[key] = struct{}{}
|
|
||||||
}
|
}
|
||||||
return &provider{name: name, keys: keys}, nil
|
return &provider{name: providerName, keys: keySet}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *provider) Identifier() string {
|
func (p *provider) Identifier() string {
|
||||||
if p == nil || p.name == "" {
|
if p == nil || p.name == "" {
|
||||||
return sdkconfig.DefaultAccessProviderName
|
return sdkaccess.DefaultAccessProviderName
|
||||||
}
|
}
|
||||||
return p.name
|
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 {
|
if p == nil {
|
||||||
return nil, sdkaccess.ErrNotHandled
|
return nil, sdkaccess.NewNotHandledError()
|
||||||
}
|
}
|
||||||
if len(p.keys) == 0 {
|
if len(p.keys) == 0 {
|
||||||
return nil, sdkaccess.ErrNotHandled
|
return nil, sdkaccess.NewNotHandledError()
|
||||||
}
|
}
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
authHeaderGoogle := r.Header.Get("X-Goog-Api-Key")
|
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")
|
queryAuthToken = r.URL.Query().Get("auth_token")
|
||||||
}
|
}
|
||||||
if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && queryKey == "" && queryAuthToken == "" {
|
if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && queryKey == "" && queryAuthToken == "" {
|
||||||
return nil, sdkaccess.ErrNoCredentials
|
return nil, sdkaccess.NewNoCredentialsError()
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := extractBearerToken(authHeader)
|
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 {
|
func extractBearerToken(header string) string {
|
||||||
@@ -110,3 +116,26 @@ func extractBearerToken(header string) string {
|
|||||||
}
|
}
|
||||||
return strings.TrimSpace(parts[1])
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
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"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,26 +17,26 @@ import (
|
|||||||
// ordered provider slice along with the identifiers of providers that were added, updated, or
|
// ordered provider slice along with the identifiers of providers that were added, updated, or
|
||||||
// removed compared to the previous configuration.
|
// removed compared to the previous configuration.
|
||||||
func ReconcileProviders(oldCfg, newCfg *config.Config, existing []sdkaccess.Provider) (result []sdkaccess.Provider, added, updated, removed []string, err error) {
|
func ReconcileProviders(oldCfg, newCfg *config.Config, existing []sdkaccess.Provider) (result []sdkaccess.Provider, added, updated, removed []string, err error) {
|
||||||
|
_ = oldCfg
|
||||||
if newCfg == nil {
|
if newCfg == nil {
|
||||||
return nil, nil, nil, nil, nil
|
return nil, nil, nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result = sdkaccess.RegisteredProviders()
|
||||||
|
|
||||||
existingMap := make(map[string]sdkaccess.Provider, len(existing))
|
existingMap := make(map[string]sdkaccess.Provider, len(existing))
|
||||||
for _, provider := range existing {
|
for _, provider := range existing {
|
||||||
if provider == nil {
|
providerID := identifierFromProvider(provider)
|
||||||
|
if providerID == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
existingMap[provider.Identifier()] = provider
|
existingMap[providerID] = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCfgMap := accessProviderMap(oldCfg)
|
finalIDs := make(map[string]struct{}, len(result))
|
||||||
newEntries := collectProviderEntries(newCfg)
|
|
||||||
|
|
||||||
result = make([]sdkaccess.Provider, 0, len(newEntries))
|
|
||||||
finalIDs := make(map[string]struct{}, len(newEntries))
|
|
||||||
|
|
||||||
isInlineProvider := func(id string) bool {
|
isInlineProvider := func(id string) bool {
|
||||||
return strings.EqualFold(id, sdkConfig.DefaultAccessProviderName)
|
return strings.EqualFold(id, sdkaccess.DefaultAccessProviderName)
|
||||||
}
|
}
|
||||||
appendChange := func(list *[]string, id string) {
|
appendChange := func(list *[]string, id string) {
|
||||||
if isInlineProvider(id) {
|
if isInlineProvider(id) {
|
||||||
@@ -45,85 +45,28 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []sdkaccess.Prov
|
|||||||
*list = append(*list, id)
|
*list = append(*list, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, providerCfg := range newEntries {
|
for _, provider := range result {
|
||||||
key := providerIdentifier(providerCfg)
|
providerID := identifierFromProvider(provider)
|
||||||
if key == "" {
|
if providerID == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
finalIDs[providerID] = struct{}{}
|
||||||
|
|
||||||
forceRebuild := strings.EqualFold(strings.TrimSpace(providerCfg.Type), sdkConfig.AccessProviderTypeConfigAPIKey)
|
existingProvider, exists := existingMap[providerID]
|
||||||
if oldCfgProvider, ok := oldCfgMap[key]; ok {
|
if !exists {
|
||||||
isAliased := oldCfgProvider == providerCfg
|
appendChange(&added, providerID)
|
||||||
if !forceRebuild && !isAliased && providerConfigEqual(oldCfgProvider, providerCfg) {
|
continue
|
||||||
if existingProvider, okExisting := existingMap[key]; okExisting {
|
|
||||||
result = append(result, existingProvider)
|
|
||||||
finalIDs[key] = struct{}{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if !providerInstanceEqual(existingProvider, provider) {
|
||||||
provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig)
|
appendChange(&updated, providerID)
|
||||||
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{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removed = make([]string, 0, len(removedSet))
|
for providerID := range existingMap {
|
||||||
for id := range removedSet {
|
if _, exists := finalIDs[providerID]; exists {
|
||||||
removed = append(removed, id)
|
continue
|
||||||
|
}
|
||||||
|
appendChange(&removed, providerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(added)
|
sort.Strings(added)
|
||||||
@@ -142,6 +85,7 @@ func ApplyAccessProviders(manager *sdkaccess.Manager, oldCfg, newCfg *config.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
existing := manager.Providers()
|
existing := manager.Providers()
|
||||||
|
configaccess.Register(&newCfg.SDKConfig)
|
||||||
providers, added, updated, removed, err := ReconcileProviders(oldCfg, newCfg, existing)
|
providers, added, updated, removed, err := ReconcileProviders(oldCfg, newCfg, existing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to reconcile request auth providers: %v", err)
|
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
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func accessProviderMap(cfg *config.Config) map[string]*sdkConfig.AccessProvider {
|
func identifierFromProvider(provider sdkaccess.Provider) string {
|
||||||
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 {
|
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if name := strings.TrimSpace(provider.Name); name != "" {
|
return strings.TrimSpace(provider.Identifier())
|
||||||
return name
|
|
||||||
}
|
|
||||||
typ := strings.TrimSpace(provider.Type)
|
|
||||||
if typ == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if strings.EqualFold(typ, sdkConfig.AccessProviderTypeConfigAPIKey) {
|
|
||||||
return sdkConfig.DefaultAccessProviderName
|
|
||||||
}
|
|
||||||
return typ
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerConfigEqual(a, b *sdkConfig.AccessProvider) bool {
|
func providerInstanceEqual(a, b sdkaccess.Provider) bool {
|
||||||
if a == nil || b == nil {
|
if a == nil || b == nil {
|
||||||
return 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
|
return false
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(a.SDK) != strings.TrimSpace(b.SDK) {
|
valueA := reflect.ValueOf(a)
|
||||||
return false
|
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 reflect.DeepEqual(a, b)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func (h *Handler) PutAPIKeys(c *gin.Context) {
|
||||||
h.putStringList(c, func(v []string) {
|
h.putStringList(c, func(v []string) {
|
||||||
h.cfg.APIKeys = append([]string(nil), v...)
|
h.cfg.APIKeys = append([]string(nil), v...)
|
||||||
h.cfg.Access.Providers = nil
|
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchAPIKeys(c *gin.Context) {
|
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) {
|
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
|
// gemini-api-key: []GeminiKey
|
||||||
|
|||||||
@@ -1033,14 +1033,10 @@ func AuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
statusCode := err.HTTPStatusCode()
|
||||||
case errors.Is(err, sdkaccess.ErrNoCredentials):
|
if statusCode >= http.StatusInternalServerError {
|
||||||
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:
|
|
||||||
log.Errorf("authentication middleware error: %v", err)
|
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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -589,9 +589,6 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
cfg.ErrorLogsMaxFiles = 10
|
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.
|
// Sanitize Gemini API key configuration and migrate legacy entries.
|
||||||
cfg.SanitizeGeminiKeys()
|
cfg.SanitizeGeminiKeys()
|
||||||
|
|
||||||
@@ -825,18 +822,6 @@ func normalizeModelPrefix(prefix string) string {
|
|||||||
return trimmed
|
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.
|
// looksLikeBcrypt returns true if the provided string appears to be a bcrypt hash.
|
||||||
func looksLikeBcrypt(s string) bool {
|
func looksLikeBcrypt(s string) bool {
|
||||||
return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$")
|
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
|
// 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.
|
// 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 {
|
func SaveConfigPreserveComments(configFile string, cfg *Config) error {
|
||||||
persistCfg := sanitizeConfigForPersist(cfg)
|
persistCfg := cfg
|
||||||
// Load original YAML as a node tree to preserve comments and ordering.
|
// Load original YAML as a node tree to preserve comments and ordering.
|
||||||
data, err := os.ReadFile(configFile)
|
data, err := os.ReadFile(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -992,16 +977,6 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error {
|
|||||||
return err
|
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"]
|
// SaveConfigPreserveCommentsUpdateNestedScalar updates a nested scalar key path like ["a","b"]
|
||||||
// while preserving comments and positions.
|
// while preserving comments and positions.
|
||||||
func SaveConfigPreserveCommentsUpdateNestedScalar(configFile string, path []string, value string) error {
|
func SaveConfigPreserveCommentsUpdateNestedScalar(configFile string, path []string, value string) error {
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ type SDKConfig struct {
|
|||||||
// APIKeys is a list of keys for authenticating clients to this proxy server.
|
// APIKeys is a list of keys for authenticating clients to this proxy server.
|
||||||
APIKeys []string `yaml:"api-keys" json:"api-keys"`
|
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 configures server-side streaming behavior (keep-alives and safe bootstrap retries).
|
||||||
Streaming StreamingConfig `yaml:"streaming" json:"streaming"`
|
Streaming StreamingConfig `yaml:"streaming" json:"streaming"`
|
||||||
|
|
||||||
@@ -42,65 +39,3 @@ type StreamingConfig struct {
|
|||||||
// <= 0 disables bootstrap retries. Default is 0.
|
// <= 0 disables bootstrap retries. Default is 0.
|
||||||
BootstrapRetries int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"`
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,90 @@
|
|||||||
package access
|
package access
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"fmt"
|
||||||
var (
|
"net/http"
|
||||||
// ErrNoCredentials indicates no recognizable credentials were supplied.
|
"strings"
|
||||||
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")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package access
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -43,7 +42,7 @@ func (m *Manager) Providers() []Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate evaluates providers until one succeeds.
|
// 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 {
|
if m == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -61,29 +60,29 @@ func (m *Manager) Authenticate(ctx context.Context, r *http.Request) (*Result, e
|
|||||||
if provider == nil {
|
if provider == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := provider.Authenticate(ctx, r)
|
res, authErr := provider.Authenticate(ctx, r)
|
||||||
if err == nil {
|
if authErr == nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrNotHandled) {
|
if IsAuthErrorCode(authErr, AuthErrorCodeNotHandled) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrNoCredentials) {
|
if IsAuthErrorCode(authErr, AuthErrorCodeNoCredentials) {
|
||||||
missing = true
|
missing = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrInvalidCredential) {
|
if IsAuthErrorCode(authErr, AuthErrorCodeInvalidCredential) {
|
||||||
invalid = true
|
invalid = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, authErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if invalid {
|
if invalid {
|
||||||
return nil, ErrInvalidCredential
|
return nil, NewInvalidCredentialError()
|
||||||
}
|
}
|
||||||
if missing {
|
if missing {
|
||||||
return nil, ErrNoCredentials
|
return nil, NewNoCredentialsError()
|
||||||
}
|
}
|
||||||
return nil, ErrNoCredentials
|
return nil, NewNoCredentialsError()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,15 @@ package access
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider validates credentials for incoming requests.
|
// Provider validates credentials for incoming requests.
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Identifier() string
|
Identifier() string
|
||||||
Authenticate(ctx context.Context, r *http.Request) (*Result, error)
|
Authenticate(ctx context.Context, r *http.Request) (*Result, *AuthError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result conveys authentication outcome.
|
// Result conveys authentication outcome.
|
||||||
@@ -22,66 +20,64 @@ type Result struct {
|
|||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderFactory builds a provider from configuration data.
|
|
||||||
type ProviderFactory func(cfg *config.AccessProvider, root *config.SDKConfig) (Provider, error)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
registryMu sync.RWMutex
|
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.
|
// RegisterProvider registers a pre-built provider instance for a given type identifier.
|
||||||
func RegisterProvider(typ string, factory ProviderFactory) {
|
func RegisterProvider(typ string, provider Provider) {
|
||||||
if typ == "" || factory == nil {
|
normalizedType := strings.TrimSpace(typ)
|
||||||
|
if normalizedType == "" || provider == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
registryMu.Lock()
|
registryMu.Lock()
|
||||||
registry[typ] = factory
|
if _, exists := registry[normalizedType]; !exists {
|
||||||
|
order = append(order, normalizedType)
|
||||||
|
}
|
||||||
|
registry[normalizedType] = provider
|
||||||
registryMu.Unlock()
|
registryMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildProvider(cfg *config.AccessProvider, root *config.SDKConfig) (Provider, error) {
|
// UnregisterProvider removes a provider by type identifier.
|
||||||
if cfg == nil {
|
func UnregisterProvider(typ string) {
|
||||||
return nil, fmt.Errorf("access: nil provider config")
|
normalizedType := strings.TrimSpace(typ)
|
||||||
|
if normalizedType == "" {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
registryMu.RLock()
|
registryMu.Lock()
|
||||||
factory, ok := registry[cfg.Type]
|
if _, exists := registry[normalizedType]; !exists {
|
||||||
registryMu.RUnlock()
|
registryMu.Unlock()
|
||||||
if !ok {
|
return
|
||||||
return nil, fmt.Errorf("access: provider type %q is not registered", cfg.Type)
|
|
||||||
}
|
}
|
||||||
provider, err := factory(cfg, root)
|
delete(registry, normalizedType)
|
||||||
if err != nil {
|
for index := range order {
|
||||||
return nil, fmt.Errorf("access: failed to build provider %q: %w", cfg.Name, err)
|
if order[index] != normalizedType {
|
||||||
}
|
|
||||||
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 == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
provider, err := BuildProvider(providerCfg, root)
|
order = append(order[:index], order[index+1:]...)
|
||||||
if err != nil {
|
break
|
||||||
return nil, err
|
}
|
||||||
|
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)
|
providers = append(providers, provider)
|
||||||
}
|
}
|
||||||
if len(providers) == 0 {
|
registryMu.RUnlock()
|
||||||
if inline := config.MakeInlineAPIKeyProvider(root.APIKeys); inline != nil {
|
return providers
|
||||||
provider, err := BuildProvider(inline, root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
providers = append(providers, provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return providers, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
47
sdk/access/types.go
Normal file
47
sdk/access/types.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
configaccess "github.com/router-for-me/CLIProxyAPI/v6/internal/access/config_access"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/api"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/api"
|
||||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
||||||
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
||||||
@@ -186,11 +187,8 @@ func (b *Builder) Build() (*Service, error) {
|
|||||||
accessManager = sdkaccess.NewManager()
|
accessManager = sdkaccess.NewManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
providers, err := sdkaccess.BuildProviders(&b.cfg.SDKConfig)
|
configaccess.Register(&b.cfg.SDKConfig)
|
||||||
if err != nil {
|
accessManager.SetProviders(sdkaccess.RegisteredProviders())
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
accessManager.SetProviders(providers)
|
|
||||||
|
|
||||||
coreManager := b.coreManager
|
coreManager := b.coreManager
|
||||||
if coreManager == nil {
|
if coreManager == nil {
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ package config
|
|||||||
import internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
import internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
|
||||||
type SDKConfig = internalconfig.SDKConfig
|
type SDKConfig = internalconfig.SDKConfig
|
||||||
type AccessConfig = internalconfig.AccessConfig
|
|
||||||
type AccessProvider = internalconfig.AccessProvider
|
|
||||||
|
|
||||||
type Config = internalconfig.Config
|
type Config = internalconfig.Config
|
||||||
|
|
||||||
@@ -34,15 +32,9 @@ type OpenAICompatibilityModel = internalconfig.OpenAICompatibilityModel
|
|||||||
type TLS = internalconfig.TLSConfig
|
type TLS = internalconfig.TLSConfig
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AccessProviderTypeConfigAPIKey = internalconfig.AccessProviderTypeConfigAPIKey
|
DefaultPanelGitHubRepository = internalconfig.DefaultPanelGitHubRepository
|
||||||
DefaultAccessProviderName = internalconfig.DefaultAccessProviderName
|
|
||||||
DefaultPanelGitHubRepository = internalconfig.DefaultPanelGitHubRepository
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeInlineAPIKeyProvider(keys []string) *AccessProvider {
|
|
||||||
return internalconfig.MakeInlineAPIKeyProvider(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(configFile string) (*Config, error) { return internalconfig.LoadConfig(configFile) }
|
func LoadConfig(configFile string) (*Config, error) { return internalconfig.LoadConfig(configFile) }
|
||||||
|
|
||||||
func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user