diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml index fedc61bc..188747ac 100644 --- a/.github/workflows/agent.yml +++ b/.github/workflows/agent.yml @@ -9,7 +9,7 @@ on: branches: [ master ] env: - GIMME_GO_VERSION: 1.24.0 + GIMME_GO_VERSION: 1.23.0 GIMME_OS: linux GIMME_ARCH: amd64 @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.24.0' + go-version: '1.23.0' check-latest: true - name: fmt run: | @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.24.0' + go-version: '1.23.0' check-latest: true - name: coveralls id: coveralls @@ -67,7 +67,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.24.0' + go-version: '1.23.0' check-latest: true - name: sourceclear env: @@ -102,7 +102,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.24' + go-version: '1.23.0' check-latest: true - name: Set up Python 3.9 uses: actions/setup-python@v3 @@ -132,7 +132,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-go@v3 with: - go-version: '1.24.0' + go-version: '1.23.0' check-latest: true - name: Get the version id: get_version @@ -164,7 +164,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-go@v3 with: - go-version: '1.24.0' + go-version: '1.23.0' check-latest: true - uses: actions/checkout@v2 with: @@ -235,7 +235,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.24.0' + go-version: '1.23.0' check-latest: true - uses: actions/checkout@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c3ae79..68932d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [4.2.1] - January 3, 2025 + +### Fixed + +* Fixed decision notifications not working with secure environment SDK keys +* Added documentation for Redis channel naming pattern in config.yaml + ## [4.2.0] - July 17, 2025 ### New Features diff --git a/config.yaml b/config.yaml index 6dc98b31..283d2890 100644 --- a/config.yaml +++ b/config.yaml @@ -252,6 +252,10 @@ synchronization: host: "redis.demo.svc:6379" password: "" database: 0 + ## channel: "optimizely-sync" # Base channel name (NOT currently parsed - uses hardcoded default) + ## Agent publishes to channels: "optimizely-sync-{sdk_key}" + ## For external Redis clients: Subscribe "optimizely-sync-{sdk_key}" or PSubscribe "optimizely-sync-*" + ## Note: Channel configuration parsing is a known bug - planned for future release ## if notification synchronization is enabled, then the active notification event-stream API ## will get the notifications from available replicas notification: diff --git a/go.mod b/go.mod index fbfab95d..9d47dac5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/optimizely/agent -go 1.21.0 +go 1.23 require ( github.com/go-chi/chi/v5 v5.0.8 diff --git a/pkg/handlers/notification.go b/pkg/handlers/notification.go index ce2d1515..c1bbcdcd 100644 --- a/pkg/handlers/notification.go +++ b/pkg/handlers/notification.go @@ -110,6 +110,10 @@ func NotificationEventStreamHandler(notificationReceiverFn NotificationReceiverF notify := r.Context().Done() sdkKey := r.Header.Get(middleware.OptlySDKHeader) + // Parse out the SDK key if it includes a secure token (format: sdkKey:apiKey) + if idx := strings.Index(sdkKey, ":"); idx != -1 { + sdkKey = sdkKey[:idx] + } ctx := context.WithValue(r.Context(), SDKKey, sdkKey) dataChan, err := notificationReceiverFn(context.WithValue(ctx, LoggerKey, middleware.GetLogger(r))) diff --git a/pkg/handlers/notification_test.go b/pkg/handlers/notification_test.go index 85eda15c..b1f0ee0f 100644 --- a/pkg/handlers/notification_test.go +++ b/pkg/handlers/notification_test.go @@ -503,3 +503,127 @@ func getMockNotificationReceiver(conf config.SyncConfig, returnError bool, msg . return dataChan, nil } } + +func (suite *NotificationTestSuite) TestSecureTokenParsing() { + testCases := []struct { + name string + sdkKeyHeader string + expectedSDKKey string + description string + }{ + { + name: "StandardSDKKey", + sdkKeyHeader: "normal_sdk_key_123", + expectedSDKKey: "normal_sdk_key_123", + description: "Standard SDK key without secure token should remain unchanged", + }, + { + name: "SecureTokenFormat", + sdkKeyHeader: "sdk_key_123:api_key_456", + expectedSDKKey: "sdk_key_123", + description: "SDK key with secure token should extract only the SDK key portion", + }, + { + name: "MultipleColons", + sdkKeyHeader: "sdk_key:api_key:extra_part", + expectedSDKKey: "sdk_key", + description: "Multiple colons should split at first colon only", + }, + { + name: "EmptySDKKey", + sdkKeyHeader: ":api_key_456", + expectedSDKKey: "", + description: "Empty SDK key portion should result in empty string", + }, + { + name: "EmptyAPIKey", + sdkKeyHeader: "sdk_key_123:", + expectedSDKKey: "sdk_key_123", + description: "Empty API key portion should extract SDK key", + }, + { + name: "ColonOnly", + sdkKeyHeader: ":", + expectedSDKKey: "", + description: "Colon only should result in empty SDK key", + }, + { + name: "EmptyHeader", + sdkKeyHeader: "", + expectedSDKKey: "", + description: "Empty header should remain empty", + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Create a mock notification receiver that captures the SDK key + var capturedSDKKey string + mockReceiver := func(ctx context.Context) (<-chan syncer.Event, error) { + capturedSDKKey = ctx.Value(SDKKey).(string) + dataChan := make(chan syncer.Event) + // Don't close the channel - let the test timeout handle cleanup + return dataChan, nil + } + + // Setup handler + suite.mux.Get("/test-notifications", NotificationEventStreamHandler(mockReceiver)) + + // Create request with SDK key header + req := httptest.NewRequest("GET", "/test-notifications", nil) + if tc.sdkKeyHeader != "" { + req.Header.Set(middleware.OptlySDKHeader, tc.sdkKeyHeader) + } + + // Create a context with a short timeout to prevent hanging + ctx, cancel := context.WithTimeout(req.Context(), 100*time.Millisecond) + defer cancel() + req = req.WithContext(ctx) + + rec := httptest.NewRecorder() + + // Execute request + suite.mux.ServeHTTP(rec, req) + + // Verify SDK key was parsed correctly + suite.Equal(tc.expectedSDKKey, capturedSDKKey, tc.description) + }) + } +} + +func (suite *NotificationTestSuite) TestSecureTokenParsingIntegration() { + // Test that secure token parsing works end-to-end with actual notification flow + + // Create a mock receiver that verifies the SDK key context + mockReceiver := func(ctx context.Context) (<-chan syncer.Event, error) { + sdkKey := ctx.Value(SDKKey).(string) + suite.Equal("test_sdk_key", sdkKey, "SDK key should be extracted from secure token format") + + dataChan := make(chan syncer.Event, 1) + // Send a test event + dataChan <- syncer.Event{ + Type: notification.Decision, + Message: map[string]string{"test": "event"}, + } + close(dataChan) + return dataChan, nil + } + + suite.mux.Get("/test-secure-notifications", NotificationEventStreamHandler(mockReceiver)) + + // Test with secure token format + req := httptest.NewRequest("GET", "/test-secure-notifications", nil) + req.Header.Set(middleware.OptlySDKHeader, "test_sdk_key:test_api_key") + rec := httptest.NewRecorder() + + // Create cancelable context for SSE + ctx, cancel := context.WithTimeout(req.Context(), 1*time.Second) + defer cancel() + + suite.mux.ServeHTTP(rec, req.WithContext(ctx)) + + // Verify response + suite.Equal(http.StatusOK, rec.Code) + response := rec.Body.String() + suite.Contains(response, `data: {"test":"event"}`, "Should receive the test event") +} diff --git a/scripts/dockerfiles/Dockerfile.alpine b/scripts/dockerfiles/Dockerfile.alpine index 08c4fd80..63773b7f 100644 --- a/scripts/dockerfiles/Dockerfile.alpine +++ b/scripts/dockerfiles/Dockerfile.alpine @@ -1,5 +1,5 @@ ARG GO_VERSION -FROM golang:$GO_VERSION-alpine3.21 as builder +FROM golang:$GO_VERSION-alpine3.20 as builder # hadolint ignore=DL3018 RUN addgroup -S agentgroup && adduser -S agentuser -G agentgroup RUN apk add --no-cache make gcc libc-dev git curl @@ -7,7 +7,7 @@ WORKDIR /go/src/github.com/optimizely/agent COPY . . RUN make setup build -FROM alpine:3.21 +FROM alpine:3.20 RUN apk add --no-cache ca-certificates COPY --from=builder /go/src/github.com/optimizely/agent/bin/optimizely /optimizely COPY --from=builder /etc/passwd /etc/passwd