Skip to content

Commit e5542b3

Browse files
authored
Add API Key Authentication Support for Elasticsearch Storage (#7402)
<!-- !! Please DELETE this comment before posting. We appreciate your contribution to the Jaeger project! πŸ‘‹πŸŽ‰ --> ## Which problem is this PR solving? part of - #7225 ## Description of the changes - Adds API key authentication support to Elasticsearch storage backend alongside existing basic and bearer token authentication. - It is part of the planned work to add support for API authentication, as discussed in #7230 (comment) ### Changes - Added new authentication method -` APIKeyAuthentication` struct to support API key-based auth alongside existing basic/bearer token methods - Dual source support - API keys can be loaded from files OR extracted from request context - Hot reloading - File-based API keys automatically reload at configurable intervals (default 10s, 0 disables) - New command flags - Added` --es.api-key-file`, `--es.api-key-allow-from-context`, `--es.api-key-reload-interval` - Flexible config - Optional `APIKeyAuthentication` struct with file path, context flag, and reload interval - New context module - Created `apikey-context.go `with` GetAPIKey() `and `ContextWithAPIKey()` functions - Conditional logic - Health check disabled when` AllowFromContext=true` AND `FilePath=""` for both bearer tokens and API keys ## How was this change tested? - make test-ci - make lint ## Checklist - [ ] I have read https://github.com/jaegertracing/jaeger/blob/master/CONTRIBUTING_GUIDELINES.md - [ ] I have signed all commits - [ ] I have added unit tests for the new functionality - [ ] I have run lint and test steps successfully - for `jaeger`: `make lint test` - for `jaeger-ui`: `npm run lint` and `npm run test` --------- Signed-off-by: danish9039 <[email protected]>
1 parent c20bd92 commit e5542b3

File tree

10 files changed

+969
-327
lines changed

10 files changed

+969
-327
lines changed

β€Žcmd/query/app/token_propagation_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ func runQueryService(t *testing.T, esURL string) *Server {
8585

8686
f.InitFromViper(v, flagsSvc.Logger)
8787
// set AllowTokenFromContext manually because we don't register the respective CLI flag from query svc
88-
bearerAuth := escfg.BearerTokenAuthentication{
88+
bearerAuth := escfg.TokenAuthentication{
8989
AllowFromContext: true,
9090
}
9191
// set the authentication in the factory options
9292
f.Options.Config.Authentication = escfg.Authentication{
93-
BearerTokenAuthentication: configoptional.Some(bearerAuth),
93+
BearerTokenAuth: configoptional.Some(bearerAuth),
9494
}
9595

9696
// Initialize the factory with metrics and logger
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) 2025 The Jaeger Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package apikey
5+
6+
import "context"
7+
8+
// apiKeyContextKey is the type used as a key for storing API keys in context.
9+
type apiKeyContextKey struct{}
10+
11+
// GetAPIKey retrieves the API key from the context.
12+
func GetAPIKey(ctx context.Context) (string, bool) {
13+
val := ctx.Value(apiKeyContextKey{})
14+
if val == nil {
15+
return "", false
16+
}
17+
if apiKey, ok := val.(string); ok {
18+
return apiKey, true
19+
}
20+
return "", false
21+
}
22+
23+
// ContextWithAPIKey sets the API key in the context if the key is non-empty.
24+
func ContextWithAPIKey(ctx context.Context, apiKey string) context.Context {
25+
if apiKey == "" {
26+
return ctx
27+
}
28+
return context.WithValue(ctx, apiKeyContextKey{}, apiKey)
29+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) 2025 The Jaeger Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package apikey
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestGetAPIKey(t *testing.T) {
14+
// No value in context
15+
emptyCtx := context.Background()
16+
apiKey, ok := GetAPIKey(emptyCtx)
17+
assert.Empty(t, apiKey)
18+
assert.False(t, ok)
19+
20+
// Correct string value in context (via ContextWithAPIKey)
21+
expectedApiKey := "test-api-key"
22+
ctxWithApiKey := ContextWithAPIKey(context.Background(), expectedApiKey)
23+
apiKey, ok = GetAPIKey(ctxWithApiKey)
24+
assert.Equal(t, expectedApiKey, apiKey)
25+
assert.True(t, ok)
26+
27+
// Non-string value in context (simulate misuse)
28+
ctxWithNonString := context.WithValue(context.Background(), apiKeyContextKey{}, 123)
29+
apiKey, ok = GetAPIKey(ctxWithNonString)
30+
assert.Empty(t, apiKey)
31+
assert.False(t, ok)
32+
33+
// No API key when empty string passed to ContextWithAPIKey
34+
emptyStringCtx := ContextWithAPIKey(context.Background(), "")
35+
apiKey, ok = GetAPIKey(emptyStringCtx)
36+
assert.Empty(t, apiKey)
37+
assert.False(t, ok)
38+
}
39+
40+
func TestContextWithAPIKey(t *testing.T) {
41+
baseCtx := context.Background()
42+
43+
// Non-empty apiKey: should set value in context
44+
apiKey := "my-secret-key"
45+
ctxWithKey := ContextWithAPIKey(baseCtx, apiKey)
46+
val, ok := GetAPIKey(ctxWithKey)
47+
assert.True(t, ok, "apiKey should be present in context")
48+
assert.Equal(t, apiKey, val)
49+
50+
// Empty apiKey: should return original context, no value set
51+
emptyCtx := ContextWithAPIKey(baseCtx, "")
52+
val, ok = GetAPIKey(emptyCtx)
53+
assert.False(t, ok, "apiKey should not be present for empty string")
54+
assert.Empty(t, val)
55+
56+
// Should not mutate original context
57+
val, ok = GetAPIKey(baseCtx)
58+
assert.False(t, ok, "original context should remain unchanged")
59+
assert.Empty(t, val)
60+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2023 The Jaeger Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package apikey
5+
6+
import (
7+
"testing"
8+
9+
"github.com/jaegertracing/jaeger/internal/testutils"
10+
)
11+
12+
func TestMain(m *testing.M) {
13+
testutils.VerifyGoLeaks(m)
14+
}

β€Žinternal/storage/elasticsearch/config/auth_helper.go

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,65 @@ import (
1313
"go.uber.org/zap"
1414

1515
"github.com/jaegertracing/jaeger/internal/auth"
16+
"github.com/jaegertracing/jaeger/internal/auth/apikey"
1617
"github.com/jaegertracing/jaeger/internal/auth/bearertoken"
1718
)
1819

19-
// initBearerAuth initializes bearer token authentication method
20-
func initBearerAuth(bearerAuth *BearerTokenAuthentication, logger *zap.Logger) (*auth.Method, error) {
21-
return initBearerAuthWithTime(bearerAuth, logger, time.Now)
22-
}
23-
24-
// initBearerAuthWithTime initializes bearer token authentication method with injectable time for testing
25-
func initBearerAuthWithTime(bearerAuth *BearerTokenAuthentication, logger *zap.Logger, timeFn func() time.Time) (*auth.Method, error) {
26-
if bearerAuth == nil || (bearerAuth.FilePath == "" && !bearerAuth.AllowFromContext) {
20+
// initTokenAuthWithTime initializes token authentication injectable time for testing
21+
func initTokenAuthWithTime(tokenAuth *TokenAuthentication, scheme string, logger *zap.Logger, timeFn func() time.Time) (*auth.Method, error) {
22+
if tokenAuth == nil || (tokenAuth.FilePath == "" && !tokenAuth.AllowFromContext) {
2723
return nil, nil
2824
}
2925

30-
if bearerAuth.FilePath != "" && bearerAuth.AllowFromContext {
31-
logger.Warn("Both Bearer Token file and context propagation are enabled - context token will take precedence over file-based token")
26+
if tokenAuth.FilePath != "" && tokenAuth.AllowFromContext {
27+
logger.Warn("Both token file and context propagation are enabled - context token will take precedence over file-based token",
28+
zap.String("auth_scheme", scheme))
3229
}
3330

3431
var tokenFn func() string
3532
var fromCtx func(context.Context) (string, bool)
3633

37-
// file-based token setup
38-
if bearerAuth.FilePath != "" {
39-
reloadInterval := bearerAuth.ReloadInterval
40-
tf, err := auth.TokenProviderWithTime(bearerAuth.FilePath, reloadInterval, logger, timeFn)
34+
// File-based token setup
35+
if tokenAuth.FilePath != "" {
36+
tf, err := auth.TokenProviderWithTime(tokenAuth.FilePath, tokenAuth.ReloadInterval, logger, timeFn)
4137
if err != nil {
4238
return nil, err
4339
}
4440
tokenFn = tf
4541
}
4642

47-
// context-based token setup
48-
if bearerAuth.AllowFromContext {
49-
fromCtx = bearertoken.GetBearerToken
43+
// Context-based token setup
44+
if tokenAuth.AllowFromContext {
45+
if scheme == "Bearer" {
46+
fromCtx = bearertoken.GetBearerToken
47+
} else if scheme == "APIKey" {
48+
fromCtx = apikey.GetAPIKey
49+
}
5050
}
5151

52-
// Return pointer to the auth method
5352
return &auth.Method{
54-
Scheme: "Bearer",
53+
Scheme: scheme,
5554
TokenFn: tokenFn,
5655
FromCtx: fromCtx,
5756
}, nil
5857
}
5958

59+
// Simplified init functions - directly call shared implementation
60+
func initBearerAuth(tokenAuth *TokenAuthentication, logger *zap.Logger) (*auth.Method, error) {
61+
if tokenAuth == nil {
62+
return nil, nil
63+
}
64+
return initTokenAuthWithTime(tokenAuth, "Bearer", logger, time.Now)
65+
}
66+
67+
func initAPIKeyAuth(tokenAuth *TokenAuthentication, logger *zap.Logger) (*auth.Method, error) {
68+
if tokenAuth == nil {
69+
return nil, nil
70+
}
71+
return initTokenAuthWithTime(tokenAuth, "APIKey", logger, time.Now)
72+
}
73+
74+
// Keep initBasicAuth unchanged
6075
func initBasicAuth(basicAuth *BasicAuthentication, logger *zap.Logger) (*auth.Method, error) {
6176
return initBasicAuthWithTime(basicAuth, logger, time.Now)
6277
}
@@ -74,8 +89,8 @@ func initBasicAuthWithTime(basicAuth *BasicAuthentication, logger *zap.Logger, t
7489
if username == "" {
7590
return nil, nil
7691
}
77-
var tokenFn func() string
7892

93+
var tokenFn func() string
7994
// Handle password from file or static password
8095
if basicAuth.PasswordFilePath != "" {
8196
// Use TokenProvider for password loading
@@ -98,7 +113,6 @@ func initBasicAuthWithTime(basicAuth *BasicAuthentication, logger *zap.Logger, t
98113
password := basicAuth.Password
99114
credentials := username + ":" + password
100115
encodedCredentials := base64.StdEncoding.EncodeToString([]byte(credentials))
101-
102116
tokenFn = func() string { return encodedCredentials }
103117
}
104118

0 commit comments

Comments
Β (0)