Skip to content
Merged
7 changes: 6 additions & 1 deletion api/v2/apisixconsumer_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ type ApisixConsumerJwtAuth struct {
}

// ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
// Exactly one of the following must be provided depending on the algorithm:
// - For symmetric algorithms (HS256, HS384, HS512): use secret. private_key and public_key are not required.
// - For asymmetric algorithms (RS*, ES*, PS*, EdDSA): at least one of public_key or private_key must be provided.
Comment thread
AlinsRan marked this conversation as resolved.
Outdated
Comment thread
AlinsRan marked this conversation as resolved.
Outdated
//
Comment thread
AlinsRan marked this conversation as resolved.
Outdated
// +kubebuilder:validation:XValidation:rule="!has(self.algorithm) || self.algorithm in ['HS256','HS384','HS512'] || (has(self.public_key) && size(self.public_key) > 0) || (has(self.private_key) && size(self.private_key) > 0)",message="asymmetric JWT algorithms (RS*/ES*/PS*/EdDSA) require at least one of public_key or private_key"
Comment thread
AlinsRan marked this conversation as resolved.
Outdated
type ApisixConsumerJwtAuthValue struct {
// Key is the unique identifier for the JWT credential.
Key string `json:"key" yaml:"key"`
Expand All @@ -138,7 +143,7 @@ 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"`
Comment thread
AlinsRan marked this conversation as resolved.
// Algorithm specifies the signing algorithm.
// Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`.
// Currently APISIX only supports `HS256`, `HS512`, `RS256`, and `ES256`. API7 Enterprise supports all algorithms.
Expand Down
310 changes: 310 additions & 0 deletions api/v2/apisixconsumer_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
// 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))
}

func TestApisixConsumer_JwtAuth_SymmetricHS384(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: "HS384",
},
},
},
},
}
assert.NoError(t, v.Validate(t, ac))
}

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: "-----BEGIN PUBLIC KEY-----\nMFww\n-----END PUBLIC KEY-----",
Algorithm: "RS256",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
},
},
},
},
}
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: "-----BEGIN RSA PRIVATE KEY-----\nMIIE\n-----END RSA 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: "-----BEGIN PUBLIC KEY-----\nMFww\n-----END PUBLIC KEY-----",
PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIE\n-----END RSA 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(), "asymmetric JWT algorithms")
}

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(), "asymmetric JWT algorithms")
}

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(), "asymmetric JWT algorithms")
}

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
},
},
},
},
}
Comment thread
AlinsRan marked this conversation as resolved.
err := v.Validate(t, ac)
require.Error(t, err)
assert.Contains(t, err.Error(), "asymmetric JWT algorithms")
}
8 changes: 7 additions & 1 deletion config/crd/bases/apisix.apache.org_apisixconsumers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,14 @@ spec:
type: string
required:
- key
- private_key
type: object
x-kubernetes-validations:
- message: asymmetric JWT algorithms (RS*/ES*/PS*/EdDSA) require
at least one of public_key or private_key
rule: '!has(self.algorithm) || self.algorithm in [''HS256'',''HS384'',''HS512'']
|| (has(self.public_key) && size(self.public_key) > 0)
|| (has(self.private_key) && size(self.private_key) >
0)'
Comment thread
AlinsRan marked this conversation as resolved.
Outdated
type: object
keyAuth:
description: KeyAuth configures the key authentication details.
Expand Down
3 changes: 3 additions & 0 deletions docs/en/latest/reference/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,9 @@ _Appears in:_


ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
Exactly one of the following must be provided depending on the algorithm:
- For symmetric algorithms (HS256, HS384, HS512): use secret. private_key and public_key are not required.
- For asymmetric algorithms (RS*, ES*, PS*, EdDSA): at least one of public_key or private_key must be provided.



Expand Down
Loading