Skip to content

Commit 5e9b13e

Browse files
authored
Merge pull request #333 from githubnext/copilot/refactor-semantic-function-clustering
Consolidate duplicate sanitization and auth parsing logic
2 parents 5e41715 + 025f9ec commit 5e9b13e

11 files changed

Lines changed: 614 additions & 274 deletions

File tree

internal/auth/header.go

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,41 @@
55
// which requires Authorization headers to contain the API key directly
66
// without any scheme prefix (e.g., NOT "Bearer <key>").
77
//
8-
// Example usage:
8+
// The package provides both full parsing with error handling (ParseAuthHeader)
9+
// and convenience methods for specific use cases (ExtractAgentID, ValidateAPIKey).
910
//
11+
// Usage Guidelines:
12+
//
13+
// - Use ParseAuthHeader() for complete authentication with error handling:
14+
// Returns both API key and agent ID, with errors for missing/invalid headers.
15+
//
16+
// - Use ExtractAgentID() when you only need the agent ID and want automatic
17+
// fallback to "default" instead of error handling.
18+
//
19+
// - Use ValidateAPIKey() to check if a provided key matches the expected value.
20+
// Automatically handles the case where authentication is disabled (no expected key).
21+
//
22+
// Example:
23+
//
24+
// // Full authentication
1025
// apiKey, agentID, err := auth.ParseAuthHeader(r.Header.Get("Authorization"))
1126
// if err != nil {
12-
// // Handle error
27+
// return err
1328
// }
29+
// if !auth.ValidateAPIKey(apiKey, expectedKey) {
30+
// return errors.New("invalid API key")
31+
// }
32+
//
33+
// // Extract agent ID only (for context, not authentication)
34+
// agentID := auth.ExtractAgentID(r.Header.Get("Authorization"))
1435
package auth
1536

1637
import (
1738
"errors"
1839
"strings"
1940

2041
"github.com/githubnext/gh-aw-mcpg/internal/logger"
42+
"github.com/githubnext/gh-aw-mcpg/internal/logger/sanitize"
2143
)
2244

2345
var log = logger.New("auth:header")
@@ -29,18 +51,6 @@ var (
2951
ErrInvalidAuthHeader = errors.New("invalid Authorization header format")
3052
)
3153

