Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
90
backend/sdk/access/errors.go
Normal file
90
backend/sdk/access/errors.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package access
|
||||
|
||||
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
|
||||
}
|
||||
88
backend/sdk/access/manager.go
Normal file
88
backend/sdk/access/manager.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Manager coordinates authentication providers.
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
providers []Provider
|
||||
}
|
||||
|
||||
// NewManager constructs an empty manager.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{}
|
||||
}
|
||||
|
||||
// SetProviders replaces the active provider list.
|
||||
func (m *Manager) SetProviders(providers []Provider) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
cloned := make([]Provider, len(providers))
|
||||
copy(cloned, providers)
|
||||
m.mu.Lock()
|
||||
m.providers = cloned
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Providers returns a snapshot of the active providers.
|
||||
func (m *Manager) Providers() []Provider {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
snapshot := make([]Provider, len(m.providers))
|
||||
copy(snapshot, m.providers)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// Authenticate evaluates providers until one succeeds.
|
||||
func (m *Manager) Authenticate(ctx context.Context, r *http.Request) (*Result, *AuthError) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
providers := m.Providers()
|
||||
if len(providers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
missing bool
|
||||
invalid bool
|
||||
)
|
||||
|
||||
for _, provider := range providers {
|
||||
if provider == nil {
|
||||
continue
|
||||
}
|
||||
res, authErr := provider.Authenticate(ctx, r)
|
||||
if authErr == nil {
|
||||
return res, nil
|
||||
}
|
||||
if IsAuthErrorCode(authErr, AuthErrorCodeNotHandled) {
|
||||
continue
|
||||
}
|
||||
if IsAuthErrorCode(authErr, AuthErrorCodeNoCredentials) {
|
||||
missing = true
|
||||
continue
|
||||
}
|
||||
if IsAuthErrorCode(authErr, AuthErrorCodeInvalidCredential) {
|
||||
invalid = true
|
||||
continue
|
||||
}
|
||||
return nil, authErr
|
||||
}
|
||||
|
||||
if invalid {
|
||||
return nil, NewInvalidCredentialError()
|
||||
}
|
||||
if missing {
|
||||
return nil, NewNoCredentialsError()
|
||||
}
|
||||
return nil, NewNoCredentialsError()
|
||||
}
|
||||
105
backend/sdk/access/registry.go
Normal file
105
backend/sdk/access/registry.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Provider validates credentials for incoming requests.
|
||||
type Provider interface {
|
||||
Identifier() string
|
||||
Authenticate(ctx context.Context, r *http.Request) (*Result, *AuthError)
|
||||
}
|
||||
|
||||
// Result conveys authentication outcome.
|
||||
type Result struct {
|
||||
Provider string
|
||||
Principal string
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
var (
|
||||
registryMu sync.RWMutex
|
||||
registry = make(map[string]Provider)
|
||||
order []string
|
||||
exclusiveProvider string
|
||||
)
|
||||
|
||||
// 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()
|
||||
if _, exists := registry[normalizedType]; !exists {
|
||||
order = append(order, normalizedType)
|
||||
}
|
||||
registry[normalizedType] = provider
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// UnregisterProvider removes a provider by type identifier.
|
||||
func UnregisterProvider(typ string) {
|
||||
normalizedType := strings.TrimSpace(typ)
|
||||
if normalizedType == "" {
|
||||
return
|
||||
}
|
||||
registryMu.Lock()
|
||||
if _, exists := registry[normalizedType]; !exists {
|
||||
registryMu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(registry, normalizedType)
|
||||
for index := range order {
|
||||
if order[index] != normalizedType {
|
||||
continue
|
||||
}
|
||||
order = append(order[:index], order[index+1:]...)
|
||||
break
|
||||
}
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// SetExclusiveProvider restricts RegisteredProviders to a single provider key when present.
|
||||
func SetExclusiveProvider(typ string) {
|
||||
normalizedType := strings.TrimSpace(typ)
|
||||
registryMu.Lock()
|
||||
exclusiveProvider = normalizedType
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// ClearExclusiveProvider removes any active provider restriction.
|
||||
func ClearExclusiveProvider() {
|
||||
registryMu.Lock()
|
||||
exclusiveProvider = ""
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// RegisteredProviders returns the global provider instances in registration order.
|
||||
func RegisteredProviders() []Provider {
|
||||
registryMu.RLock()
|
||||
if len(order) == 0 {
|
||||
registryMu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
if exclusiveProvider != "" {
|
||||
if provider, exists := registry[exclusiveProvider]; exists && provider != nil {
|
||||
registryMu.RUnlock()
|
||||
return []Provider{provider}
|
||||
}
|
||||
}
|
||||
providers := make([]Provider, 0, len(order))
|
||||
for _, providerType := range order {
|
||||
provider, exists := registry[providerType]
|
||||
if !exists || provider == nil {
|
||||
continue
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
registryMu.RUnlock()
|
||||
return providers
|
||||
}
|
||||
81
backend/sdk/access/registry_test.go
Normal file
81
backend/sdk/access/registry_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testProvider struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (p testProvider) Identifier() string {
|
||||
return p.id
|
||||
}
|
||||
|
||||
func (p testProvider) Authenticate(context.Context, *http.Request) (*Result, *AuthError) {
|
||||
return &Result{Provider: p.id, Principal: p.id}, nil
|
||||
}
|
||||
|
||||
func TestRegisteredProvidersReturnsOnlyExclusiveProvider(t *testing.T) {
|
||||
UnregisterProvider("test-a")
|
||||
UnregisterProvider("test-b")
|
||||
ClearExclusiveProvider()
|
||||
defer UnregisterProvider("test-a")
|
||||
defer UnregisterProvider("test-b")
|
||||
defer ClearExclusiveProvider()
|
||||
|
||||
RegisterProvider("test-a", testProvider{id: "test-a"})
|
||||
RegisterProvider("test-b", testProvider{id: "test-b"})
|
||||
SetExclusiveProvider("test-b")
|
||||
|
||||
providers := RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != "test-b" {
|
||||
t.Fatalf("RegisteredProviders()[0] = %q, want test-b", providers[0].Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredProvidersRestoresAllProvidersAfterExclusiveCleared(t *testing.T) {
|
||||
UnregisterProvider("test-a")
|
||||
UnregisterProvider("test-b")
|
||||
ClearExclusiveProvider()
|
||||
defer UnregisterProvider("test-a")
|
||||
defer UnregisterProvider("test-b")
|
||||
defer ClearExclusiveProvider()
|
||||
|
||||
RegisterProvider("test-a", testProvider{id: "test-a"})
|
||||
RegisterProvider("test-b", testProvider{id: "test-b"})
|
||||
SetExclusiveProvider("test-b")
|
||||
ClearExclusiveProvider()
|
||||
|
||||
providers := RegisteredProviders()
|
||||
if len(providers) != 2 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 2", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != "test-a" || providers[1].Identifier() != "test-b" {
|
||||
t.Fatalf("RegisteredProviders() = [%q, %q], want [test-a, test-b]", providers[0].Identifier(), providers[1].Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisteredProvidersIgnoresStaleExclusiveProvider(t *testing.T) {
|
||||
UnregisterProvider("test-a")
|
||||
UnregisterProvider("missing")
|
||||
ClearExclusiveProvider()
|
||||
defer UnregisterProvider("test-a")
|
||||
defer ClearExclusiveProvider()
|
||||
|
||||
RegisterProvider("test-a", testProvider{id: "test-a"})
|
||||
SetExclusiveProvider("missing")
|
||||
|
||||
providers := RegisteredProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("RegisteredProviders() len = %d, want 1", len(providers))
|
||||
}
|
||||
if providers[0].Identifier() != "test-a" {
|
||||
t.Fatalf("RegisteredProviders()[0] = %q, want test-a", providers[0].Identifier())
|
||||
}
|
||||
}
|
||||
47
backend/sdk/access/types.go
Normal file
47
backend/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
|
||||
}
|
||||
Loading…
Reference in a new issue