Skip to content

Commit 00db174

Browse files
jrhynessclaude
andcommitted
fix: address CodeRabbit security and validation findings
Three improvements based on CodeRabbit review: 1. UUID Validation (Defense-in-Depth): - Add strict UUID validation in ValidateAPIKey before using metadata.ID - Fail-closed: return error instead of proceeding with malformed IDs - Prevents malformed IDs from being used in cache keys or authz decisions - Updated tests to use realistic UUIDs instead of human-readable IDs 2. Negative TTL Test Coverage: - Extract ValidateCacheTTLs() helper function from SetupWithManager - Update TestMaaSAuthPolicyReconciler_NegativeTTLRejection to actually test the validation logic used by SetupWithManager - Test now verifies expectSetupFail cases properly reject negative TTLs 3. X-MaaS-Group JSON Construction (Injection Prevention): - Replace string concatenation ('["' + ... + '"]') with CEL string() conversion - Properly escapes special characters in group names - Prevents JSON injection if group names contain quotes or brackets - Empty arrays now serialize as [] instead of potentially malformed strings All tests pass after changes. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 144c17f commit 00db174

File tree

4 files changed

+50
-26
lines changed

4 files changed

+50
-26
lines changed

maas-api/internal/api_keys/service.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@ func (s *Service) ValidateAPIKey(ctx context.Context, key string) (*ValidationRe
239239
}, nil
240240
}
241241

