This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new e89f0f24 fix: relax jwtAuth private_key requirement and add CEL 
validation (#2759)
e89f0f24 is described below

commit e89f0f24ec4f6086b4539feb12a786eca6e079f3
Author: AlinsRan <[email protected]>
AuthorDate: Sat May 9 12:45:32 2026 +0800

    fix: relax jwtAuth private_key requirement and add CEL validation (#2759)
---
 api/adc/plugin_types.go                            |   2 +-
 api/v2/apisixconsumer_types.go                     |   9 +-
 api/v2/apisixconsumer_validation_test.go           | 337 +++++++++++++++++++++
 .../bases/apisix.apache.org_apisixconsumers.yaml   |  11 +-
 docs/en/latest/reference/api-reference.md          |   5 +-
 5 files changed, 358 insertions(+), 6 deletions(-)

diff --git a/api/adc/plugin_types.go b/api/adc/plugin_types.go
index 6d230886..ee68a746 100644
--- a/api/adc/plugin_types.go
+++ b/api/adc/plugin_types.go
@@ -68,7 +68,7 @@ type JwtAuthConsumerConfig struct {
        Key                 string `json:"key" yaml:"key"`
        Secret              string `json:"secret,omitempty" 
yaml:"secret,omitempty"`
        PublicKey           string `json:"public_key,omitempty" 
yaml:"public_key,omitempty"`
-       PrivateKey          string `json:"private_key" 
yaml:"private_key,omitempty"`
+       PrivateKey          string `json:"private_key,omitempty" 
yaml:"private_key,omitempty"`
        Algorithm           string `json:"algorithm,omitempty" 
yaml:"algorithm,omitempty"`
        Exp                 int64  `json:"exp,omitempty" yaml:"exp,omitempty"`
        Base64Secret        bool   `json:"base64_secret,omitempty" 
yaml:"base64_secret,omitempty"`
diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go
index c9e4afba..7a05f9ca 100644
--- a/api/v2/apisixconsumer_types.go
+++ b/api/v2/apisixconsumer_types.go
@@ -130,6 +130,11 @@ type ApisixConsumerJwtAuth struct {
 }
 
 // ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
+// For asymmetric algorithms (RS*, ES*, PS*, EdDSA), at least one of public_key
+// or private_key must be provided. Symmetric algorithms (HS256, HS384, HS512)
+// and unset algorithm do not require any key field.
+//
+// +kubebuilder:validation:XValidation:rule="!has(self.algorithm) || 
size(self.algorithm) == 0 || self.algorithm in ['HS256','HS384','HS512'] || 
(has(self.public_key) && size(self.public_key.trim()) > 0) || 
(has(self.private_key) && size(self.private_key.trim()) > 
0)",message="algorithms other than HS256/HS384/HS512 require at least one 
non-empty public_key or private_key"
 type ApisixConsumerJwtAuthValue struct {
        // Key is the unique identifier for the JWT credential.
        Key string `json:"key" yaml:"key"`
@@ -138,9 +143,9 @@ type ApisixConsumerJwtAuthValue struct {
        // PublicKey is the public key used to verify JWT signatures (for 
asymmetric algorithms).
        PublicKey string `json:"public_key,omitempty" 
yaml:"public_key,omitempty"`
        // PrivateKey is the private key used to sign the JWT (for asymmetric 
algorithms).
-       PrivateKey string `json:"private_key" yaml:"private_key,omitempty"`
+       PrivateKey string `json:"private_key,omitempty" 
yaml:"private_key,omitempty"`
        // Algorithm specifies the signing algorithm.
-       // Can be `HS256`, `HS512`, `RS256`, or `ES256`.
+       // Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, 
`ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`.
        Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"`
        // Exp is the token expiration period in seconds.
        Exp int64 `json:"exp,omitempty" yaml:"exp,omitempty"`
diff --git a/api/v2/apisixconsumer_validation_test.go 
b/api/v2/apisixconsumer_validation_test.go
new file mode 100644
index 00000000..88fdd1d6
--- /dev/null
+++ b/api/v2/apisixconsumer_validation_test.go
@@ -0,0 +1,337 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v2_test
+
+import (
+       "context"
+       "encoding/json"
+       "os"
+       "path/filepath"
+       "runtime"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
+       apiextensionsv1 
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+       structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
+       "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
+       "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
+       celconfig "k8s.io/apiserver/pkg/apis/cel"
+       sigsyaml "sigs.k8s.io/yaml"
+
+       apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+)
+
+// consumerSchemaValidator holds the parsed CRD schema for ApisixConsumer
+// and provides a Validate method for use in tests.
+type consumerSchemaValidator struct {
+       structural *structuralschema.Structural
+       internal   *apiextensions.JSONSchemaProps
+}
+
+func (v *consumerSchemaValidator) Validate(t *testing.T, ac 
*apisixv2.ApisixConsumer) error {
+       t.Helper()
+
+       data, err := json.Marshal(ac)
+       require.NoError(t, err, "failed to marshal ApisixConsumer")
+
+       var obj map[string]interface{}
+       require.NoError(t, json.Unmarshal(data, &obj), "failed to unmarshal to 
map")
+
+       schemaValidator, _, err := validation.NewSchemaValidator(v.internal)
+       require.NoError(t, err, "failed to build schema validator")
+
+       if errs := validation.ValidateCustomResource(nil, obj, 
schemaValidator); len(errs) > 0 {
+               return errs.ToAggregate()
+       }
+
+       celValidator := cel.NewValidator(v.structural, false, 
celconfig.PerCallLimit)
+       celErrs, _ := celValidator.Validate(context.Background(), nil, 
v.structural, obj, nil, celconfig.RuntimeCELCostBudget)
+       if len(celErrs) > 0 {
+               return celErrs.ToAggregate()
+       }
+       return nil
+}
+
+// loadApisixConsumerSchema reads the ApisixConsumer CRD YAML and returns a
+// validator backed by the real generated schema.
+func loadApisixConsumerSchema(t *testing.T) *consumerSchemaValidator {
+       t.Helper()
+
+       _, thisFile, _, _ := runtime.Caller(0)
+       crdPath := filepath.Join(filepath.Dir(thisFile), "..", "..",
+               "config", "crd", "bases", 
"apisix.apache.org_apisixconsumers.yaml")
+
+       data, err := os.ReadFile(crdPath)
+       require.NoError(t, err, "failed to read CRD file: %s", crdPath)
+
+       jsonData, err := sigsyaml.YAMLToJSON(data)
+       require.NoError(t, err, "failed to convert CRD YAML to JSON")
+
+       var crd apiextensionsv1.CustomResourceDefinition
+       require.NoError(t, json.Unmarshal(jsonData, &crd), "failed to unmarshal 
CRD")
+
+       var v1Schema *apiextensionsv1.JSONSchemaProps
+       for _, v := range crd.Spec.Versions {
+               if v.Name == "v2" {
+                       v1Schema = v.Schema.OpenAPIV3Schema
+                       break
+               }
+       }
+       require.NotNil(t, v1Schema, "v2 schema not found in CRD")
+
+       var internal apiextensions.JSONSchemaProps
+       require.NoError(t,
+               
apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v1Schema,
 &internal, nil),
+               "failed to convert v1 schema to internal",
+       )
+
+       structural, err := structuralschema.NewStructural(&internal)
+       require.NoError(t, err, "failed to build structural schema")
+       return &consumerSchemaValidator{structural: structural, internal: 
&internal}
+}
+
+func TestApisixConsumer_JwtAuth_SymmetricHS256(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               Secret:    "my-secret",
+                                               Algorithm: "HS256",
+                                       },
+                               },
+                       },
+               },
+       }
+       assert.NoError(t, v.Validate(t, ac))
+}
+
+// TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey verifies
+// that a whitespace-only public_key is treated as absent and rejected for
+// asymmetric algorithms.
+func TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey(t 
*testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               Algorithm: "RS256",
+                                               PublicKey: "   ",
+                                       },
+                               },
+                       },
+               },
+       }
+       err := v.Validate(t, ac)
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "algorithms other than 
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_SymmetricHS512(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               Secret:    "my-secret",
+                                               Algorithm: "HS512",
+                                       },
+                               },
+                       },
+               },
+       }
+       assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:    "my-key",
+                                               Secret: "my-secret",
+                                       },
+                               },
+                       },
+               },
+       }
+       assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPublicKey(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               PublicKey: "test-public-key",
+                                               Algorithm: "RS256",
+                                       },
+                               },
+                       },
+               },
+       }
+       assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPrivateKey(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:        "my-key",
+                                               PrivateKey: "test-private-key",
+                                               Algorithm:  "RS256",
+                                       },
+                               },
+                       },
+               },
+       }
+       assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithBothKeys(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:        "my-key",
+                                               PublicKey:  "test-public-key",
+                                               PrivateKey: "test-private-key",
+                                               Algorithm:  "RS256",
+                                       },
+                               },
+                       },
+               },
+       }
+       assert.NoError(t, v.Validate(t, ac))
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               Algorithm: "RS256",
+                                       },
+                               },
+                       },
+               },
+       }
+       err := v.Validate(t, ac)
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "algorithms other than 
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricES256WithoutAnyKey(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               Algorithm: "ES256",
+                                       },
+                               },
+                       },
+               },
+       }
+       err := v.Validate(t, ac)
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "algorithms other than 
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               Algorithm: "EdDSA",
+                                       },
+                               },
+                       },
+               },
+       }
+       err := v.Validate(t, ac)
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "algorithms other than 
HS256/HS384/HS512")
+}
+
+func TestApisixConsumer_JwtAuth_AsymmetricWithEmptyPublicKey(t *testing.T) {
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:       "my-key",
+                                               Algorithm: "RS256",
+                                               // PublicKey is empty string — 
omitempty means it won't appear
+                                               // in the serialized JSON, same 
effect as not set
+                                       },
+                               },
+                       },
+               },
+       }
+       err := v.Validate(t, ac)
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "algorithms other than 
HS256/HS384/HS512")
+}
+
+// TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric verifies that an
+// explicitly empty algorithm string is treated the same as an unset algorithm
+// (defaults to HS256) and does not require public_key or private_key.
+func TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric(t *testing.T) 
{
+       v := loadApisixConsumerSchema(t)
+       ac := &apisixv2.ApisixConsumer{
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
+                                               Key:    "my-key",
+                                               Secret: "my-secret",
+                                               // Algorithm is explicitly 
empty string — should be treated as
+                                               // unset and not require 
asymmetric keys.
+                                       },
+                               },
+                       },
+               },
+       }
+       assert.NoError(t, v.Validate(t, ac))
+}
diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml 
b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
index 9235ef27..db0ec861 100644
--- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
@@ -176,7 +176,7 @@ spec:
                           algorithm:
                             description: |-
                               Algorithm specifies the signing algorithm.