32-
// sanitizeForLogging returns a sanitized version of the input string for safe logging.
33-
// It shows only the first 4 characters followed by "..." to prevent exposing sensitive data.
34-
// For strings with 4 or fewer characters, it returns only "...".
35-
func sanitizeForLogging(input string) string {
36-
if len(input) > 4 {
37-
return input[:4] + "..."
38-
} else if len(input) > 0 {
39-
return "..."
40-
}
41-
return ""
42-
}
43-
4454
// ParseAuthHeader parses the Authorization header and extracts the API key and agent ID.
4555
// Per MCP spec 7.1, the Authorization header should contain the API key directly
4656
// without any Bearer prefix or other scheme.
@@ -54,7 +64,7 @@ func sanitizeForLogging(input string) string {
5464
// - agentID: The extracted agent/session identifier
5565
// - error: ErrMissingAuthHeader if header is empty, nil otherwise
5666
func ParseAuthHeader(authHeader string) (apiKey string, agentID string, error error) {
57-
log.Printf("Parsing auth header: sanitized=%s, length=%d", sanitizeForLogging(authHeader), len(authHeader))
67+
log.Printf("Parsing auth header: sanitized=%s, length=%d", sanitize.TruncateSecret(authHeader), len(authHeader))
5868

5969
if authHeader == "" {
6070
log.Print("Auth header missing, returning error")
@@ -96,3 +106,23 @@ func ValidateAPIKey(provided, expected string) bool {
96106
log.Printf("API key validation result: matches=%t", matches)
97107
return matches
98108
}
109+
110+
// ExtractAgentID extracts the agent ID from an Authorization header.
111+
// This is a convenience wrapper around ParseAuthHeader that only returns the agent ID.
112+
// Returns "default" if the header is empty or cannot be parsed.
113+
//
114+
// This function is intended for use cases where you only need the agent ID
115+
// and don't need full error handling. For complete authentication handling,
116+
// use ParseAuthHeader instead.
117+
func ExtractAgentID(authHeader string) string {
118+
if authHeader == "" {
119+
return "default"
120+
}
121+
122+
_, agentID, err := ParseAuthHeader(authHeader)
123+
if err != nil {
124+
return "default"
125+
}
126+
127+
return agentID
128+
}

internal/auth/header_test.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55

66
"github.com/stretchr/testify/assert"
77
"github.com/stretchr/testify/require"
8+
9+
"github.com/githubnext/gh-aw-mcpg/internal/logger/sanitize"
810
)
911

10-
func TestSanitizeForLogging(t *testing.T) {
12+
func TestTruncateSecret(t *testing.T) {
1113
tests := []struct {
1214
name string
1315
input string
@@ -62,7 +64,7 @@ func TestSanitizeForLogging(t *testing.T) {
6264

6365
for _, tt := range tests {
6466
t.Run(tt.name, func(t *testing.T) {
65-
got := sanitizeForLogging(tt.input)
67+
got := sanitize.TruncateSecret(tt.input)
6668
assert.Equal(t, tt.want, got)
6769
})
6870
}
@@ -235,3 +237,49 @@ func TestValidateAPIKey(t *testing.T) {
235237
})
236238
}
237239
}
240+
241+
func TestExtractAgentID(t *testing.T) {
242+
tests := []struct {
243+
name string
244+
authHeader string
245+
want string
246+
}{
247+
{
248+
name: "Empty header returns default",
249+
authHeader: "",
250+
want: "default",
251+
},
252+
{
253+
name: "Plain API key",
254+
authHeader: "my-api-key",
255+
want: "my-api-key",
256+
},
257+
{
258+
name: "Bearer token",
259+
authHeader: "Bearer my-token-123",
260+
want: "my-token-123",
261+
},
262+
{
263+
name: "Agent format",
264+
authHeader: "Agent agent-abc",
265+
want: "agent-abc",
266+
},
267+
{
268+
name: "Long API key",
269+
authHeader: "my-super-long-api-key-with-many-characters",
270+
want: "my-super-long-api-key-with-many-characters",
271+
},
272+
{
273+
name: "API key with special characters",
274+
authHeader: "key!@#$%^&*()",
275+
want: "key!@#$%^&*()",
276+
},
277+
}
278+
279+
for _, tt := range tests {
280+
t.Run(tt.name, func(t *testing.T) {
281+
got := ExtractAgentID(tt.authHeader)
282+
assert.Equal(t, tt.want, got)
283+
})
284+
}
285+
}

internal/guard/context.go

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
1+
// Package guard provides security context management and guard registry for the MCP Gateway.
2+
//
3+
// This package is responsible for managing security labels (DIFC - Decentralized Information
4+
// Flow Control) and extracting agent identifiers from request contexts.
5+
//
6+
// Relationship with internal/auth:
7+
//
8+
// - internal/auth: Primary authentication logic (header parsing, validation)
9+
// - internal/guard: Security context management (agent ID tracking, guard registry)
10+
//
11+
// For new code, prefer using internal/auth for authentication-related operations.
12+
// The guard package's ExtractAgentIDFromAuthHeader() is deprecated and delegates
13+
// to auth.ExtractAgentID() for centralized authentication logic.
14+
//
15+
// Example:
16+
//
17+
// // Store agent ID in context (use auth.ExtractAgentID for parsing)
18+
// agentID := auth.ExtractAgentID(authHeader)
19+
// ctx = guard.SetAgentIDInContext(ctx, agentID)
20+
//
21+
// // Retrieve agent ID from context
22+
// agentID := guard.GetAgentIDFromContext(ctx) // Returns "default" if not found
123
package guard
224

325
import (
426
"context"
5-
"strings"
27+
28+
"github.com/githubnext/gh-aw-mcpg/internal/auth"
629
)
730

831
// ContextKey is used for storing values in context
@@ -30,35 +53,14 @@ func SetAgentIDInContext(ctx context.Context, agentID string) context.Context {
3053
return context.WithValue(ctx, AgentIDContextKey, agentID)
3154
}
3255

33-
// ExtractAgentIDFromAuthHeader extracts agent ID from Authorization header
56+
// ExtractAgentIDFromAuthHeader extracts agent ID from Authorization header.
3457
//
35-
// Note: For MCP spec 7.1 compliant parsing, see internal/auth.ParseAuthHeader()
36-
// which provides centralized authentication header parsing.
58+
// Deprecated: Use auth.ExtractAgentID() instead for centralized authentication parsing.
59+
// This function is maintained for backward compatibility but delegates to the auth package.
3760
//
38-
// This function supports formats:
39-
// - "Bearer <token>" - uses token as agent ID
40-
// - "Agent <agent-id>" - uses agent-id directly
41-
// - Any other format - uses the entire value as agent ID
61+
// For MCP spec 7.1 compliant parsing with full error handling, use auth.ParseAuthHeader().
4262
func ExtractAgentIDFromAuthHeader(authHeader string) string {
43-
if authHeader == "" {
44-
return "default"
45-
}
46-
47-
// Handle "Bearer <token>" format
48-
if strings.HasPrefix(authHeader, "Bearer ") {
49-
token := strings.TrimPrefix(authHeader, "Bearer ")
50-
// Use the token as the agent ID
51-
// In production, you might want to validate/decode the token
52-
return token
53-
}
54-
55-
// Handle "Agent <agent-id>" format
56-
if strings.HasPrefix(authHeader, "Agent ") {
57-
return strings.TrimPrefix(authHeader, "Agent ")
58-
}
59-
60-
// Use the entire header value as agent ID
61-
return authHeader
63+
return auth.ExtractAgentID(authHeader)
6264
}
6365

6466
// GetRequestStateFromContext retrieves guard request state from context

internal/launcher/launcher.go

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,13 @@ import (
1010

1111
"github.com/githubnext/gh-aw-mcpg/internal/config"
1212
"github.com/githubnext/gh-aw-mcpg/internal/logger"
13+
"github.com/githubnext/gh-aw-mcpg/internal/logger/sanitize"
1314
"github.com/githubnext/gh-aw-mcpg/internal/mcp"
1415
"github.com/githubnext/gh-aw-mcpg/internal/tty"
1516
)
1617

1718
var logLauncher = logger.New("launcher:launcher")
1819

19-
// sanitizeEnvForLogging returns a sanitized version of environment variables
20-
// where each value is truncated to first 4 characters followed by "..."
21-
// This prevents sensitive information like API keys from being logged in full.
22-
func sanitizeEnvForLogging(env map[string]string) map[string]string {
23-
if env == nil {
24-
return nil
25-
}
26-
sanitized := make(map[string]string, len(env))
27-
for key, value := range env {
28-
if len(value) <= 4 {
29-
// If value is 4 chars or less, just use "..."
30-
sanitized[key] = "..."
31-
} else {
32-
// Show first 4 characters and append "..."
33-
sanitized[key] = value[:4] + "..."
34-
}
35-
}
36-
return sanitized
37-
}
38-
3920
// Launcher manages backend MCP server connections
4021
type Launcher struct {
4122
ctx context.Context
@@ -161,7 +142,7 @@ func GetOrLaunch(l *Launcher, serverID string) (*mcp.Connection, error) {
161142
}
162143

163144
if len(serverCfg.Env) > 0 {
164-
log.Printf("[LAUNCHER] Additional env vars: %v", sanitizeEnvForLogging(serverCfg.Env))
145+
log.Printf("[LAUNCHER] Additional env vars: %v", sanitize.TruncateSecretMap(serverCfg.Env))
165146
}
166147

167148
// Create connection
@@ -174,7 +155,7 @@ func GetOrLaunch(l *Launcher, serverID string) (*mcp.Connection, error) {
174155
log.Printf("[LAUNCHER] Debug Information:")
175156
log.Printf("[LAUNCHER] - Command: %s", serverCfg.Command)
176157
log.Printf("[LAUNCHER] - Args: %v", serverCfg.Args)
177-
log.Printf("[LAUNCHER] - Env vars: %v", sanitizeEnvForLogging(serverCfg.Env))
158+
log.Printf("[LAUNCHER] - Env vars: %v", sanitize.TruncateSecretMap(serverCfg.Env))
178159
log.Printf("[LAUNCHER] - Running in container: %v", l.runningInContainer)
179160
log.Printf("[LAUNCHER] - Is direct command: %v", isDirectCommand)
180161

internal/launcher/launcher_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/stretchr/testify/require"
1414

1515
"github.com/githubnext/gh-aw-mcpg/internal/config"
16+
"github.com/githubnext/gh-aw-mcpg/internal/logger/sanitize"
1617
)
1718

1819
// loadConfigFromJSON is a test helper that creates a config from JSON via stdin
@@ -180,7 +181,7 @@ func TestMixedHTTPAndStdioServers(t *testing.T) {
180181
assert.Equal(t, "stdio", stdioServer.Type)
181182
}
182183

183-
func TestSanitizeEnvForLogging(t *testing.T) {
184+
func TestTruncateSecretMap(t *testing.T) {
184185
tests := []struct {
185186
name string
186187
input map[string]string
@@ -242,14 +243,14 @@ func TestSanitizeEnvForLogging(t *testing.T) {
242243
"EMPTY": "",
243244
},
244245
expected: map[string]string{
245-
"EMPTY": "...",
246+
"EMPTY": "",
246247
},
247248
},
248249
}
249250

250251
for _, tt := range tests {
251252
t.Run(tt.name, func(t *testing.T) {
252-
result := sanitizeEnvForLogging(tt.input)
253+
result := sanitize.TruncateSecretMap(tt.input)
253254

254255
if tt.expected == nil {
255256
assert.Nil(t, result)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
// Package logger provides structured logging for the MCP Gateway.
2+
//
3+
// This file contains helper functions for managing global logger state with proper
4+
// mutex handling. These helpers encapsulate common patterns for initializing and
5+
// closing global loggers (FileLogger, JSONLLogger, MarkdownLogger) to reduce code
6+
// duplication while maintaining thread safety.
7+
//
8+
// Functions in this file follow a consistent pattern:
9+
//
10+
// - init*: Initialize a global logger with proper locking and cleanup of any existing logger
11+
// - close*: Close and clear a global logger with proper locking
12+
//
13+
// These helpers are used internally by the logger package and should not be called
14+
// directly by external code. Use the public Init* and Close* functions instead.
115
package logger
216

317
// This file contains helper functions that encapsulate the common patterns

0 commit comments

Comments
 (0)