refactor(sdk): simplify provider lifecycle and registration logic

This commit is contained in:
Luis Pater
2026-02-10 15:38:03 +08:00
parent 896de027cc
commit 0040d78496
15 changed files with 391 additions and 534 deletions

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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
}

47
sdk/access/types.go Normal file
View 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
}