Skip to content

Conversation

@nimsara66
Copy link
Contributor

@nimsara66 nimsara66 commented Jan 22, 2026

Purpose

The platform gateway currently lacks built-in support for secret management and runtime secret resolution. This pull request introduces a Gateway REST API for managing secrets, along with an annotation-based mechanism to securely resolve those secrets at runtime.

Related discussions

#654
#673

Summary by CodeRabbit

  • New Features

    • Added Secrets Management API: create, list, retrieve, update, delete via /secrets; secrets stored encrypted and returned decrypted in responses
    • Policy resolution: templated secret values in policy parameters are now resolved automatically
    • OpenAPI/tags: new "Secrets Management" tag and updated provider-template grouping
  • Validation

    • Secret input validation (name, type, size) with clear error responses
  • Tests

    • End-to-end BDD scenarios covering secret lifecycle, errors, auth, and listing
  • Other

    • Default encryption keys included in the runtime image; DB and indexes added for secrets storage

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

Walkthrough

Adds AES‑GCM encryption providers and key management, a ProviderManager and SecretService with full CRUD and storage, a PolicyResolver to inject decrypted secret values into policies, config/validation for encryption/secrets, DB/schema and SQLite storage methods, API handlers/OpenAPI for /secrets, Dockerfile asset copy, and integration tests.

Changes

Cohort / File(s) Summary
Encryption infra
gateway/gateway-controller/pkg/encryption/aesgcm/keymgmt.go, gateway/gateway-controller/pkg/encryption/aesgcm/provider.go, gateway/gateway-controller/pkg/encryption/errors.go, gateway/gateway-controller/pkg/encryption/manager.go
Add Key, KeyManager, AESGCMProvider, EncryptionProvider interface, ProviderManager, EncryptedPayload marshal/unmarshal, health checks, and encryption error types.
Secrets service & models
gateway/gateway-controller/pkg/secrets/service.go, gateway/gateway-controller/pkg/models/secrets.go
New SecretService with Create/List/Get/Update/Delete, SecretParams, MaxSecretSize, encryption/storage wiring, and Secret model.
Storage schema & implementation
gateway/gateway-controller/pkg/storage/gateway-controller-db.sql, gateway/gateway-controller/pkg/storage/interface.go, gateway/gateway-controller/pkg/storage/sqlite.go
Add secrets table and index; extend Storage interface with secret CRUD and existence check; implement SQLiteStorage secret CRUD, list, update, delete, and existence methods.
Policy resolution
gateway/gateway-controller/pkg/resolver/policy_resolver.go, gateway/gateway-controller/default-policies/modify-headers-v0.1.0.yaml, gateway/policies/modify-headers/v0.1.0/policy-definition.yaml
New PolicyResolver that discovers resolve paths in policy schemas, resolves $secret{...} templates via SecretService, and returns resolved StoredConfig; policy definitions marked for resolution.
API layer & OpenAPI
gateway/gateway-controller/pkg/api/handlers/handlers.go, gateway/gateway-controller/api/openapi.yaml
Wire secretsService and policyResolver into APIServer; add secret CRUD handlers and /secrets OpenAPI endpoints and schemas; adjust tags.
Initialization & wiring
gateway/gateway-controller/cmd/controller/main.go, gateway/gateway-controller/pkg/api/handlers/policy_ordering_test.go
Initialize encryption providers, ProviderManager, SecretService, and PolicyResolver; pass secretsService and policyResolver into APIServer and tests; adjust startup order and policy resolution usage.
Configuration & validation
gateway/gateway-controller/pkg/config/config.go, gateway/gateway-controller/pkg/config/secret_validator.go, gateway/gateway-controller/pkg/config/mcp_validator.go, gateway/gateway-controller/pkg/config/parser.go
Add Encryption config types and default key path; add SecretValidator and validation rules; small parser error message change and MCP apiVersion constant swap.
Dockerfile updates
gateway/gateway-controller/Dockerfile, gateway/gateway-controller/Dockerfile.coverage
COPY aesgcm-keys into runtime image at /app/aesgcm-keys.
Policy defaults
gateway/gateway-controller/default-policies/modify-headers-v0.1.0.yaml, gateway/policies/modify-headers/v0.1.0/policy-definition.yaml
Add resolve: - value to request/response header item schemas to mark fields for resolution.
Integration tests
gateway/it/features/secrets.feature, gateway/it/steps_secrets.go, gateway/it/suite_test.go
Add BDD feature and step implementations for secrets lifecycle, validation, conflicts, auth checks; register steps in test suite.

Sequence Diagram(s)

sequenceDiagram
    participant Client as API Client
    participant Handler as Secret Handler
    participant Service as Secret Service
    participant Manager as Provider Manager
    participant Provider as AES‑GCM Provider
    participant Storage as SQLite Storage

    Client->>Handler: POST /secrets (plaintext)
    Handler->>Service: CreateSecret(params)
    Service->>Manager: Encrypt(plaintext)
    Manager->>Provider: Encrypt(plaintext)
    Provider->>Provider: generate nonce & produce ciphertext
    Provider-->>Manager: EncryptedPayload
    Manager-->>Service: EncryptedPayload
    Service->>Storage: SaveSecret(model with ciphertext)
    Storage-->>Service: Saved
    Service-->>Handler: Secret (plaintext in response)
    Handler-->>Client: 201 Created
Loading
sequenceDiagram
    participant APIServer as API Server
    participant Resolver as PolicyResolver
    participant SecretSvc as Secret Service
    participant Manager as Provider Manager
    participant Provider as AES‑GCM Provider

    APIServer->>Resolver: ResolvePolicies(apiConfig)
    Resolver->>Resolver: Identify resolve paths in schemas
    Resolver->>SecretSvc: Get(secretHandle)
    SecretSvc->>Manager: Decrypt(encrypted payload)
    Manager->>Provider: Decrypt(ciphertext)
    Provider->>Provider: extract nonce & decrypt
    Provider-->>Manager: plaintext
    Manager-->>SecretSvc: plaintext
    SecretSvc-->>Resolver: plaintext value
    Resolver->>Resolver: Substitute plaintext into policy params
    Resolver-->>APIServer: Resolved apiConfig
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐇 I found the keys beneath a seed,

AES hums soft to hide a thread,
Secrets tucked in ciphered sleep,
Policies wake with truths to keep,
I hop, I guard — a quiet thread.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers Purpose and Related discussions but is missing critical sections required by the template: Goals, Approach, User stories, Documentation, Automation tests, Security checks, Samples, Related PRs, and Test environment. Expand the description to include all required template sections, particularly Goals, Approach, test details, security checks, and test environment information to meet repository standards.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature being added: secret management for the gateway controller, which aligns with the comprehensive implementation of secrets API, encryption, storage, and integration throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
gateway/gateway-controller/pkg/api/handlers/policy_ordering_test.go (1)

29-49: Use a properly initialized SecretService stub to prevent nil panics when secret resolution is tested.

&secrets.SecretService{} leaves all fields (storage, providerManager, logger) nil. The Get() method immediately calls s.logger.Info() at line 161 without nil-guards, which will panic if secret resolution is triggered in future x-resolve tests. Either use the NewSecretsService() constructor with a nop logger and in-memory storage, or create a lightweight test stub with safe defaults.

gateway/gateway-controller/api/openapi.yaml (1)

3632-3880: Align secret size limits and value-return semantics across schemas.

There are conflicting limits and descriptions: SecretConfigData.value is capped at 8192 and says “never returned,” while SecretCreateRequest/SecretUpdateRequest allow 10240 and responses clearly include the value. SecretListResponse also claims “metadata” but the schema is a string list. This is an API contract inconsistency.

✅ Minimal alignment proposal (example)
-        value:
-          type: string
-          description: Secret value (stored securely and never returned in API responses)
-          minLength: 1
-          maxLength: 8192
+        value:
+          type: string
+          description: Secret value (stored securely; returned only on create/get, never in list responses)
+          minLength: 1
+          maxLength: 8192
-          maxLength: 10240
+          maxLength: 8192
-          maxLength: 10240
+          maxLength: 8192
gateway/gateway-controller/pkg/api/handlers/handlers.go (1)

92-123: secretService and policyResolver are never assigned in NewAPIServer.

The constructor accepts secretService and policyResolver parameters (lines 92-93) but they are never assigned to the server struct. This will cause nil pointer panics when secret endpoints or policy resolution are invoked.