242+
// Validate metadata.ID is a syntactically valid UUID (fail-closed defense-in-depth)
243+
// Database should always return valid UUIDs, but verify to prevent malformed IDs
244+
// from being used in cache keys or authorization decisions
245+
if _, err := uuid.Parse(metadata.ID); err != nil {
246+
s.logger.Error("API key has invalid UUID format", "key_id", metadata.ID, "error", err)
247+
return nil, fmt.Errorf("database integrity error: invalid key ID format: %w", err)
248+
}
249+
242250
// Success - return user identity and groups for Authorino
243251
return &ValidationResult{
244252
Valid: true,

maas-api/internal/api_keys/service_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ func TestValidateAPIKey_ValidKey(t *testing.T) {
4343
ctx := context.Background()
4444
svc, store := createTestService(t)
4545

46-
// Create a valid API key
47-
keyID := "test-key-id"
46+
// Create a valid API key with UUID (matches production database behavior)
47+
keyID := "550e8400-e29b-41d4-a716-446655440000"
4848
plainKey, hash := createTestAPIKey(t)
4949
username := "alice"
5050
groups := []string{"tier-premium", "system:authenticated"}
@@ -108,7 +108,7 @@ func TestValidateAPIKey_RevokedKey(t *testing.T) {
108108
svc, store := createTestService(t)
109109

110110
// Create and immediately revoke a key
111-
keyID := "revoked-key-id"
111+
keyID := "550e8400-e29b-41d4-a716-446655440001"
112112
plainKey, hash := createTestAPIKey(t)
113113
username := "bob"
114114
groups := []string{"tier-free"}
@@ -134,7 +134,7 @@ func TestValidateAPIKey_ExpiredKey(t *testing.T) {
134134
svc, store := createTestService(t)
135135

136136
// Create a key that's already expired
137-
keyID := "expired-key-id"
137+
keyID := "550e8400-e29b-41d4-a716-446655440002"
138138
plainKey, hash := createTestAPIKey(t)
139139
username := "charlie"
140140
groups := []string{"tier-basic"}
@@ -157,7 +157,7 @@ func TestValidateAPIKey_EmptyGroups(t *testing.T) {
157157
svc, store := createTestService(t)
158158

159159
// Create a key with no groups (nil)
160-
keyID := "no-groups-key"
160+
keyID := "550e8400-e29b-41d4-a716-446655440003"
161161
plainKey, hash := createTestAPIKey(t)
162162
username := "dave"
163163

@@ -180,7 +180,7 @@ func TestValidateAPIKey_UpdatesLastUsed(t *testing.T) {
180180
svc, store := createTestService(t)
181181

182182
// Create a valid API key
183-
keyID := "last-used-key"
183+
keyID := "550e8400-e29b-41d4-a716-446655440004"
184184
plainKey, hash := createTestAPIKey(t)
185185
username := "eve"
186186
groups := []string{"tier-enterprise"}
@@ -216,7 +216,7 @@ func TestGetAPIKey(t *testing.T) {
216216
svc, store := createTestService(t)
217217

218218
// Create a key
219-
keyID := "get-test-key"
219+
keyID := "550e8400-e29b-41d4-a716-446655440005"
220220
_, hash := createTestAPIKey(t)
221221
username := "alice"
222222
keyName := "Alice's Key"
@@ -249,7 +249,7 @@ func TestRevokeAPIKey(t *testing.T) {
249249
svc, store := createTestService(t)
250250

251251
// Create a key
252-
keyID := "revoke-test-key"
252+
keyID := "550e8400-e29b-41d4-a716-446655440006"
253253
_, hash := createTestAPIKey(t)
254254
username := "bob"
255255

maas-controller/pkg/controller/maas/maasauthpolicy_controller.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -459,10 +459,11 @@ allow {
459459
"metrics": false,
460460
"priority": int64(0),
461461
},
462-
// Groups - construct JSON array string from API key validation or K8s identity
462+
// Groups - serialize to JSON array string from API key validation or K8s identity
463+
// Using string() conversion of JSON-serialized groups for proper escaping
463464
"X-MaaS-Group": map[string]interface{}{
464465
"plain": map[string]interface{}{
465-
"expression": `'["' + ((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : auth.identity.user.groups).join('","') + '"]'`,
466+
"expression": `string(((has(auth.metadata) && has(auth.metadata.apiKeyValidation)) ? auth.metadata.apiKeyValidation.groups : auth.identity.user.groups))`,
466467
},
467468
"metrics": false,
468469
"priority": int64(0),
@@ -758,17 +759,26 @@ func (r *MaaSAuthPolicyReconciler) updateStatus(ctx context.Context, policy *maa
758759
}
759760
}
760761

761-
func (r *MaaSAuthPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
762-
// Validate cache TTL configuration
763-
log := ctrl.Log.WithName("maas-authpolicy-controller")
764-
765-
// Reject negative TTL values
762+
// ValidateCacheTTLs validates that cache TTL configuration is valid.
763+
// Returns an error if either TTL is negative (fail-closed validation).
764+
func (r *MaaSAuthPolicyReconciler) ValidateCacheTTLs() error {
766765
if r.MetadataCacheTTL < 0 {
767766
return fmt.Errorf("metadata cache TTL must be non-negative, got %d", r.MetadataCacheTTL)
768767
}
769768
if r.AuthzCacheTTL < 0 {
770769
return fmt.Errorf("authorization cache TTL must be non-negative, got %d", r.AuthzCacheTTL)
771770
}
771+
return nil
772+
}
773+
774+
func (r *MaaSAuthPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
775+
// Validate cache TTL configuration
776+
log := ctrl.Log.WithName("maas-authpolicy-controller")
777+
778+
// Reject negative TTL values
779+
if err := r.ValidateCacheTTLs(); err != nil {
780+
return err
781+
}
772782

773783
if r.AuthzCacheTTL > r.MetadataCacheTTL {
774784
log.Info("WARNING: Authorization cache TTL exceeds metadata cache TTL. "+

maas-controller/pkg/controller/maas/maasauthpolicy_controller_test.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -585,18 +585,24 @@ func TestMaaSAuthPolicyReconciler_NegativeTTLRejection(t *testing.T) {
585585
AuthzCacheTTL: tc.authzTTL,
586586
}
587587

588-
// SetupWithManager requires a manager, but we can't easily create one in unit tests.
589-
// Instead, directly test the validation logic by checking the authzCacheTTL() defensive clamp
590-
// and noting that SetupWithManager would reject negative values.
591-
592-
// The defensive clamp in authzCacheTTL() should never return negative values
593-
effectiveTTL := r.authzCacheTTL()
594-
if effectiveTTL < 0 {
595-
t.Errorf("authzCacheTTL() returned negative value %d, should be clamped to 0", effectiveTTL)
596-
}
588+
// Test the validation logic used by SetupWithManager
589+
err := r.ValidateCacheTTLs()
590+
591+
if tc.expectSetupFail {
592+
if err == nil {
593+
t.Errorf("Expected validation to fail for negative TTLs, but got no error")
594+
}
595+
} else {
596+
if err != nil {
597+
t.Errorf("Expected validation to succeed, but got error: %v", err)
598+
}
597599

598-
// Note: Full SetupWithManager validation is tested via integration tests
599-
// as it requires a controller-runtime manager.
600+
// Also verify authzCacheTTL() defensive clamp never returns negative
601+
effectiveTTL := r.authzCacheTTL()
602+
if effectiveTTL < 0 {
603+
t.Errorf("authzCacheTTL() returned negative value %d, should be clamped to 0", effectiveTTL)
604+
}
605+
}
600606
})
601607
}
602608
}

0 commit comments

Comments
 (0)