Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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: |
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions pkg/handlers/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
124 changes: 124 additions & 0 deletions pkg/handlers/notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
4 changes: 2 additions & 2 deletions scripts/dockerfiles/Dockerfile.alpine
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
ARG GO_VERSION
FROM golang:$GO_VERSION-alpine3.21 as builder
FROM golang:$GO_VERSION-alpine3.20 as builder

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

busybox 1.36.1 / Dockerfile.alpine.FROM

Total vulnerabilities: 4

Critical: 0 High: 0 Medium: 4 Low: 0
Vulnerability IDSeverityCVSSFixed inStatus
CVE-2023-42363 MEDIUM MEDIUM 5.5 - Open
CVE-2023-42364 MEDIUM MEDIUM 5.5 - Open
CVE-2023-42365 MEDIUM MEDIUM 5.5 - Open
CVE-2023-42366 MEDIUM MEDIUM 5.5 - Open

# hadolint ignore=DL3018
RUN addgroup -S agentgroup && adduser -S agentuser -G agentgroup
RUN apk add --no-cache make gcc libc-dev git curl
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
Expand Down