🐛 Proposed fix
 	server := &APIServer{
 		store:                store,
 		db:                   db,
 		snapshotManager:      snapshotManager,
 		policyManager:        policyManager,
 		policyDefinitions:    policyDefinitions,
 		parser:               config.NewParser(),
 		validator:            validator,
 		logger:               logger,
 		deploymentService:    deploymentService,
 		mcpDeploymentService: utils.NewMCPDeploymentService(store, db, snapshotManager),
 		llmDeploymentService: utils.NewLLMDeploymentService(store, db, snapshotManager, templateDefinitions,
 			deploymentService, &systemConfig.GatewayController.Router),
 		apiKeyService:      utils.NewAPIKeyService(store, db, apiKeyXDSManager),
 		apiKeyXDSManager:   apiKeyXDSManager,
 		controlPlaneClient: controlPlaneClient,
 		routerConfig:       &systemConfig.GatewayController.Router,
 		httpClient:         &http.Client{Timeout: 10 * time.Second},
 		systemConfig:       systemConfig,
+		secretService:      secretService,
+		policyResolver:     policyResolver,
 	}
🤖 Fix all issues with AI agents
In `@gateway/gateway-controller/api/openapi.yaml`:
- Around line 1703-1735: The OpenAPI spec uses the "secrets" tag for the
/secrets endpoints (see operationId listSecrets and the POST create secret) but
lacks a corresponding tag definition; add a "secrets" entry to the global tags
list (with a short name "secrets" and a brief description like "Operations for
managing secrets") so the tag is declared for the spec and clients/docs can
display it correctly.

In `@gateway/gateway-controller/Dockerfile`:
- Around line 80-82: Remove the baked-in test keys by deleting the COPY
aesgcm-keys /app/aesgcm-keys instruction from the Dockerfile and update
build/runtime guidance to require providing key files via mounted volumes or
external secret stores; update documentation and examples to show using the
configuration/env var (e.g.,
GATEWAY_GATEWAY_CONTROLLER_ENCRYPTION_PROVIDERS_0_KEYS_0_FILEPATH=/mnt/secrets/key.bin)
to point to the mounted key path, and add a note in deployment docs that keys
must be mounted or supplied externally (and not included in image layers) for
production.

In `@gateway/gateway-controller/pkg/api/handlers/handlers.go`:
- Around line 2266-2270: The call to s.secretService.CreateSecret builds a
secrets.SecretParams without the CorrelationID, so update the struct literal
passed to CreateSecret to include the correlation ID (e.g., CorrelationID:
correlationID or CorrelationID: log.CorrelationID()) so service logging/tracing
receives it; locate the call to s.secretService.CreateSecret and add the
CorrelationID field to the secrets.SecretParams being constructed.
- Around line 2389-2421: UpdateSecret currently ignores the path parameter id
and relies only on the request body; modify UpdateSecret to parse the incoming
body to extract the secret handle/name (same place UpdateAPI validates id vs
body) and verify it matches the id path parameter before calling
s.secretService.UpdateSecret; if they differ return a 400 error indicating
path/body mismatch, otherwise proceed as before (ensure the validation
references the id parameter and the parsed metadata.name from the request body
and keep the existing logging and error handling).
- Around line 2286-2291: The CreateSecret response currently includes
secret.Value in the c.JSON response (the c.JSON call that returns id, value,
created_at, updated_at); remove the plaintext secret value from that response so
it only returns the secret handle/ID and timestamps (e.g., "id", "created_at",
"updated_at") and do not expose secret.Value. If you need to signal success to
callers, return a creation-only token or confirmation flag instead of the full
secret, and ensure any retrieval of the secret is done via a separate secure
fetch endpoint.

In `@gateway/gateway-controller/pkg/encryption/aesgcm/keymgmt.go`:
- Around line 31-60: NewKeyManager currently silently overwrites duplicate
KeyConfig.Version entries when populating km.keys; update NewKeyManager to
detect duplicate versions before calling km.loadKey (or immediately after) and
return an error if a version already exists to fail fast. Specifically, in
NewKeyManager, check km.keys[config.Version] (or track seen versions) for an
existing key and return fmt.Errorf("duplicate key version: %s", config.Version)
if found; keep existing behavior for setting km.primaryKey and km.primaryVersion
when i==0 and log as before.

In `@gateway/gateway-controller/pkg/encryption/aesgcm/provider.go`:
- Around line 52-60: The Encrypt method may dereference a nil key because
p.keyManager.GetPrimaryKey() is used without checking for nil; update
AESGCMProvider.Encrypt to validate the returned key from
keyManager.GetPrimaryKey() (check if key == nil) before accessing key.Data and
return a clear error (e.g., "no primary key available") so callers receive a
controlled error instead of a panic; reference the AESGCMProvider.Encrypt
function and the keyManager.GetPrimaryKey()/key.Data usage when making the
change.

In `@gateway/gateway-controller/pkg/resolver/policy_resolver.go`:
- Around line 299-333: The resolveSecretTemplates function can panic when
pr.secretsService is nil; add a nil check at the start of resolveSecretTemplates
(or before any call to pr.secretsService.Get) and return a clear validation
error (e.g., "secrets service not configured") instead of attempting to call
Get; ensure you still handle templates with no secret service by returning an
error immediately and do not attempt to decrypt, referencing
resolveSecretTemplates, pr.secretsService, and the Get/decryptedSecret.Value
usage so reviewers can locate the change.
- Around line 220-223: The current error handling after calling
resolvedConfig.Configuration.Spec.FromAPIConfigData(apiData) swallows the
original error by appending an empty config.ValidationError; change it to
capture and propagate the actual error details by creating a
config.ValidationError (or appropriate error wrapper) that includes the returned
err message and context (e.g., source API data or field), and append that
populated ValidationError to the errors slice so logs and API responses surface
the root cause; update any callers or tests expecting the previous empty
ValidationError shape if necessary.

In `@gateway/gateway-controller/pkg/secrets/service.go`:
- Around line 258-260: In the if !exists error branch, fix the typo and
terminology by replacing the message fmt.Errorf("secret with id '%s' does not
exists", handle) with a grammatically correct and consistent message such as
fmt.Errorf("secret with handle '%s' does not exist", handle); ensure the
placeholder still uses the existing handle variable and preserve the fmt.Errorf
usage.
- Around line 17-20: Add validation in CreateSecret to enforce the MaxSecretSize
limit: before encrypting or storing the secret, check the byte length of
secretConfig.Spec.Value and return a clear error (e.g., ErrSecretTooLarge or
fmt.Errorf) if it exceeds MaxSecretSize. Reference the MaxSecretSize constant
and the CreateSecret function and ensure the check occurs prior to any
encryption or memory-heavy operations on secretConfig.Spec.Value so oversized
secrets are rejected early.

In `@gateway/gateway-controller/pkg/storage/interface.go`:
- Around line 247-255: The interface methods DeleteSecret and SecretExists
currently use the parameter name id string but implementations and callers use
handle (user-provided identifier) and query the Secret.handle column; update the
interface signatures to use handle string instead of id string and adjust their
doc comments to reference "handle" to keep names consistent with implementations
(e.g., the SQLite methods that accept handle string). Ensure only the parameter
name changes (not types) so existing callers compile.

In `@gateway/gateway-controller/pkg/storage/sqlite.go`:
- Around line 1689-1713: In GetSecrets on SQLiteStorage, ensure the sql.Rows are
properly closed and any iteration error is checked: after s.db.Query(query)
defer rows.Close(), iterate as before, then after the loop check if err :=
rows.Err(); err != nil and return fmt.Errorf("failed during rows iteration: %w",
err) if non-nil; keep the existing error handling for Scan and the final return
of ids.
- Around line 1645-1687: The SaveSecret implementation currently does a
pre-check using SecretExists which creates a TOCTOU race; remove the pre-check
and rely on the DB INSERT to enforce uniqueness, then map unique-constraint DB
errors to ErrConflict (add a helper like isSecretUniqueConstraintError that
detects "UNIQUE constraint failed: secrets.handle" and "UNIQUE constraint
failed: secrets.id") inside SaveSecret so when s.db.Exec returns a
unique-constraint error you return fmt.Errorf("%w: secret with id '%s' already
exists", ErrConflict, secret.Handle) and otherwise log/return the original
error; keep the existing logging and field names (secret.Handle,
secret.Provider, secret.KeyVersion) and ensure SecretExists is no longer called
from SaveSecret.

In `@gateway/it/features/health.feature`:
- Around line 27-34: Feature uses the step "the healthy response status code
should be 200" but no step definition is registered; add a step definition that
either registers that exact phrase or maps it to the existing generic step in
assert_steps.go ("the response status code should be (\\d+)"). Implement a new
step binding (e.g., in the same steps file as other assertions) that calls the
existing assertion helper used by the generic step so both feature lines
(gateway health and router ready) validate status 200 without duplicating logic.

In `@gateway/it/features/secrets.feature`:
- Around line 282-289: Update the scenario title to match the actual limit
asserted in the test: change "Create secret with oversized value exceeding 10KB
limit" to "Create secret with oversized value exceeding 8192 characters (8KB)
limit" so the description aligns with the asserted error message "Secret value
must be at most 8192 characters"; ensure the scenario name in the feature file
and any related documentation use 8192 characters / 8KB consistently.
- Around line 484-502: The test assertion in the "Update non-existent secret
returns not found" scenario is checking for the brittle string "does not
exists"; update the expectation to match the API contract by asserting the
response body contains the canonical message "Secret not found." (ensure the
JSON "status" remains "error" and the status code expectation stays 404 in the
same scenario).
- Around line 80-86: The post-delete retrieval uses the wrong secret ID
("wso2-openai-keye") so the test doesn't confirm deletion; update the retrieval
step to use the correct ID "wso2-openai-key" (the same ID used in the delete
step) so the scenario checks for a 404 after deletion (look for the
Given/When/Then lines referencing "wso2-openai-keye" and replace it with
"wso2-openai-key").
🧹 Nitpick comments (8)
gateway/gateway-controller/pkg/config/secret_validator.go (1)

35-36: Unused supportedKinds field.

The supportedKinds field is initialized but never used. Line 81 hardcodes "Secret" instead of checking against supportedKinds. Either use the field or remove it for consistency with supportedTypes.

♻️ Option 1: Use the field
 // Validate kind
-if config.Kind != "Secret" {
+if !slices.Contains(v.supportedKinds, config.Kind) {
     errors = append(errors, ValidationError{
         Field:   "kind",
-        Message: "Unsupported configuration kind (only 'Secret' is supported)",
+        Message: fmt.Sprintf("Unsupported configuration kind (supported kinds: %s)", strings.Join(v.supportedKinds, ", ")),
     })
 }
♻️ Option 2: Remove the unused field
 type SecretValidator struct {
     // urlFriendlyNameRegex matches URL-safe characters for secret display names
     urlFriendlyNameRegex *regexp.Regexp
-
-    // supportedKinds defines supported Secret kinds
-    supportedKinds []string

     // supportedTypes defines supported secret types
     supportedTypes []string
 }

 func NewSecretValidator() *SecretValidator {
     return &SecretValidator{
         urlFriendlyNameRegex: regexp.MustCompile(`^[a-zA-Z0-9\-_. ]+$`),
-        supportedKinds:       []string{"Secret"},
         supportedTypes:       []string{"default"},
     }
 }

Also applies to: 46-46, 80-86

gateway/gateway-controller/pkg/config/config.go (2)

308-323: Add validation for encryption providers and keys.

Consider validating provider type, non-empty key lists, unique key versions, and non-empty file paths in Config.Validate() to fail fast on misconfiguration.


505-517: Default encryption key path is shipped in Docker images but relies on filesystem availability.

The key file is properly copied into the Docker image (COPY aesgcm-keys /app/aesgcm-keys), so the default config works for standard deployments. However, the initialization is conditional only on whether providers are configured—not on whether key files exist. If anyone runs the binary outside Docker or with a custom config that includes the default provider configuration but lacks the key files, startup will fail with log.Fatal.

Consider making encryption providers opt-in with empty defaults, or document that key files must be present when the default provider config is used in non-Docker deployments.

gateway/gateway-controller/pkg/encryption/aesgcm/provider.go (1)

99-102: Ciphertext length validation is incomplete.

The check validates len(payload.Ciphertext) < NonceSize, but AES-GCM also requires at least the authentication tag (16 bytes). A ciphertext shorter than NonceSize + gcm.Overhead() would fail during gcm.Open() anyway, but an explicit check provides clearer error messages.

♻️ Suggested improvement
-	// Validate ciphertext length (must be at least nonce size + tag size)
-	if len(payload.Ciphertext) < NonceSize {
-		return nil, fmt.Errorf("ciphertext too short: %d bytes", len(payload.Ciphertext))
+	// Validate ciphertext length (must be at least nonce size + tag size)
+	// AES-GCM tag is 16 bytes
+	minLength := NonceSize + 16
+	if len(payload.Ciphertext) < minLength {
+		return nil, fmt.Errorf("ciphertext too short: %d bytes (minimum %d)", len(payload.Ciphertext), minLength)
 	}
gateway/gateway-controller/pkg/storage/interface.go (1)

236-239: Doc comment/parameter naming inconsistency.

The GetSecret method's doc says "retrieves a secret by ID" but the parameter is named handle. Looking at the Secret model (which has both ID and Handle fields), this could cause confusion. The doc should clarify that this retrieves by Handle (user-provided identifier), not the internal UUID ID.

📝 Suggested doc fix
-	// GetSecret retrieves a secret by ID.
+	// GetSecret retrieves a secret by handle.
 	//
-	// Returns error if the secret does not exist.
+	// Returns error if the secret with the given handle does not exist.
 	GetSecret(handle string) (*models.Secret, error)
gateway/gateway-controller/pkg/api/handlers/handlers.go (1)

2315-2318: Redundant loop when copying slice.

The loop copies elements from ids to secretsList one by one, but since both are []string, you can simplify this.

♻️ Simplified code
-	secretsList := make([]string, 0, len(ids))
-	for _, id := range ids {
-		secretsList = append(secretsList, id)
-	}
+	secretsList := ids

Or if a copy is intentionally needed:

secretsList := make([]string, len(ids))
copy(secretsList, ids)
gateway/gateway-controller/pkg/secrets/service.go (1)

102-111: Minor: time.Now().UTC() called twice.

Both CreatedAt and UpdatedAt should have the same timestamp for a new secret. Consider storing the result in a variable.

♻️ Suggested improvement
+	now := time.Now().UTC()
 	// Create secret model
 	secret := &models.Secret{
 		ID:         generateUUID(),
 		Handle:     handle,
 		Value:      "", // Don't store plaintext
 		Provider:   payload.Provider,
 		KeyVersion: payload.KeyVersion,
 		Ciphertext: []byte(ciphertext),
-		CreatedAt:  time.Now().UTC(),
-		UpdatedAt:  time.Now().UTC(),
+		CreatedAt:  now,
+		UpdatedAt:  now,
 	}
gateway/gateway-controller/pkg/encryption/manager.go (1)

103-107: Typo in comment: "curruption" → "corruption".

✏️ Fix typo
 	// Find the provider that can decrypt this payload
 	// If the provider name matches, use it to decrypt
 	// If no match, return error indicating no provider found
-	// If decryption fails, return error indicating curruption or invalid data
+	// If decryption fails, return error indicating corruption or invalid data

Comment on lines 2389 to 2460
// UpdateSecret handles PUT /secrets/{id}
func (s *APIServer) UpdateSecret(c *gin.Context, id string) {
log := middleware.GetLogger(c, s.logger)

// Read request body
body, err := io.ReadAll(c.Request.Body)
if err != nil {
log.Error("Failed to read request body", zap.Error(err))
c.JSON(http.StatusBadRequest, api.ErrorResponse{
Status: "error",
Message: "Failed to read request body",
})
return
}

// Get correlation ID from context
correlationID := middleware.GetCorrelationID(c)

// Delegate to service which parses/validates/encrypt and persists
secret, err := s.secretService.UpdateSecret(secrets.SecretParams{
Data: body,
ContentType: c.GetHeader("Content-Type"),
Logger: log,
})
if err != nil {
log.Error("Failed to encrypt Secret", zap.Error(err))
if strings.Contains(err.Error(), "does not exist") {
c.JSON(http.StatusNotFound, api.ErrorResponse{Status: "error", Message: err.Error()})
} else {
c.JSON(http.StatusBadRequest, api.ErrorResponse{Status: "error", Message: err.Error()})
}
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

UpdateSecret ignores the id path parameter.

The handler accepts id as a path parameter but never uses it. Instead, it relies solely on the metadata.name from the request body. This could lead to confusion where a PUT to /secrets/foo with a body containing metadata.name: bar would update bar instead of foo. Consider validating that the path id matches the body's handle, similar to UpdateAPI (lines 546-560).

🐛 Proposed fix
 func (s *APIServer) UpdateSecret(c *gin.Context, id string) {
 	log := middleware.GetLogger(c, s.logger)

 	// Read request body
 	body, err := io.ReadAll(c.Request.Body)
 	if err != nil {
 		log.Error("Failed to read request body", zap.Error(err))
 		c.JSON(http.StatusBadRequest, api.ErrorResponse{
 			Status:  "error",
 			Message: "Failed to read request body",
 		})
 		return
 	}

+	// Parse to validate handle matches path parameter
+	var secretConfig api.SecretConfiguration
+	if err := s.parser.Parse(body, c.GetHeader("Content-Type"), &secretConfig); err == nil {
+		if secretConfig.Metadata.Name != "" && secretConfig.Metadata.Name != id {
+			log.Warn("Handle mismatch between path and body",
+				zap.String("path_id", id),
+				zap.String("body_handle", secretConfig.Metadata.Name))
+			c.JSON(http.StatusBadRequest, api.ErrorResponse{
+				Status:  "error",
+				Message: fmt.Sprintf("Handle mismatch: path has '%s' but body metadata.name has '%s'", id, secretConfig.Metadata.Name),
+			})
+			return
+		}
+	}
+
 	// Get correlation ID from context
 	correlationID := middleware.GetCorrelationID(c)
🤖 Prompt for AI Agents
In `@gateway/gateway-controller/pkg/api/handlers/handlers.go` around lines 2389 -
2421, UpdateSecret currently ignores the path parameter id and relies only on
the request body; modify UpdateSecret to parse the incoming body to extract the
secret handle/name (same place UpdateAPI validates id vs body) and verify it
matches the id path parameter before calling s.secretService.UpdateSecret; if
they differ return a 400 error indicating path/body mismatch, otherwise proceed
as before (ensure the validation references the id parameter and the parsed
metadata.name from the request body and keep the existing logging and error
handling).

Comment on lines 282 to 293
Scenario: Create secret with oversized value exceeding 10KB limit
Given I authenticate using basic auth as "admin"
When I create a secret with oversized value
Then the response status code should be 400
And the response should be valid JSON
And the JSON response field "status" should be "error"
And the response body should contain "Secret value must be at most 8192 characters"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify the oversized-value limit in the scenario text.

The scenario title says “10KB,” but the asserted message is 8192 characters (8KB). Align the description with the actual limit.

✏️ Suggested wording fix
-  Scenario: Create secret with oversized value exceeding 10KB limit
+  Scenario: Create secret with oversized value exceeding 8KB limit
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Scenario: Create secret with oversized value exceeding 10KB limit
Given I authenticate using basic auth as "admin"
When I create a secret with oversized value
Then the response status code should be 400
And the response should be valid JSON
And the JSON response field "status" should be "error"
And the response body should contain "Secret value must be at most 8192 characters"
Scenario: Create secret with oversized value exceeding 8KB limit
Given I authenticate using basic auth as "admin"
When I create a secret with oversized value
Then the response status code should be 400
And the response should be valid JSON
And the JSON response field "status" should be "error"
And the response body should contain "Secret value must be at most 8192 characters"
🤖 Prompt for AI Agents
In `@gateway/it/features/secrets.feature` around lines 282 - 289, Update the
scenario title to match the actual limit asserted in the test: change "Create
secret with oversized value exceeding 10KB limit" to "Create secret with
oversized value exceeding 8192 characters (8KB) limit" so the description aligns
with the asserted error message "Secret value must be at most 8192 characters";
ensure the scenario name in the feature file and any related documentation use
8192 characters / 8KB consistently.

Comment on lines 484 to 506
Scenario: Update non-existent secret returns not found
Given I authenticate using basic auth as "admin"
When I update the secret "non-existent-update-secret-abc" with:
"""
apiVersion: gateway.api-platform.wso2.com/v1alpha1
kind: Secret
metadata:
name: non-existent-update-secret-abc
spec:
displayName: Non-existent Secret
description: Attempting to update non-existent secret
type: default
value: new-value
"""
Then the response status code should be 404
And the response should be valid JSON
And the JSON response field "status" should be "error"
And the response body should contain "does not exists"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align the not-found error text with the API contract.

The spec examples use “Secret not found.” This assertion uses “does not exists,” which risks brittle tests.

✏️ Suggested fix
-    And the response body should contain "does not exists"
+    And the response body should contain "not found"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Scenario: Update non-existent secret returns not found
Given I authenticate using basic auth as "admin"
When I update the secret "non-existent-update-secret-abc" with:
"""
apiVersion: gateway.api-platform.wso2.com/v1alpha1
kind: Secret
metadata:
name: non-existent-update-secret-abc
spec:
displayName: Non-existent Secret
description: Attempting to update non-existent secret
type: default
value: new-value
"""
Then the response status code should be 404
And the response should be valid JSON
And the JSON response field "status" should be "error"
And the response body should contain "does not exists"
Scenario: Update non-existent secret returns not found
Given I authenticate using basic auth as "admin"
When I update the secret "non-existent-update-secret-abc" with:
"""
apiVersion: gateway.api-platform.wso2.com/v1alpha1
kind: Secret
metadata:
name: non-existent-update-secret-abc
spec:
displayName: Non-existent Secret
description: Attempting to update non-existent secret
type: default
value: new-value
"""
Then the response status code should be 404
And the response should be valid JSON
And the JSON response field "status" should be "error"
And the response body should contain "not found"
🤖 Prompt for AI Agents
In `@gateway/it/features/secrets.feature` around lines 484 - 502, The test
assertion in the "Update non-existent secret returns not found" scenario is
checking for the brittle string "does not exists"; update the expectation to
match the API contract by asserting the response body contains the canonical
message "Secret not found." (ensure the JSON "status" remains "error" and the
status code expectation stays 404 in the same scenario).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
gateway/gateway-controller/pkg/storage/gateway-controller-db.sql (1)

146-168: Add secrets table creation to version 5 schema migration.

The secrets table (defined in the SQL file) is missing from the version 5 migration in sqlite.go. Existing databases upgrading from v4 to v5 will not create this table, causing runtime failures when code attempts to use SaveSecret, GetSecrets, and other secrets operations. New installations will work correctly since they load the full schema from the SQL file initially.

Add the secrets table and idx_secrets_updated_at index to the if version == 4 migration block in the initSchema() function before setting PRAGMA user_version = 5.

gateway/gateway-controller/cmd/controller/main.go (1)

122-179: Fail fast or disable /secrets when encryption isn’t configured.

secretsService stays nil when no encryption providers are configured (or in memory-only mode), yet the API server always registers /secrets endpoints and the policy resolver depends on it. This will panic at runtime. Consider failing startup with a clear message or wiring a disabled service/conditional routes.

🛠️ Example fail-fast guard
 	// Initialize encryption providers for secret management
 	var encryptionProviderManager *encryption.ProviderManager
 	var secretsService *secrets.SecretService
+
+	if len(cfg.GatewayController.Encryption.Providers) == 0 {
+		log.Fatal("Secret management requires at least one encryption provider to be configured")
+	}
+	if !cfg.IsPersistentMode() {
+		log.Fatal("Secret management requires persistent storage")
+	}

Also applies to: 364-365

🤖 Fix all issues with AI agents
In `@gateway/gateway-controller/api/openapi.yaml`:
- Around line 3688-3691: SecretConfigData.value is documented as "never
returned" but SecretResponse and examples currently include plaintext; remove
value from the SecretResponse schema and from all response examples (and any
response example objects under the SecretResponse component) so responses never
expose secret values, and instead mark SecretConfigData.value as writeOnly: true
(or keep it only in request schemas) so it can be accepted on create/update but
never emitted; update any related refs/usages and example blocks (including the
other occurrence at the 3832-3847 region) to reflect the removal.

In `@gateway/gateway-controller/pkg/api/handlers/handlers.go`:
- Around line 2249-2268: The handlers call s.secretService (e.g., CreateSecret)
without guarding for nil which causes panics when the encryption
provider/service is not configured; add a nil-check at the start of CreateSecret
that returns HTTP 503 (Service Unavailable) with an api.ErrorResponse and a
descriptive message when s.secretService == nil, and apply the same guard to the
other secret handlers (Get, Update, Delete, List) so they consistently return
503 instead of dereferencing a nil service.

In `@gateway/gateway-controller/pkg/encryption/aesgcm/provider.go`:
- Around line 99-102: The length check on payload.Ciphertext is incomplete:
update the validation in the provider (where payload.Ciphertext and NonceSize
are used) to ensure the ciphertext length is at least NonceSize plus the GCM
authentication tag size (e.g., TagSize or 16 bytes) by checking
len(payload.Ciphertext) >= NonceSize + TagSize, and update the error message to
reflect the required minimum (nonce + tag) when returning the fmt.Errorf about
"ciphertext too short".

In `@gateway/gateway-controller/pkg/encryption/manager.go`:
- Around line 95-101: The Decrypt method on ProviderManager dereferences payload
(payload.Provider, payload.KeyVersion) before checking for nil; add an early
nil-guard at the start of ProviderManager.Decrypt that returns a clear error
(e.g., fmt.Errorf or errors.New) when payload == nil, and log the error via
m.logger before returning so callers don't panic when passing a nil
*EncryptedPayload; update any tests that call ProviderManager.Decrypt to assert
the new error path if needed.

In `@gateway/gateway-controller/pkg/resolver/policy_resolver.go`:
- Around line 118-125: ResolvePolicies currently unconditionally calls
apiConfig.Configuration.Spec.AsAPIConfigData(), which fails for async/webhook
configs; update ResolvePolicies to first detect the API kind (e.g., inspect
apiConfig.Configuration.Spec.Type or check for a non-nil Rest-specific field)
and only call AsAPIConfigData for REST APIs, skipping policy resolution for
other kinds (or add a separate webhook/async handling branch) so that non-Rest
configs do not produce validation errors and are not skipped incorrectly; locate
the logic around ResolvePolicies and apiConfig.Configuration.Spec and implement
the guard/alternate path there.

In `@gateway/gateway-controller/pkg/secrets/service.go`:
- Around line 59-63: The code reads secretConfig.Metadata.Name into handle
before checking the parse error; move the extraction of handle (from
secretConfig.Metadata.Name) to after verifying err == nil (i.e., after the
s.parser.Parse call and its error check), or alternatively check err first and
only access secretConfig.Metadata.Name when parsing succeeded so handle cannot
be empty/invalid; update the block around s.parser.Parse, the err check, and the
handle assignment accordingly.
- Around line 224-229: In UpdateSecret the code reads secretConfig.Metadata.Name
into handle before checking the Parse error; move the extraction of handle (the
access to secretConfig.Metadata.Name) to after verifying err == nil so you only
use secretConfig when Parse succeeds, i.e. call s.parser.Parse(params.Data,
params.ContentType, &secretConfig), check if err != nil and return the wrapped
error, and only then set handle from secretConfig.Metadata.Name; reference
s.parser.Parse and secretConfig.Metadata.Name in UpdateSecret to locate the fix.
♻️ Duplicate comments (16)
gateway/gateway-controller/pkg/storage/interface.go (1)

247-255: Interface parameter names inconsistent with implementation.

The DeleteSecret and SecretExists methods declare parameters as id string, but the SQLite implementations and the Secret model use handle as the user-provided identifier. The GetSecret method correctly uses handle string.

gateway/it/features/secrets.feature (3)

84-86: Fix the typo in the post-delete retrieval ID.

Line 85 uses "wso2-openai-keye" instead of "wso2-openai-key", so this test doesn't validate that the deleted secret returns 404.


282-288: Scenario title mismatches the actual limit.

The scenario says "10KB limit" but the asserted error message is "8192 characters" (8KB).


498-501: Error message text may be inconsistent with API contract.

The assertion checks for "does not exists" which has a grammatical error and may not match the actual API response format.

gateway/gateway-controller/pkg/encryption/aesgcm/provider.go (1)

52-60: Potential nil pointer dereference if GetPrimaryKey() returns nil.

Encrypt calls p.keyManager.GetPrimaryKey() without checking for nil before accessing key.Data. While HealthCheck validates this, Encrypt could be called independently.

gateway/gateway-controller/pkg/storage/sqlite.go (2)

1645-1687: TOCTOU race condition in SaveSecret.

The SecretExists + INSERT pattern creates a race where concurrent creates can bypass the existence check and hit the unique constraint, surfacing as 500 errors instead of proper 409 conflict responses. Other methods in this file (SaveConfig, SaveCertificate) use constraint-error-mapping which is race-safe.


1689-1713: Missing defer rows.Close() and rows.Err() check in GetSecrets.

The result set isn't closed, which can leak database resources. Also missing the post-iteration error check.

gateway/gateway-controller/pkg/encryption/aesgcm/keymgmt.go (1)

41-48: Duplicate key versions are silently overwritten.

If two KeyConfig entries share the same Version, the later entry overwrites the former without warning, potentially breaking decryption of secrets encrypted with the overwritten key.

gateway/gateway-controller/pkg/secrets/service.go (2)

17-20: MaxSecretSize constant is defined but never enforced.

The constant suggests a 10KB limit, but no validation checks the size of secretConfig.Spec.Value before encryption. Large secrets could consume excessive memory.


258-260: Typo in error message and inconsistent terminology.

The error message "does not exists" has a grammatical error and uses "id" while the variable is handle.

gateway/gateway-controller/pkg/api/handlers/handlers.go (3)

2267-2272: Propagate correlation ID into SecretParams.

The correlation ID is read but not passed to the secret service, so service logs/tracing lose it.

🛠️ Suggested fix
 	secret, err := s.secretService.CreateSecret(secrets.SecretParams{
-		Data:        body,
-		ContentType: c.GetHeader("Content-Type"),
-		Logger:      log,
+		Data:          body,
+		ContentType:   c.GetHeader("Content-Type"),
+		CorrelationID: correlationID,
+		Logger:        log,
 	})
 	secret, err := s.secretService.UpdateSecret(secrets.SecretParams{
-		Data:        body,
-		ContentType: c.GetHeader("Content-Type"),
-		Logger:      log,
+		Data:          body,
+		ContentType:   c.GetHeader("Content-Type"),
+		CorrelationID: correlationID,
+		Logger:        log,
 	})

Also applies to: 2409-2414


2287-2292: Do not return plaintext secret values in API responses.

Line 2289/2385/2432 returns secret.Value in plaintext. This is a critical security exposure—return only opaque IDs and timestamps.

🛠️ Suggested fix (apply to Create/Get/Update responses)
 	c.JSON(http.StatusCreated, gin.H{
 		"id":         secret.Handle,
-		"value":      secret.Value,
 		"created_at": secret.CreatedAt,
 		"updated_at": secret.UpdatedAt,
 	})

Also applies to: 2382-2387, 2430-2434


2391-2414: Validate path id matches body handle in UpdateSecret.

The handler ignores the id path parameter and relies only on the body. This can update the wrong secret (e.g., /secrets/foo with body metadata.name: bar).

🛠️ Suggested fix
 	// Read request body
 	body, err := io.ReadAll(c.Request.Body)
 	if err != nil {
 		...
 	}
+
+	// Parse to validate handle matches path parameter
+	var secretConfig api.SecretConfiguration
+	if err := s.parser.Parse(body, c.GetHeader("Content-Type"), &secretConfig); err == nil {
+		if secretConfig.Metadata.Name != "" && secretConfig.Metadata.Name != id {
+			log.Warn("Handle mismatch between path and body",
+				zap.String("path_id", id),
+				zap.String("body_handle", secretConfig.Metadata.Name))
+			c.JSON(http.StatusBadRequest, api.ErrorResponse{
+				Status:  "error",
+				Message: fmt.Sprintf("Handle mismatch: path has '%s' but body metadata.name has '%s'", id, secretConfig.Metadata.Name),
+			})
+			return
+		}
+	}
gateway/gateway-controller/api/openapi.yaml (1)

3965-3981: Declare the secrets tag in the global tags list.

The /secrets operations use the secrets tag, but it’s not defined under tags:.

🛠️ Suggested addition
 tags:
   - name: System
     description: System health and status endpoints
+  - name: secrets
+    description: Manage encrypted secrets for gateway configuration
   - name: API Management
     description: CRUD operations for API configurations
gateway/gateway-controller/pkg/resolver/policy_resolver.go (2)

220-223: Include error details when rebuilding the spec fails.

Appending an empty ValidationError hides the root cause.

🛠️ Suggested fix
 	err = resolvedConfig.Configuration.Spec.FromAPIConfigData(apiData)
 	if err != nil {
-		errors = append(errors, config.ValidationError{})
+		errors = append(errors, config.ValidationError{
+			Field:   "spec",
+			Message: fmt.Sprintf("Failed to rebuild API spec after policy resolution: %v", err),
+		})
 	}

302-320: Guard against nil secretsService in template resolution.

If secretsService isn’t configured, any $secret{} template will panic.

🛠️ Suggested fix
 func (pr *PolicyResolver) resolveSecretTemplates(templateStr string) (string, error) {
+	if pr.secretsService == nil {
+		return "", fmt.Errorf("secrets service is not configured")
+	}
 	// Pattern to match $secret{key}
 	secretPattern := `\$secret\{([^}]+)\}`
🧹 Nitpick comments (5)
gateway/gateway-controller/pkg/config/config.go (2)

62-63: Add validation for encryption provider config.
New config types introduce provider/key settings, but Validate() doesn’t check provider type, key list, or empty file paths. A fail-fast validation step would prevent opaque runtime errors.

Also applies to: 308-323


505-517: Static default encryption key in version control poses a security risk for production.

The hardcoded key path ./aesgcm-keys/default-aesgcm256-v1.bin points to a default key checked into the repository. While file existence is validated at startup and the path is configurable via GATEWAY_* environment variables or config file overrides, using an identical key across all deployments—especially one committed to version control—weakens security in production. Consider removing or replacing the default key with a per-instance provisioned key, and document the requirement to override via configuration in production deployments.

gateway/gateway-controller/pkg/storage/interface.go (1)

231-234: Return type mismatch with method semantics.

GetSecrets() returns []string (handles only), which is inconsistent with other "GetAll" methods in this interface that return full model objects. Consider renaming to GetSecretHandles() or ListSecretHandles() for clarity, or return []*models.Secret for consistency with GetAllConfigs(), GetAllLLMProviderTemplates(), etc.

gateway/gateway-controller/pkg/storage/sqlite.go (1)

1734-1736: Use errors.Is for error comparison.

Direct comparison err == sql.ErrNoRows should use errors.Is(err, sql.ErrNoRows) for consistency with other methods in this file and to handle wrapped errors correctly.

♻️ Proposed fix
-	if err == sql.ErrNoRows {
+	if errors.Is(err, sql.ErrNoRows) {
 		return nil, fmt.Errorf("%w: id=%s", ErrNotFound, handle)
 	}
gateway/gateway-controller/pkg/encryption/aesgcm/keymgmt.go (1)

102-105: Consider zeroing key data on manager shutdown.

The Key.Data byte slice holds sensitive cryptographic material in memory. For defense-in-depth, consider adding a method to securely zero the key data when the manager is no longer needed (though Go's GC makes this less critical than in languages with manual memory management).

Comment on lines 3688 to 3622
value:
type: string
description: Secret value (stored securely and never returned in API responses)
minLength: 1
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align secret value documentation with the response schema.

SecretConfigData.value says it’s never returned, but SecretResponse includes value and examples show plaintext. Decide on the intended behavior and update the schema/examples accordingly (prefer removing value from responses for security).

Also applies to: 3832-3847

🤖 Prompt for AI Agents
In `@gateway/gateway-controller/api/openapi.yaml` around lines 3688 - 3691,
SecretConfigData.value is documented as "never returned" but SecretResponse and
examples currently include plaintext; remove value from the SecretResponse
schema and from all response examples (and any response example objects under
the SecretResponse component) so responses never expose secret values, and
instead mark SecretConfigData.value as writeOnly: true (or keep it only in
request schemas) so it can be accepted on create/update but never emitted;
update any related refs/usages and example blocks (including the other
occurrence at the 3832-3847 region) to reflect the removal.

Comment on lines 95 to 122
// Decrypt decrypts the payload using the provider chain
// It tries to match the provider by name from the payload metadata
func (m *ProviderManager) Decrypt(payload *EncryptedPayload) ([]byte, error) {
m.logger.Debug("Decrypting payload",
zap.String("provider", payload.Provider),
zap.String("key_version", payload.KeyVersion),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against nil payloads in Decrypt.

payload is dereferenced on Line 98 before any nil check, so a nil input will panic. Add an early guard and return a clear error.

🛠️ Suggested fix
 func (m *ProviderManager) Decrypt(payload *EncryptedPayload) ([]byte, error) {
+	if payload == nil {
+		return nil, fmt.Errorf("payload is nil")
+	}
 	m.logger.Debug("Decrypting payload",
 		zap.String("provider", payload.Provider),
 		zap.String("key_version", payload.KeyVersion),
 	)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Decrypt decrypts the payload using the provider chain
// It tries to match the provider by name from the payload metadata
func (m *ProviderManager) Decrypt(payload *EncryptedPayload) ([]byte, error) {
m.logger.Debug("Decrypting payload",
zap.String("provider", payload.Provider),
zap.String("key_version", payload.KeyVersion),
)
// Decrypt decrypts the payload using the provider chain
// It tries to match the provider by name from the payload metadata
func (m *ProviderManager) Decrypt(payload *EncryptedPayload) ([]byte, error) {
if payload == nil {
return nil, fmt.Errorf("payload is nil")
}
m.logger.Debug("Decrypting payload",
zap.String("provider", payload.Provider),
zap.String("key_version", payload.KeyVersion),
)
🤖 Prompt for AI Agents
In `@gateway/gateway-controller/pkg/encryption/manager.go` around lines 95 - 101,
The Decrypt method on ProviderManager dereferences payload (payload.Provider,
payload.KeyVersion) before checking for nil; add an early nil-guard at the start
of ProviderManager.Decrypt that returns a clear error (e.g., fmt.Errorf or
errors.New) when payload == nil, and log the error via m.logger before returning
so callers don't panic when passing a nil *EncryptedPayload; update any tests
that call ProviderManager.Decrypt to assert the new error path if needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@gateway/gateway-controller/api/openapi.yaml`:
- Around line 3688-3693: The OpenAPI schema for SecretConfigData.value currently
sets maxLength: 8192 causing a mismatch with the 10KB limit enforced elsewhere;
update the OpenAPI definition (symbol: SecretConfigData.value in openapi.yaml)
to set maxLength: 10240 to match the 10KB constraint and ensure any related
examples/validators reference the same 10240 byte/character limit so client-side
validation aligns with server rules.

In `@gateway/gateway-controller/pkg/config/secret_validator.go`:
- Around line 76-82: The validation currently reports the wrong field name: when
checking config.ApiVersion against
api.SecretConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, change the
ValidationError Field from "version" to "apiVersion" so the error references the
actual schema key; update the ValidationError created in the ApiVersion check
(where config.ApiVersion is compared) to set Field: "apiVersion" and keep the
existing Message text.

In `@gateway/gateway-controller/pkg/storage/sqlite.go`:
- Around line 1664-1666: The error messages use the word "id" but these paths
operate on handles; update all messages to reference "handle" instead. Replace
occurrences like fmt.Errorf("%w: secret with id '%s' already exists",
ErrConflict, secret.Handle) with wording that says "secret with handle '%s'
already exists" (using ErrConflict and secret.Handle), and make the same wording
change for the other similar error/return strings referenced (around the UNIQUE
constraint check "UNIQUE constraint failed: secrets.handle" and the other
occurrences flagged at 1737-1739, 1788-1790, 1819-1821, 1830-1831) so all
secret-related errors consistently say "handle" instead of "id".
♻️ Duplicate comments (3)
gateway/gateway-controller/pkg/storage/interface.go (1)

225-255: Docs should reference handle, not id.

The method parameters are handle, but comments still say “ID”. Align wording to avoid confusion.

🔧 Proposed fix
-	// Returns an error if a secret with the same ID already exists.
+	// Returns an error if a secret with the same handle already exists.
@@
-	// GetSecret retrieves a secret by ID.
+	// GetSecret retrieves a secret by handle.
@@
-	// DeleteSecret permanently removes a secret.
+	// DeleteSecret permanently removes a secret by handle.
@@
-	// SecretExists checks if a secret with the given handle exists.
+	// SecretExists checks if a secret with the given handle exists.
gateway/gateway-controller/pkg/api/handlers/handlers.go (1)

2416-2459: UpdateSecret still ignores the id path parameter.

The handler accepts id as a path parameter but only validates against the existing secret, not against the body's metadata.name. A PUT to /secrets/foo with body containing metadata.name: bar would check if foo exists, then store with the body's handle, leading to confusing behavior. Validate that the path id matches the body's handle, similar to UpdateAPI (lines 548-561).

🐛 Proposed fix
 func (s *APIServer) UpdateSecret(c *gin.Context, id string) {
 	log := middleware.GetLogger(c, s.logger)

 	// Read request body
 	body, err := io.ReadAll(c.Request.Body)
 	if err != nil {
 		log.Error("Failed to read request body", zap.Error(err))
 		c.JSON(http.StatusBadRequest, api.ErrorResponse{
 			Status:  "error",
 			Message: "Failed to read request body",
 		})
 		return
 	}

+	// Parse to validate handle matches path parameter
+	var secretConfig api.SecretConfiguration
+	if err := s.parser.Parse(body, c.GetHeader("Content-Type"), &secretConfig); err == nil {
+		if secretConfig.Metadata.Name != "" && secretConfig.Metadata.Name != id {
+			log.Warn("Handle mismatch between path and body",
+				zap.String("path_id", id),
+				zap.String("body_handle", secretConfig.Metadata.Name))
+			c.JSON(http.StatusBadRequest, api.ErrorResponse{
+				Status:  "error",
+				Message: fmt.Sprintf("Handle mismatch: path has '%s' but body metadata.name has '%s'", id, secretConfig.Metadata.Name),
+			})
+			return
+		}
+	}
+
 	// Get correlation ID from context
 	correlationID := middleware.GetCorrelationID(c)
gateway/gateway-controller/pkg/secrets/service.go (1)

17-20: MaxSecretSize constant is defined but never enforced.

The constant suggests a 10KB limit on secret values, but no validation checks the size of the secret value before encryption. Large secrets could consume excessive memory during encryption or storage.

🐛 Proposed enforcement in CreateSecret
 func (s *SecretService) CreateSecret(params SecretParams) (*models.Secret, error) {
 	var secretConfig api.SecretConfiguration
 	// Parse configuration
 	err := s.parser.Parse(params.Data, params.ContentType, &secretConfig)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse configuration: %w", err)
 	}
 	handle := secretConfig.Metadata.Name

+	// Validate secret size
+	if len(secretConfig.Spec.Value) > MaxSecretSize {
+		return nil, fmt.Errorf("secret value exceeds maximum size of %d bytes", MaxSecretSize)
+	}
+
 	// Validate configuration
🧹 Nitpick comments (5)
gateway/gateway-controller/api/openapi.yaml (1)

3789-3831: Clarify which request schema is authoritative for secrets.

SecretCreateRequest and SecretUpdateRequest are defined but /secrets POST/PUT use SecretConfiguration. Consider either referencing these schemas in the endpoints or removing them to avoid confusion for clients and SDK generators.

gateway/gateway-controller/pkg/api/handlers/handlers.go (1)

1655-1672: Policy resolution error handling looks reasonable but consider logging correlation ID.

The error aggregation and fallback to original config on resolution failure is appropriate. However, consider adding a correlation ID to the error log for traceability since this runs in the context of API creation/update operations.

gateway/gateway-controller/pkg/encryption/manager.go (1)

165-170: Add nil guard in MarshalPayload for defensive coding.

MarshalPayload dereferences payload.Ciphertext without a nil check. While callers should ensure non-nil payloads, a guard would prevent panics from programming errors.

♻️ Proposed fix
 func MarshalPayload(payload *EncryptedPayload) string {
+	if payload == nil {
+		return ""
+	}
 	encoded := base64.StdEncoding.EncodeToString(payload.Ciphertext)
 	return fmt.Sprintf("enc:%s:v1:%s:%s", payload.Provider, payload.KeyVersion, encoded)
 }
gateway/gateway-controller/pkg/resolver/policy_resolver.go (1)

310-317: Consider pre-compiling the regex pattern.

The secretPattern regex is compiled on every call to resolveSecretTemplates. For APIs with many policies containing secret templates, this adds unnecessary overhead.

♻️ Proposed fix - move regex to package level
+var secretPatternRe = regexp.MustCompile(`\$secret\{([^}]+)\}`)
+
 func (pr *PolicyResolver) resolveSecretTemplates(templateStr string) (string, error) {
 	if pr.secretsService == nil {
 		return "", fmt.Errorf("secret service is not initialized properly")
 	}

-	// Pattern to match $secret{key}
-	secretPattern := `\$secret\{([^}]+)\}`
-	re := regexp.MustCompile(secretPattern)
-
 	var resolveErr error
-	resolved := re.ReplaceAllStringFunc(templateStr, func(match string) string {
+	resolved := secretPatternRe.ReplaceAllStringFunc(templateStr, func(match string) string {
 		// Extract the secret key from $secret{key}
-		matches := re.FindStringSubmatch(match)
+		matches := secretPatternRe.FindStringSubmatch(match)
gateway/gateway-controller/pkg/secrets/service.go (1)

252-259: Error message terminology is slightly inconsistent.

Line 258 uses "secret configuration not found: id=%s" but the variable is handle. Consider using consistent terminology.

✏️ Proposed fix
 	if !exists {
-		return nil, fmt.Errorf("secret configuration not found: id=%s", handle)
+		return nil, fmt.Errorf("secret with handle '%s' not found", handle)
 	}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@gateway/gateway-controller/api/openapi.yaml`:
- Around line 1707-1711: The OpenAPI docs for operationId listSecrets claim the
endpoint returns secret metadata but SecretListResponse.secrets currently
contains only string IDs; either change SecretListResponse to return full
metadata objects (e.g., SecretMetadata with name, createdAt, labels, etc.) and
update the schema reference used by listSecrets, or update the listSecrets
description to state it returns only secret identifiers. Locate and modify the
schema named SecretListResponse and the path operation listSecrets (and the
analogous definitions at the other occurrence) so the response schema and the
description are consistent.
- Around line 1740-1748: The OpenAPI spec currently mixes two incompatible
request contracts for secret create/update: the schema refs under requestBody
point to SecretConfiguration while the examples and separate schemas
SecretCreateRequest/SecretUpdateRequest expect a simple {id, value} payload;
choose one contract and make the spec consistent across all occurrences (e.g.,
update the requestBody refs to use `#/components/schemas/SecretCreateRequest` and
SecretUpdateRequest and align examples, or remove
SecretCreateRequest/SecretUpdateRequest and change examples to match
SecretConfiguration). Update every instance mentioned (including the refs around
lines matching 1740–1748, 1771–1776, 1905–1913, 3789–3831) so the
requestBody.schema, examples, and error examples all reference the same schema
symbol (SecretConfiguration OR SecretCreateRequest/SecretUpdateRequest) and
remove any unused schemas to avoid codegen mismatches.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@gateway/gateway-controller/api/openapi.yaml`:
- Around line 1764-1822: Update the OpenAPI error response examples to match the
ErrorResponse schema: replace the top-level "error" field with "status" and
remove any "correlation_id" entries in each example (e.g., examples named
missing-id, invalid-value, unauthorized, duplicate, encryption-error and all
other error examples referencing ErrorResponse). Keep the "message" field as-is
and ensure each example conforms to the ErrorResponse shape (status, message,
errors) used elsewhere; apply this change consistently for all error response
blocks that reference the ErrorResponse schema.

Comment on lines 1764 to 1786
'400':
description: Bad request - missing or invalid fields
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
missing-id:
summary: Missing required field
value:
error: "bad_request"
message: "Missing required field: id"
correlation_id: "req-abc-123"
invalid-value:
summary: Invalid value
value:
error: "bad_request"
message: "Secret value too large (max 10KB)"
correlation_id: "req-def-456"
'401':
description: Unauthorized - authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
unauthorized:
summary: Authentication missing
value:
error: "unauthorized"
message: "Authentication required"
correlation_id: "req-ghi-789"
'409':
description: Conflict - secret with this ID already exists
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
duplicate:
summary: Duplicate ID
value:
error: "conflict"
message: "Secret already exists"
correlation_id: "req-jkl-012"
'500':
description: Internal server error - encryption failed
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
encryption-error:
summary: Encryption failure
value:
error: "internal_error"
message: "Failed to encrypt secret"
correlation_id: "req-mno-345"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the ErrorResponse schema definition
rg -n -A 15 'ErrorResponse:' gateway/gateway-controller/api/openapi.yaml | head -50

Repository: wso2/api-platform

Length of output: 542


🏁 Script executed:

#!/bin/bash
# Check all the error response examples mentioned
rg -n -C 3 'error.*:.*bad_request|unauthorized|conflict|internal_error' gateway/gateway-controller/api/openapi.yaml

Repository: wso2/api-platform

Length of output: 2283


🏁 Script executed:

#!/bin/bash
# Search for correlation_id mentions in the file
rg -n 'correlation_id|correlationId' gateway/gateway-controller/api/openapi.yaml | head -20

Repository: wso2/api-platform

Length of output: 610


Fix error response examples to match ErrorResponse schema definition.

The error response examples use error and correlation_id fields, but the ErrorResponse schema (line 3001+) only defines status, message, and errors. Examples across all error responses (lines 1764–1822 and additional instances at 1872–1894, 1930–1966, 1987–2004) must be updated to use status instead of error and remove correlation_id unless the schema is extended to include this field.

🤖 Prompt for AI Agents
In `@gateway/gateway-controller/api/openapi.yaml` around lines 1764 - 1822, Update
the OpenAPI error response examples to match the ErrorResponse schema: replace
the top-level "error" field with "status" and remove any "correlation_id"
entries in each example (e.g., examples named missing-id, invalid-value,
unauthorized, duplicate, encryption-error and all other error examples
referencing ErrorResponse). Keep the "message" field as-is and ensure each
example conforms to the ErrorResponse shape (status, message, errors) used
elsewhere; apply this change consistently for all error response blocks that
reference the ErrorResponse schema.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@gateway/gateway-controller/pkg/api/handlers/handlers.go`:
- Around line 1656-1673: The code calls s.policyResolver.ResolvePolicies(cfg)
without guarding for a nil policyResolver, which can panic; before calling
ResolvePolicies, check if s.policyResolver == nil and if so log a warning via
s.logger (including cfg.ID) and set apiCfg = &cfg.Configuration as the fallback,
otherwise call resolvedCfg, validationErrors :=
s.policyResolver.ResolvePolicies(cfg) and proceed with the existing
validationErrors handling; update references to
s.policyResolver.ResolvePolicies, apiCfg, cfg and s.logger.Error so the nil-path
mirrors the current error fallback behavior.

In `@gateway/gateway-controller/pkg/config/config.go`:
- Around line 526-538: Config.Validate currently doesn't check encryption
settings and the repo ships a hard-coded AES-GCM key; update Config.Validate to
validate EncryptionConfig by ensuring EncryptionConfig.Providers is non-empty,
each ProviderConfig.Providers/ProviderConfig.Keys is non-empty, the provider
Type is one of the supported set (e.g., "aesgcm"), and each
EncryptionKeyConfig.FilePath points to an existing, readable file (return a
validation error if missing or unreadable). Also add a runtime mode check so
non-development deployments must not use the shipped default key path
(./aesgcm-keys/default-aesgcm256-v1.bin) and must explicitly provide key files;
fail validation if the default path is present in non-dev mode. After changes,
rebuild images per guidelines (cd gateway && make build-local).

In `@gateway/gateway-controller/pkg/secrets/service.go`:
- Around line 270-289: The error message uses "id=%s" but the variable is named
handle; update the fmt.Errorf call returned after s.storage.SecretExists(handle)
to use "handle=%s" for consistency (locate the call that returns
fmt.Errorf("secret configuration not found: id=%s", handle) and change the
format string to "secret configuration not found: handle=%s").

In `@gateway/it/features/secrets.feature`:
- Around line 98-151: The scenario "List all secrets returns metadata without
sensitive values" currently lacks negative assertions; update the scenario
(around the "When I list all secrets" / "Then the response status code should be
200" steps) to assert that the response body does NOT contain any secret values
(e.g., "super-secret-value-1", "super-secret-value-2", "super-secret-value-3")
and does not expose a "value" field for the listed items (ensure the step(s)
following "When I list all secrets" verify absence of those strings/fields).
♻️ Duplicate comments (6)
gateway/gateway-controller/api/openapi.yaml (2)

3720-3750: Security consideration: SecretResponse returns decrypted secret values.

The SecretResponse schema returns the plaintext value field. While this appears intentional for the secret retrieval use case, consider whether this is the desired behavior from a security standpoint. Many secret management systems return values only on create or require separate authenticated calls for retrieval.

If returning values is intended, this is fine. Otherwise, consider marking value as writeOnly: true in SecretConfigData and removing it from SecretResponse.


1928-1934: Fix error response example to match ErrorResponse schema.

The error example uses error and correlation_id fields, but the ErrorResponse schema (lines 2932-2949) only defines status, message, and errors. This inconsistency can cause confusion in API documentation and client implementations.

📝 Suggested fix
                 not-found:
                   summary: Secret does not exist
                   value:
-                    error: "not_found"
-                    message: "secret configuration not found"
-                    correlation_id: "req-bcd-890"
+                    status: "error"
+                    message: "secret configuration not found"
gateway/gateway-controller/pkg/config/secret_validator.go (1)

76-81: Use apiVersion in the validation error field.
The schema field is apiVersion, but the error reports version.

🔧 Proposed fix
-			Field:   "version",
+			Field:   "apiVersion",
gateway/gateway-controller/pkg/storage/sqlite.go (1)

1731-1733: Use “handle” terminology in secret errors/messages.
These paths operate on secret handles, not IDs.

🔧 Proposed fix
-			return fmt.Errorf("%w: secret with id '%s' already exists", ErrConflict, secret.Handle)
+			return fmt.Errorf("%w: secret with handle '%s' already exists", ErrConflict, secret.Handle)
@@
-		return nil, fmt.Errorf("secret %w: id=%s", ErrNotFound, handle)
+		return nil, fmt.Errorf("secret %w: handle=%s", ErrNotFound, handle)
@@
-		return fmt.Errorf("secret %w: id=%s", ErrNotFound, secret.Handle)
+		return fmt.Errorf("secret %w: handle=%s", ErrNotFound, secret.Handle)
@@
-		return fmt.Errorf("secret %w: id=%s", ErrNotFound, handle)
+		return fmt.Errorf("secret %w: handle=%s", ErrNotFound, handle)
@@
-// SecretExists checks if a secret with the given ID exists
+// SecretExists checks if a secret with the given handle exists

Also applies to: 1804-1806, 1855-1857, 1886-1888, 1897-1898

gateway/gateway-controller/pkg/api/handlers/handlers.go (1)

2417-2460: UpdateSecret doesn't validate path id matches body metadata.name.

The handler passes the path id to the service, but if the request body contains a different metadata.name, the service will check for conflicts but won't prevent the update. This could lead to confusion where PUT /secrets/foo with body containing metadata.name: bar would update foo (or fail if bar exists). Consider validating consistency similar to UpdateAPI (lines 549-563).

🐛 Proposed fix
 func (s *APIServer) UpdateSecret(c *gin.Context, id string) {
 	log := middleware.GetLogger(c, s.logger)

 	// Read request body
 	body, err := io.ReadAll(c.Request.Body)
 	if err != nil {
 		log.Error("Failed to read request body", zap.Error(err))
 		c.JSON(http.StatusBadRequest, api.ErrorResponse{
 			Status:  "error",
 			Message: "Failed to read request body",
 		})
 		return
 	}

+	// Parse to validate handle matches path parameter
+	var secretConfig api.SecretConfiguration
+	if err := s.parser.Parse(body, c.GetHeader("Content-Type"), &secretConfig); err == nil {
+		if secretConfig.Metadata.Name != "" && secretConfig.Metadata.Name != id {
+			log.Warn("Handle mismatch between path and body",
+				zap.String("path_id", id),
+				zap.String("body_handle", secretConfig.Metadata.Name))
+			c.JSON(http.StatusBadRequest, api.ErrorResponse{
+				Status:  "error",
+				Message: fmt.Sprintf("Handle mismatch: path has '%s' but body metadata.name has '%s'", id, secretConfig.Metadata.Name),
+			})
+			return
+		}
+	}
+
 	// Get correlation ID from context
 	correlationID := middleware.GetCorrelationID(c)
gateway/gateway-controller/pkg/secrets/service.go (1)

35-38: MaxSecretSize constant is defined but never enforced.

The constant suggests a 10KB limit on secret values, but no validation checks the size of secretConfig.Spec.Value before encryption in CreateSecret or UpdateSecret. Large secrets could consume excessive memory during encryption or storage.

🐛 Proposed enforcement in CreateSecret and UpdateSecret

Add this validation after parsing and before encryption in both methods:

 	// Validate configuration
+	// Check secret size limit
+	if len(secretConfig.Spec.Value) > MaxSecretSize {
+		return nil, fmt.Errorf("secret value exceeds maximum size of %d bytes", MaxSecretSize)
+	}
+
 	validationErrors := s.validator.Validate(&secretConfig)
🧹 Nitpick comments (3)
gateway/gateway-controller/pkg/storage/sqlite.go (1)

1754-1763: Simplify the rows.Close() defer (or log close errors).
The current wrapper ignores the close error and adds noise.

♻️ Proposed fix
-	defer func(rows *sql.Rows) {
-		err := rows.Close()
-		if err != nil {
-
-		}
-	}(rows)
+	defer rows.Close()
gateway/it/features/secrets.feature (1)

542-589: Consider cleanup steps to keep the suite isolated.
This scenario creates two secrets but never deletes them.

♻️ Suggested cleanup
     Then the response status code should be 409
     And the response should be valid JSON
     And the JSON response field "status" should be "error"
     And the response body should contain "already exists"
+
+    Given I authenticate using basic auth as "admin"
+    When I delete the secret "update-conflict-secret-1"
+    Then the response status code should be 200
+    Given I authenticate using basic auth as "admin"
+    When I delete the secret "update-conflict-secret-2"
+    Then the response status code should be 200
gateway/gateway-controller/cmd/controller/main.go (1)

1-45: Reminder: Rebuild Docker images after gateway changes.

As per the coding guidelines, when modifying code in gateway components, rebuild Docker images using:

cd gateway && make build-local

Based on coding guidelines for gateway/**/*.{go,yaml,yml,Dockerfile}.

Krishanx92
Krishanx92 previously approved these changes Jan 23, 2026
required:
- action
- name
resolve:
Copy link
Contributor

Choose a reason for hiding this comment

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

Have to update gateway controller repo and release new version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants