Add redis support for distributed caching (#83)

* Add redis support for distributed caching

* Move towards the self-provided Redis connection pool and RESP protocol implementation.
Official redis client library won't work with yaegi.

* fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* ... and another all nighter.

* fixup! ... and another all nighter.

* fixup! fixup! ... and another all nighter.

* fixup! fixup! fixup! ... and another all nighter.

* Resolve issue #85 by adding ability to set custom claims in JWT tokens

* Remove redundant validation in auth middleware ( issue #89 )

* Add ability to set cookie prefix for session cookies ( #87 )

* fixup! Add ability to set cookie prefix for session cookies ( #87 )

* Add ability to set cookie max age - issue #91

* Potential fix for code scanning alert no. 10: Size computation for allocation may overflow

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fixup! Merge main into 0.8.0-redis: resolve conflicts

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
2025-11-30 02:18:46 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent 5fcbd54955
commit e64fc7f730
318 changed files with 100989 additions and 948 deletions
+320
View File
@@ -0,0 +1,320 @@
// Package compat provides backward compatibility layer during refactoring
package compat
import (
"fmt"
"reflect"
"sync"
)
// CompatibilityLayer provides backward compatibility during the migration
type CompatibilityLayer struct {
mappings map[string]string // old path -> new path
converters map[string]Converter
deprecations map[string]string // deprecated field -> warning message
mu sync.RWMutex
}
// Converter is a function that converts old value format to new format
type Converter func(oldValue interface{}) (newValue interface{}, err error)
// Global compatibility layer instance
var (
layer *CompatibilityLayer
layerOnce sync.Once
)
// GetLayer returns the global compatibility layer instance
func GetLayer() *CompatibilityLayer {
layerOnce.Do(func() {
layer = &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
layer.initialize()
})
return layer
}
// initialize sets up default compatibility mappings
func (c *CompatibilityLayer) initialize() {
// Configuration path mappings (old -> new)
c.RegisterMapping("ProviderURL", "Provider.IssuerURL")
c.RegisterMapping("ClientID", "Provider.ClientID")
c.RegisterMapping("ClientSecret", "Provider.ClientSecret")
c.RegisterMapping("CallbackURL", "Provider.RedirectURL")
c.RegisterMapping("LogoutURL", "Provider.LogoutURL")
c.RegisterMapping("SessionEncryptionKey", "Session.EncryptionKey")
c.RegisterMapping("Scopes", "Provider.Scopes")
c.RegisterMapping("RateLimit", "Middleware.RateLimit")
c.RegisterMapping("RefreshGracePeriodSeconds", "Token.RefreshGracePeriod")
// Redis configuration mappings
c.RegisterMapping("RedisAddr", "Redis.Addresses[0]")
c.RegisterMapping("RedisPassword", "Redis.Password")
c.RegisterMapping("RedisDB", "Redis.DB")
// Session configuration mappings
c.RegisterMapping("SessionName", "Session.Name")
c.RegisterMapping("SessionMaxAge", "Session.MaxAge")
c.RegisterMapping("SessionSecret", "Session.Secret")
c.RegisterMapping("SessionChunkSize", "Session.ChunkSize")
// Security configuration mappings
c.RegisterMapping("ForceHTTPS", "Security.ForceHTTPS")
c.RegisterMapping("EnablePKCE", "Security.EnablePKCE")
c.RegisterMapping("AllowedUsers", "Security.AllowedUsers")
c.RegisterMapping("AllowedUserDomains", "Security.AllowedUserDomains")
c.RegisterMapping("AllowedRolesAndGroups", "Security.AllowedRolesAndGroups")
c.RegisterMapping("ExcludedURLs", "Security.ExcludedURLs")
// Register converters for complex transformations
c.RegisterConverter("RefreshGracePeriodSeconds", func(oldValue interface{}) (interface{}, error) {
// Convert seconds (int) to duration string
if seconds, ok := oldValue.(int); ok {
return fmt.Sprintf("%ds", seconds), nil
}
return oldValue, nil
})
// Register deprecations
c.RegisterDeprecation("LogLevel", "LogLevel is deprecated, use Logging.Level instead")
c.RegisterDeprecation("HTTPClient", "HTTPClient is deprecated, configure via Transport settings")
}
// RegisterMapping registers a field mapping from old to new path
func (c *CompatibilityLayer) RegisterMapping(oldPath, newPath string) {
c.mu.Lock()
defer c.mu.Unlock()
c.mappings[oldPath] = newPath
}
// RegisterConverter registers a value converter for a field
func (c *CompatibilityLayer) RegisterConverter(field string, converter Converter) {
c.mu.Lock()
defer c.mu.Unlock()
c.converters[field] = converter
}
// RegisterDeprecation registers a deprecation warning for a field
func (c *CompatibilityLayer) RegisterDeprecation(field, message string) {
c.mu.Lock()
defer c.mu.Unlock()
c.deprecations[field] = message
}
// GetMapping returns the new path for an old configuration path
func (c *CompatibilityLayer) GetMapping(oldPath string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
newPath, exists := c.mappings[oldPath]
return newPath, exists
}
// Convert applies conversion logic to a value
func (c *CompatibilityLayer) Convert(field string, value interface{}) (interface{}, error) {
c.mu.RLock()
converter, exists := c.converters[field]
c.mu.RUnlock()
if !exists {
return value, nil
}
return converter(value)
}
// CheckDeprecation checks if a field is deprecated and returns warning message
func (c *CompatibilityLayer) CheckDeprecation(field string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
message, deprecated := c.deprecations[field]
return message, deprecated
}
// MigrateMap migrates an old configuration map to new structure
func (c *CompatibilityLayer) MigrateMap(oldConfig map[string]interface{}) (map[string]interface{}, []string) {
newConfig := make(map[string]interface{})
warnings := []string{}
for key, value := range oldConfig {
// Check for deprecation
if warning, deprecated := c.CheckDeprecation(key); deprecated {
warnings = append(warnings, warning)
}
// Get new path
newPath, hasMappming := c.GetMapping(key)
if !hasMappming {
// No mapping, use as-is
newConfig[key] = value
continue
}
// Apply converter if exists
convertedValue, err := c.Convert(key, value)
if err != nil {
warnings = append(warnings, fmt.Sprintf("Failed to convert %s: %v", key, err))
convertedValue = value
}
// Set value at new path
setNestedValue(newConfig, newPath, convertedValue)
}
return newConfig, warnings
}
// setNestedValue sets a value in a nested map structure using dot notation
func setNestedValue(m map[string]interface{}, path string, value interface{}) {
keys := splitPath(path)
if len(keys) == 0 {
return
}
current := m
for i := 0; i < len(keys)-1; i++ {
key := keys[i]
// Check if this key has array notation
if isArrayPath(key) {
// Handle array notation (e.g., "Addresses[0]")
continue // Skip array handling for now, will be handled in actual migration
}
if _, exists := current[key]; !exists {
current[key] = make(map[string]interface{})
}
// Ensure it's a map
if next, ok := current[key].(map[string]interface{}); ok {
current = next
} else {
// Can't traverse further, create new map
newMap := make(map[string]interface{})
current[key] = newMap
current = newMap
}
}
// Set the final value
finalKey := keys[len(keys)-1]
current[finalKey] = value
}
// splitPath splits a configuration path into segments
func splitPath(path string) []string {
segments := []string{}
current := ""
for i := 0; i < len(path); i++ {
if path[i] == '.' {
if current != "" {
segments = append(segments, current)
current = ""
}
} else {
current += string(path[i])
}
}
if current != "" {
segments = append(segments, current)
}
return segments
}
// isArrayPath checks if a path segment contains array notation
func isArrayPath(segment string) bool {
for _, char := range segment {
if char == '[' {
return true
}
}
return false
}
// ConfigAdapter provides an adapter interface for old code to work with new config
type ConfigAdapter struct {
newConfig interface{}
oldPaths map[string]func() interface{}
mu sync.RWMutex
}
// NewConfigAdapter creates a new configuration adapter
func NewConfigAdapter(newConfig interface{}) *ConfigAdapter {
adapter := &ConfigAdapter{
newConfig: newConfig,
oldPaths: make(map[string]func() interface{}),
}
return adapter
}
// RegisterGetter registers a getter function for an old path
func (a *ConfigAdapter) RegisterGetter(oldPath string, getter func() interface{}) {
a.mu.Lock()
defer a.mu.Unlock()
a.oldPaths[oldPath] = getter
}
// Get retrieves a value using old path notation
func (a *ConfigAdapter) Get(oldPath string) (interface{}, bool) {
a.mu.RLock()
getter, exists := a.oldPaths[oldPath]
a.mu.RUnlock()
if !exists {
// Try to get from new config using reflection
return a.getFromNewConfig(oldPath)
}
return getter(), true
}
// getFromNewConfig attempts to retrieve value from new config using reflection
func (a *ConfigAdapter) getFromNewConfig(path string) (interface{}, bool) {
// Check if there's a mapping for this path
compat := GetLayer()
if newPath, hasMappming := compat.GetMapping(path); hasMappming {
return a.getNestedField(newPath)
}
// Try direct access
return a.getNestedField(path)
}
// getNestedField retrieves a nested field value using reflection
func (a *ConfigAdapter) getNestedField(path string) (interface{}, bool) {
segments := splitPath(path)
if len(segments) == 0 {
return nil, false
}
v := reflect.ValueOf(a.newConfig)
// Dereference pointer if needed
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
for _, segment := range segments {
if v.Kind() != reflect.Struct {
return nil, false
}
field := v.FieldByName(segment)
if !field.IsValid() {
return nil, false
}
v = field
}
if v.IsValid() && v.CanInterface() {
return v.Interface(), true
}
return nil, false
}
+495
View File
@@ -0,0 +1,495 @@
//go:build !yaegi
package compat
import (
"sync"
"testing"
)
func TestGetLayer_Singleton(t *testing.T) {
// Reset global state
layerOnce = sync.Once{}
layer = nil
layer1 := GetLayer()
layer2 := GetLayer()
if layer1 != layer2 {
t.Error("Expected GetLayer to return same instance")
}
}
func TestGetLayer_Initialize(t *testing.T) {
// Reset global state
layerOnce = sync.Once{}
layer = nil
l := GetLayer()
// Check default mappings exist
if _, exists := l.GetMapping("ProviderURL"); !exists {
t.Error("Expected ProviderURL mapping to exist")
}
if _, exists := l.GetMapping("ClientID"); !exists {
t.Error("Expected ClientID mapping to exist")
}
// Check deprecations exist
if _, deprecated := l.CheckDeprecation("LogLevel"); !deprecated {
t.Error("Expected LogLevel to be marked deprecated")
}
}
func TestRegisterMapping(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
l.RegisterMapping("OldField", "New.Field")
newPath, exists := l.GetMapping("OldField")
if !exists {
t.Error("Expected mapping to exist")
}
if newPath != "New.Field" {
t.Errorf("Expected 'New.Field', got '%s'", newPath)
}
}
func TestRegisterConverter(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
converter := func(oldValue interface{}) (interface{}, error) {
if str, ok := oldValue.(string); ok {
return str + "_converted", nil
}
return oldValue, nil
}
l.RegisterConverter("TestField", converter)
result, err := l.Convert("TestField", "test")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != "test_converted" {
t.Errorf("Expected 'test_converted', got '%v'", result)
}
}
func TestConvert_NoConverter(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
// No converter registered
result, err := l.Convert("UnknownField", "value")
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result != "value" {
t.Error("Expected original value when no converter exists")
}
}
func TestRegisterDeprecation(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
l.RegisterDeprecation("OldField", "This field is deprecated")
message, deprecated := l.CheckDeprecation("OldField")
if !deprecated {
t.Error("Expected field to be deprecated")
}
if message != "This field is deprecated" {
t.Errorf("Expected deprecation message, got '%s'", message)
}
}
func TestCheckDeprecation_NotDeprecated(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
_, deprecated := l.CheckDeprecation("NewField")
if deprecated {
t.Error("Expected field not to be deprecated")
}
}
func TestMigrateMap_BasicMapping(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
l.RegisterMapping("OldField", "New.Field")
oldConfig := map[string]interface{}{
"OldField": "value123",
}
newConfig, warnings := l.MigrateMap(oldConfig)
if len(warnings) != 0 {
t.Errorf("Expected no warnings, got %d", len(warnings))
}
// Check nested structure
if newMap, ok := newConfig["New"].(map[string]interface{}); ok {
if val, exists := newMap["Field"]; !exists || val != "value123" {
t.Errorf("Expected nested field value 'value123', got %v", val)
}
} else {
t.Error("Expected nested map structure")
}
}
func TestMigrateMap_WithDeprecation(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
l.RegisterMapping("DeprecatedField", "New.Field")
l.RegisterDeprecation("DeprecatedField", "Field is deprecated")
oldConfig := map[string]interface{}{
"DeprecatedField": "value",
}
_, warnings := l.MigrateMap(oldConfig)
if len(warnings) != 1 {
t.Errorf("Expected 1 warning, got %d", len(warnings))
}
if warnings[0] != "Field is deprecated" {
t.Errorf("Expected deprecation warning, got '%s'", warnings[0])
}
}
func TestMigrateMap_WithConverter(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
l.RegisterMapping("Seconds", "Duration")
l.RegisterConverter("Seconds", func(oldValue interface{}) (interface{}, error) {
if seconds, ok := oldValue.(int); ok {
return seconds * 1000, nil // Convert to milliseconds
}
return oldValue, nil
})
oldConfig := map[string]interface{}{
"Seconds": 60,
}
newConfig, _ := l.MigrateMap(oldConfig)
if val, ok := newConfig["Duration"]; !ok || val != 60000 {
t.Errorf("Expected Duration to be 60000, got %v", val)
}
}
func TestMigrateMap_NoMapping(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
oldConfig := map[string]interface{}{
"UnmappedField": "value",
}
newConfig, _ := l.MigrateMap(oldConfig)
if val, ok := newConfig["UnmappedField"]; !ok || val != "value" {
t.Error("Expected unmapped field to be copied as-is")
}
}
func TestSplitPath(t *testing.T) {
tests := []struct {
path string
expected []string
}{
{"Simple", []string{"Simple"}},
{"Nested.Path", []string{"Nested", "Path"}},
{"Deep.Nested.Path", []string{"Deep", "Nested", "Path"}},
{"", []string{}},
{"Single", []string{"Single"}},
}
for _, tt := range tests {
result := splitPath(tt.path)
if len(result) != len(tt.expected) {
t.Errorf("Path '%s': expected %d segments, got %d", tt.path, len(tt.expected), len(result))
continue
}
for i, segment := range result {
if segment != tt.expected[i] {
t.Errorf("Path '%s': segment %d expected '%s', got '%s'", tt.path, i, tt.expected[i], segment)
}
}
}
}
func TestIsArrayPath(t *testing.T) {
tests := []struct {
segment string
expected bool
}{
{"Addresses[0]", true},
{"Items[5]", true},
{"Simple", false},
{"NoArray", false},
{"[start", true},
}
for _, tt := range tests {
result := isArrayPath(tt.segment)
if result != tt.expected {
t.Errorf("Segment '%s': expected %v, got %v", tt.segment, tt.expected, result)
}
}
}
func TestSetNestedValue_SingleLevel(t *testing.T) {
m := make(map[string]interface{})
setNestedValue(m, "Field", "value")
if val, ok := m["Field"]; !ok || val != "value" {
t.Error("Expected single level field to be set")
}
}
func TestSetNestedValue_MultiLevel(t *testing.T) {
m := make(map[string]interface{})
setNestedValue(m, "Parent.Child", "value")
parent, ok := m["Parent"].(map[string]interface{})
if !ok {
t.Fatal("Expected Parent to be a map")
}
if val, ok := parent["Child"]; !ok || val != "value" {
t.Error("Expected nested field to be set")
}
}
func TestSetNestedValue_DeepNesting(t *testing.T) {
m := make(map[string]interface{})
setNestedValue(m, "Level1.Level2.Level3", "deep_value")
level1, ok := m["Level1"].(map[string]interface{})
if !ok {
t.Fatal("Expected Level1 to be a map")
}
level2, ok := level1["Level2"].(map[string]interface{})
if !ok {
t.Fatal("Expected Level2 to be a map")
}
if val, ok := level2["Level3"]; !ok || val != "deep_value" {
t.Error("Expected deeply nested field to be set")
}
}
// ConfigAdapter tests
func TestNewConfigAdapter(t *testing.T) {
config := map[string]interface{}{"key": "value"}
adapter := NewConfigAdapter(config)
if adapter == nil {
t.Fatal("Expected adapter to be created")
}
if adapter.newConfig == nil {
t.Error("Expected config to be stored")
}
}
func TestConfigAdapter_RegisterGetter(t *testing.T) {
adapter := NewConfigAdapter(nil)
called := false
adapter.RegisterGetter("TestPath", func() interface{} {
called = true
return "test_value"
})
val, exists := adapter.Get("TestPath")
if !exists {
t.Error("Expected getter to exist")
}
if val != "test_value" {
t.Errorf("Expected 'test_value', got %v", val)
}
if !called {
t.Error("Expected getter function to be called")
}
}
type TestConfig struct {
Provider struct {
IssuerURL string
ClientID string
}
Session struct {
EncryptionKey string
}
}
func TestConfigAdapter_GetNestedField(t *testing.T) {
config := &TestConfig{}
config.Provider.IssuerURL = "https://test.com"
config.Provider.ClientID = "test-client"
config.Session.EncryptionKey = "secret123"
adapter := NewConfigAdapter(config)
// Test nested field access
val, exists := adapter.getNestedField("Provider.IssuerURL")
if !exists {
t.Error("Expected field to exist")
}
if val != "https://test.com" {
t.Errorf("Expected 'https://test.com', got %v", val)
}
// Test another nested field
val2, exists2 := adapter.getNestedField("Provider.ClientID")
if !exists2 || val2 != "test-client" {
t.Error("Expected ClientID to be accessible")
}
// Test non-existent field
_, exists3 := adapter.getNestedField("NonExistent.Field")
if exists3 {
t.Error("Expected non-existent field to return false")
}
}
// Race condition tests
func TestCompatibilityLayer_ConcurrentAccess(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
var wg sync.WaitGroup
// Concurrent registrations
for i := 0; i < 100; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
l.RegisterMapping(string(rune('A'+idx%26)), "New.Field")
}(i)
}
// Concurrent reads
for i := 0; i < 100; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_, _ = l.GetMapping(string(rune('A' + idx%26)))
}(i)
}
wg.Wait()
}
func TestCompatibilityLayer_ConcurrentMigrate(t *testing.T) {
l := &CompatibilityLayer{
mappings: make(map[string]string),
converters: make(map[string]Converter),
deprecations: make(map[string]string),
}
l.RegisterMapping("OldField", "New.Field")
var wg sync.WaitGroup
// Concurrent migrations
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
oldConfig := map[string]interface{}{
"OldField": "value",
}
_, _ = l.MigrateMap(oldConfig)
}()
}
wg.Wait()
}
func TestConfigAdapter_ConcurrentAccess(t *testing.T) {
config := &TestConfig{}
config.Provider.IssuerURL = "https://test.com"
adapter := NewConfigAdapter(config)
var wg sync.WaitGroup
// Concurrent getter registrations
for i := 0; i < 50; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
path := string(rune('A' + idx%26))
adapter.RegisterGetter(path, func() interface{} {
return "value"
})
}(i)
}
// Concurrent gets
for i := 0; i < 50; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
path := string(rune('A' + idx%26))
_, _ = adapter.Get(path)
}(i)
}
wg.Wait()
}