Skip to content

Commit 6073371

Browse files
committed
fix: use single quotes in CEL rule to avoid Unicode quote corruption
controller-gen misinterprets empty string literals written with double quotes inside a double-quoted marker value, producing Unicode right double quotation marks in the generated CRD. Switch to single-quoted empty strings ('') which are valid CEL and survive the YAML generation pipeline intact. Also add CEL unit tests that load the schema directly from the generated CRD YAML file, ensuring tests always validate against the real schema.
1 parent b50d0f6 commit 6073371

3 files changed

Lines changed: 79 additions & 41 deletions

File tree

api/v2/apisixconsumer_jwtauth_cel_test.go

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,72 @@ package v2_test
1717

1818
import (
1919
"context"
20+
"encoding/json"
21+
"os"
22+
"path/filepath"
23+
"runtime"
2024
"testing"
2125

2226
"github.com/stretchr/testify/assert"
2327
"github.com/stretchr/testify/require"
2428
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
29+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2530
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
2631
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
2732
celconfig "k8s.io/apiserver/pkg/apis/cel"
33+
sigsyaml "sigs.k8s.io/yaml"
2834
)
2935

30-
// jwtAuthValueSchema mirrors the CEL rule on ApisixConsumerJwtAuthValue.
31-
// It must be kept in sync with the +kubebuilder:validation:XValidation marker
32-
// on that type.
33-
var jwtAuthValueSchema = &apiextensions.JSONSchemaProps{
34-
Type: "object",
35-
Properties: map[string]apiextensions.JSONSchemaProps{
36-
"key": {Type: "string"},
37-
"secret": {Type: "string"},
38-
"public_key": {Type: "string"},
39-
"private_key": {Type: "string"},
40-
"algorithm": {Type: "string"},
41-
"exp": {Type: "integer", Format: "int64"},
42-
"base64_secret": {Type: "boolean"},
43-
"lifetime_grace_period": {Type: "integer", Format: "int64"},
44-
},
45-
Required: []string{"key"},
46-
XValidations: []apiextensions.ValidationRule{
47-
{
48-
Rule: "!has(self.algorithm) || self.algorithm in ['HS256','HS384','HS512'] || (has(self.public_key) && self.public_key != '') || (has(self.private_key) && self.private_key != '')",
49-
Message: "asymmetric JWT algorithms (RS*/ES*/PS*/EdDSA) require at least one of public_key or private_key",
50-
},
51-
},
52-
}
53-
54-
func validateJwtAuthValue(t *testing.T, obj map[string]interface{}) error {
36+
// loadJwtAuthValueSchema reads the ApisixConsumer CRD YAML and extracts the
37+
// structural schema for spec.authParameter.jwtAuth.value, so that CEL tests
38+
// always validate against the real generated schema rather than a hand-written copy.
39+
func loadJwtAuthValueSchema(t *testing.T) *structuralschema.Structural {
5540
t.Helper()
56-
structural, err := structuralschema.NewStructural(jwtAuthValueSchema)
41+
42+
_, thisFile, _, _ := runtime.Caller(0)
43+
crdPath := filepath.Join(filepath.Dir(thisFile), "..", "..",
44+
"config", "crd", "bases", "apisix.apache.org_apisixconsumers.yaml")
45+
46+
data, err := os.ReadFile(crdPath)
47+
require.NoError(t, err, "failed to read CRD file: %s", crdPath)
48+
49+
var crd apiextensionsv1.CustomResourceDefinition
50+
jsonData, err := sigsyaml.YAMLToJSON(data)
51+
require.NoError(t, err, "failed to convert CRD YAML to JSON")
52+
err = json.Unmarshal(jsonData, &crd)
53+
require.NoError(t, err, "failed to unmarshal CRD")
54+
55+
// Find the v2 version schema.
56+
var v1Schema *apiextensionsv1.JSONSchemaProps
57+
for _, v := range crd.Spec.Versions {
58+
if v.Name == "v2" {
59+
v1Schema = v.Schema.OpenAPIV3Schema
60+
break
61+
}
62+
}
63+
require.NotNil(t, v1Schema, "v2 schema not found in CRD")
64+
65+
// Navigate: spec.authParameter.jwtAuth.value
66+
jwtAuthValueV1 := v1Schema.
67+
Properties["spec"].
68+
Properties["authParameter"].
69+
Properties["jwtAuth"].
70+
Properties["value"]
71+
72+
// Convert v1 JSONSchemaProps to internal type required by NewStructural.
73+
var internal apiextensions.JSONSchemaProps
74+
err = apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(
75+
&jwtAuthValueV1, &internal, nil,
76+
)
77+
require.NoError(t, err, "failed to convert v1 schema to internal")
78+
79+
structural, err := structuralschema.NewStructural(&internal)
5780
require.NoError(t, err, "failed to build structural schema")
81+
return structural
82+
}
5883

84+
func validateJwtAuthValue(t *testing.T, structural *structuralschema.Structural, obj map[string]interface{}) error {
85+
t.Helper()
5986
celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit)
6087
errs, _ := celValidator.Validate(context.Background(), nil, structural, obj, nil, celconfig.RuntimeCELCostBudget)
6188
if len(errs) > 0 {
@@ -67,118 +94,129 @@ func validateJwtAuthValue(t *testing.T, obj map[string]interface{}) error {
6794
// TestJwtAuthCEL_SymmetricHS256WithSecret verifies that HS256 + secret
6895
// without private_key passes CEL validation.
6996
func TestJwtAuthCEL_SymmetricHS256WithSecret(t *testing.T) {
97+
schema := loadJwtAuthValueSchema(t)
7098
obj := map[string]interface{}{
7199
"key": "my-key",
72100
"secret": "my-secret",
73101
"algorithm": "HS256",
74102
}
75-
assert.NoError(t, validateJwtAuthValue(t, obj))
103+
assert.NoError(t, validateJwtAuthValue(t, schema, obj))
76104
}
77105

78106
// TestJwtAuthCEL_SymmetricHS384WithSecret verifies that HS384 + secret passes.
79107
func TestJwtAuthCEL_SymmetricHS384WithSecret(t *testing.T) {
108+
schema := loadJwtAuthValueSchema(t)
80109
obj := map[string]interface{}{
81110
"key": "my-key",
82111
"secret": "my-secret",
83112
"algorithm": "HS384",
84113
}
85-
assert.NoError(t, validateJwtAuthValue(t, obj))
114+
assert.NoError(t, validateJwtAuthValue(t, schema, obj))
86115
}
87116

88117
// TestJwtAuthCEL_SymmetricHS512WithSecret verifies that HS512 + secret passes.
89118
func TestJwtAuthCEL_SymmetricHS512WithSecret(t *testing.T) {
119+
schema := loadJwtAuthValueSchema(t)
90120
obj := map[string]interface{}{
91121
"key": "my-key",
92122
"secret": "my-secret",
93123
"algorithm": "HS512",
94124
}
95-
assert.NoError(t, validateJwtAuthValue(t, obj))
125+
assert.NoError(t, validateJwtAuthValue(t, schema, obj))
96126
}
97127

98128
// TestJwtAuthCEL_NoAlgorithmDefaultsToSymmetric verifies that omitting
99129
// algorithm (defaults to HS256 server-side) passes CEL validation.
100130
func TestJwtAuthCEL_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
131+
schema := loadJwtAuthValueSchema(t)
101132
obj := map[string]interface{}{
102133
"key": "my-key",
103134
"secret": "my-secret",
104135
}
105-
assert.NoError(t, validateJwtAuthValue(t, obj))
136+
assert.NoError(t, validateJwtAuthValue(t, schema, obj))
106137
}
107138

108139
// TestJwtAuthCEL_AsymmetricRS256WithPublicKey verifies that RS256 + public_key passes.
109140
func TestJwtAuthCEL_AsymmetricRS256WithPublicKey(t *testing.T) {
141+
schema := loadJwtAuthValueSchema(t)
110142
obj := map[string]interface{}{
111143
"key": "my-key",
112144
"public_key": "-----BEGIN PUBLIC KEY-----\nMFww\n-----END PUBLIC KEY-----",
113145
"algorithm": "RS256",
114146
}
115-
assert.NoError(t, validateJwtAuthValue(t, obj))
147+
assert.NoError(t, validateJwtAuthValue(t, schema, obj))
116148
}
117149

118150
// TestJwtAuthCEL_AsymmetricRS256WithPrivateKey verifies that RS256 + private_key passes
119-
// (backward compatibility: existing configurations only have private_key).
151+
// (backward compatibility: existing configurations may only have private_key).
120152
func TestJwtAuthCEL_AsymmetricRS256WithPrivateKey(t *testing.T) {
153+
schema := loadJwtAuthValueSchema(t)
121154
obj := map[string]interface{}{
122155
"key": "my-key",
123156
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIE\n-----END RSA PRIVATE KEY-----",
124157
"algorithm": "RS256",
125158
}
126-
assert.NoError(t, validateJwtAuthValue(t, obj))
159+
assert.NoError(t, validateJwtAuthValue(t, schema, obj))
127160
}
128161

129162
// TestJwtAuthCEL_AsymmetricRS256WithBothKeys verifies that RS256 + both keys passes.
130163
func TestJwtAuthCEL_AsymmetricRS256WithBothKeys(t *testing.T) {
164+
schema := loadJwtAuthValueSchema(t)
131165
obj := map[string]interface{}{
132166
"key": "my-key",
133167
"public_key": "-----BEGIN PUBLIC KEY-----\nMFww\n-----END PUBLIC KEY-----",
134168
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIE\n-----END RSA PRIVATE KEY-----",
135169
"algorithm": "RS256",
136170
}
137-
assert.NoError(t, validateJwtAuthValue(t, obj))
171+
assert.NoError(t, validateJwtAuthValue(t, schema, obj))
138172
}
139173

140174
// TestJwtAuthCEL_AsymmetricRS256WithoutAnyKey verifies that RS256 without
141175
// any key is rejected by CEL validation.
142176
func TestJwtAuthCEL_AsymmetricRS256WithoutAnyKey(t *testing.T) {
177+
schema := loadJwtAuthValueSchema(t)
143178
obj := map[string]interface{}{
144179
"key": "my-key",
145180
"algorithm": "RS256",
146181
}
147-
err := validateJwtAuthValue(t, obj)
182+
err := validateJwtAuthValue(t, schema, obj)
148183
assert.Error(t, err, "RS256 without public_key or private_key should be rejected")
149184
assert.Contains(t, err.Error(), "asymmetric JWT algorithms")
150185
}
151186

152187
// TestJwtAuthCEL_AsymmetricES256WithoutAnyKey verifies that ES256 without
153188
// any key is rejected.
154189
func TestJwtAuthCEL_AsymmetricES256WithoutAnyKey(t *testing.T) {
190+
schema := loadJwtAuthValueSchema(t)
155191
obj := map[string]interface{}{
156192
"key": "my-key",
157193
"algorithm": "ES256",
158194
}
159-
err := validateJwtAuthValue(t, obj)
195+
err := validateJwtAuthValue(t, schema, obj)
160196
assert.Error(t, err, "ES256 without public_key or private_key should be rejected")
161197
}
162198

163199
// TestJwtAuthCEL_AsymmetricEdDSAWithoutAnyKey verifies that EdDSA without
164200
// any key is rejected.
165201
func TestJwtAuthCEL_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
202+
schema := loadJwtAuthValueSchema(t)
166203
obj := map[string]interface{}{
167204
"key": "my-key",
168205
"algorithm": "EdDSA",
169206
}
170-
err := validateJwtAuthValue(t, obj)
207+
err := validateJwtAuthValue(t, schema, obj)
171208
assert.Error(t, err, "EdDSA without public_key or private_key should be rejected")
172209
}
173210

174211
// TestJwtAuthCEL_AsymmetricWithEmptyPublicKey verifies that an asymmetric
175212
// algorithm with an empty public_key string is rejected (same as absent).
176213
func TestJwtAuthCEL_AsymmetricWithEmptyPublicKey(t *testing.T) {
214+
schema := loadJwtAuthValueSchema(t)
177215
obj := map[string]interface{}{
178216
"key": "my-key",
179217
"public_key": "",
180218
"algorithm": "RS256",
181219
}
182-
err := validateJwtAuthValue(t, obj)
220+
err := validateJwtAuthValue(t, schema, obj)
183221
assert.Error(t, err, "RS256 with empty public_key should be rejected")
184222
}

api/v2/apisixconsumer_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ type ApisixConsumerJwtAuth struct {
134134
// - For symmetric algorithms (HS256, HS384, HS512): use secret. private_key and public_key are not required.
135135
// - For asymmetric algorithms (RS*, ES*, PS*, EdDSA): at least one of public_key or private_key must be provided.
136136
//
137-
// +kubebuilder:validation:XValidation:rule="!has(self.algorithm) || self.algorithm in ['HS256','HS384','HS512'] || (has(self.public_key) && self.public_key != ) || (has(self.private_key) && self.private_key != )",message="asymmetric JWT algorithms (RS*/ES*/PS*/EdDSA) require at least one of public_key or private_key"
137+
// +kubebuilder:validation:XValidation:rule="!has(self.algorithm) || self.algorithm in ['HS256','HS384','HS512'] || (has(self.public_key) && self.public_key != '') || (has(self.private_key) && self.private_key != '')",message="asymmetric JWT algorithms (RS*/ES*/PS*/EdDSA) require at least one of public_key or private_key"
138138
type ApisixConsumerJwtAuthValue struct {
139139
// Key is the unique identifier for the JWT credential.
140140
Key string `json:"key" yaml:"key"`

config/crd/bases/apisix.apache.org_apisixconsumers.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ spec:
215215
- message: asymmetric JWT algorithms (RS*/ES*/PS*/EdDSA) require
216216
at least one of public_key or private_key
217217
rule: '!has(self.algorithm) || self.algorithm in [''HS256'',''HS384'',''HS512'']
218-
|| (has(self.public_key) && self.public_key != ) || (has(self.private_key)
219-
&& self.private_key != )'
218+
|| (has(self.public_key) && self.public_key != '''') ||
219+
(has(self.private_key) && self.private_key != '''')'
220220
type: object
221221
keyAuth:
222222
description: KeyAuth configures the key authentication details.

0 commit comments

Comments
 (0)