-                              Can be `HS256`, `HS512`, `RS256`, or `ES256`.
+                              Can be `HS256`, `HS384`, `HS512`, `RS256`, 
`RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or 
`EdDSA`.
                             type: string
                           base64_secret:
                             description: Base64Secret indicates whether the 
secret
@@ -209,8 +209,15 @@ spec:
                             type: string
                         required:
                         - key
-                        - private_key
                         type: object
+                        x-kubernetes-validations:
+                        - message: algorithms other than HS256/HS384/HS512 
require
+                            at least one non-empty public_key or private_key
+                          rule: '!has(self.algorithm) || size(self.algorithm) 
== 0
+                            || self.algorithm in 
[''HS256'',''HS384'',''HS512''] ||
+                            (has(self.public_key) && 
size(self.public_key.trim())
+                            > 0) || (has(self.private_key) && 
size(self.private_key.trim())
+                            > 0)'
                     type: object
                   keyAuth:
                     description: KeyAuth configures the key authentication 
details.
diff --git a/docs/en/latest/reference/api-reference.md 
b/docs/en/latest/reference/api-reference.md
index f385468d..4ee8a633 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -781,6 +781,9 @@ _Appears in:_
 
 
 ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
+For asymmetric algorithms (RS*, ES*, PS*, EdDSA), at least one of public_key
+or private_key must be provided. Symmetric algorithms (HS256, HS384, HS512)
+and unset algorithm do not require any key field.
 
 
 
@@ -790,7 +793,7 @@ ApisixConsumerJwtAuthValue defines configuration for JWT 
authentication.
 | `secret` _string_ | Secret is the shared secret used to sign the JWT (for 
symmetric algorithms). |
 | `public_key` _string_ | PublicKey is the public key used to verify JWT 
signatures (for asymmetric algorithms). |
 | `private_key` _string_ | PrivateKey is the private key used to sign the JWT 
(for asymmetric algorithms). |
-| `algorithm` _string_ | Algorithm specifies the signing algorithm. Can be 
`HS256`, `HS512`, `RS256`, or `ES256`. |
+| `algorithm` _string_ | Algorithm specifies the signing algorithm. Can be 
`HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, 
`ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`. |
 | `exp` _integer_ | Exp is the token expiration period in seconds. |
 | `base64_secret` _boolean_ | Base64Secret indicates whether the secret is 
base64-encoded. |
 | `lifetime_grace_period` _integer_ | LifetimeGracePeriod is the allowed clock 
skew in seconds for token expiration. |

Reply via email to