Skip to content

Commit b50d0f6

Browse files
committed
test: add CEL validation unit tests for ApisixConsumerJwtAuthValue
Tests directly exercise the x-kubernetes-validations CEL rule using k8s.io/apiextensions-apiserver's cel.NewValidator, without requiring envtest or a real API server. Covers: - symmetric algorithms (HS256/HS384/HS512) with secret, no private_key - algorithm omitted (defaults to symmetric) - asymmetric algorithms (RS256/ES256/EdDSA) with public_key only - asymmetric algorithms with private_key only (backward compat) - asymmetric algorithms with both keys - asymmetric algorithms with no keys (rejected) - asymmetric algorithms with empty public_key string (rejected)
1 parent 06efa8c commit b50d0f6

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package v2_test
17+
18+
import (
19+
"context"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
25+
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
26+
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
27+
celconfig "k8s.io/apiserver/pkg/apis/cel"
28+
)
29+
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 {
55+
t.Helper()
56+
structural, err := structuralschema.NewStructural(jwtAuthValueSchema)
57+
require.NoError(t, err, "failed to build structural schema")
58+
59+
celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit)
60+
errs, _ := celValidator.Validate(context.Background(), nil, structural, obj, nil, celconfig.RuntimeCELCostBudget)
61+
if len(errs) > 0 {
62+
return errs.ToAggregate()
63+
}
64+
return nil
65+
}
66+
67+
// TestJwtAuthCEL_SymmetricHS256WithSecret verifies that HS256 + secret
68+
// without private_key passes CEL validation.
69+
func TestJwtAuthCEL_SymmetricHS256WithSecret(t *testing.T) {
70+
obj := map[string]interface{}{
71+
"key": "my-key",
72+
"secret": "my-secret",
73+
"algorithm": "HS256",
74+
}
75+
assert.NoError(t, validateJwtAuthValue(t, obj))
76+
}
77+
78+
// TestJwtAuthCEL_SymmetricHS384WithSecret verifies that HS384 + secret passes.
79+
func TestJwtAuthCEL_SymmetricHS384WithSecret(t *testing.T) {
80+
obj := map[string]interface{}{
81+
"key": "my-key",
82+
"secret": "my-secret",
83+
"algorithm": "HS384",
84+
}
85+
assert.NoError(t, validateJwtAuthValue(t, obj))
86+
}
87+
88+
// TestJwtAuthCEL_SymmetricHS512WithSecret verifies that HS512 + secret passes.
89+
func TestJwtAuthCEL_SymmetricHS512WithSecret(t *testing.T) {
90+
obj := map[string]interface{}{
91+
"key": "my-key",
92+
"secret": "my-secret",
93+
"algorithm": "HS512",
94+
}
95+
assert.NoError(t, validateJwtAuthValue(t, obj))
96+
}
97+
98+
// TestJwtAuthCEL_NoAlgorithmDefaultsToSymmetric verifies that omitting
99+
// algorithm (defaults to HS256 server-side) passes CEL validation.
100+
func TestJwtAuthCEL_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
101+
obj := map[string]interface{}{
102+
"key": "my-key",
103+
"secret": "my-secret",
104+
}
105+
assert.NoError(t, validateJwtAuthValue(t, obj))
106+
}
107+
108+
// TestJwtAuthCEL_AsymmetricRS256WithPublicKey verifies that RS256 + public_key passes.
109+
func TestJwtAuthCEL_AsymmetricRS256WithPublicKey(t *testing.T) {
110+
obj := map[string]interface{}{
111+
"key": "my-key",
112+
"public_key": "-----BEGIN PUBLIC KEY-----\nMFww\n-----END PUBLIC KEY-----",
113+
"algorithm": "RS256",
114+
}
115+
assert.NoError(t, validateJwtAuthValue(t, obj))
116+
}
117+
118+
// TestJwtAuthCEL_AsymmetricRS256WithPrivateKey verifies that RS256 + private_key passes
119+
// (backward compatibility: existing configurations only have private_key).
120+
func TestJwtAuthCEL_AsymmetricRS256WithPrivateKey(t *testing.T) {
121+
obj := map[string]interface{}{
122+
"key": "my-key",
123+
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIE\n-----END RSA PRIVATE KEY-----",
124+
"algorithm": "RS256",
125+
}
126+
assert.NoError(t, validateJwtAuthValue(t, obj))
127+
}
128+
129+
// TestJwtAuthCEL_AsymmetricRS256WithBothKeys verifies that RS256 + both keys passes.
130+
func TestJwtAuthCEL_AsymmetricRS256WithBothKeys(t *testing.T) {
131+
obj := map[string]interface{}{
132+
"key": "my-key",
133+
"public_key": "-----BEGIN PUBLIC KEY-----\nMFww\n-----END PUBLIC KEY-----",
134+
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIE\n-----END RSA PRIVATE KEY-----",
135+
"algorithm": "RS256",
136+
}
137+
assert.NoError(t, validateJwtAuthValue(t, obj))
138+
}
139+
140+
// TestJwtAuthCEL_AsymmetricRS256WithoutAnyKey verifies that RS256 without
141+
// any key is rejected by CEL validation.
142+
func TestJwtAuthCEL_AsymmetricRS256WithoutAnyKey(t *testing.T) {
143+
obj := map[string]interface{}{
144+
"key": "my-key",
145+
"algorithm": "RS256",
146+
}
147+
err := validateJwtAuthValue(t, obj)
148+
assert.Error(t, err, "RS256 without public_key or private_key should be rejected")
149+
assert.Contains(t, err.Error(), "asymmetric JWT algorithms")
150+
}
151+
152+
// TestJwtAuthCEL_AsymmetricES256WithoutAnyKey verifies that ES256 without
153+
// any key is rejected.
154+
func TestJwtAuthCEL_AsymmetricES256WithoutAnyKey(t *testing.T) {
155+
obj := map[string]interface{}{
156+
"key": "my-key",
157+
"algorithm": "ES256",
158+
}
159+
err := validateJwtAuthValue(t, obj)
160+
assert.Error(t, err, "ES256 without public_key or private_key should be rejected")
161+
}
162+
163+
// TestJwtAuthCEL_AsymmetricEdDSAWithoutAnyKey verifies that EdDSA without
164+
// any key is rejected.
165+
func TestJwtAuthCEL_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
166+
obj := map[string]interface{}{
167+
"key": "my-key",
168+
"algorithm": "EdDSA",
169+
}
170+
err := validateJwtAuthValue(t, obj)
171+
assert.Error(t, err, "EdDSA without public_key or private_key should be rejected")
172+
}
173+
174+
// TestJwtAuthCEL_AsymmetricWithEmptyPublicKey verifies that an asymmetric
175+
// algorithm with an empty public_key string is rejected (same as absent).
176+
func TestJwtAuthCEL_AsymmetricWithEmptyPublicKey(t *testing.T) {
177+
obj := map[string]interface{}{
178+
"key": "my-key",
179+
"public_key": "",
180+
"algorithm": "RS256",
181+
}
182+
err := validateJwtAuthValue(t, obj)
183+
assert.Error(t, err, "RS256 with empty public_key should be rejected")
184+
}

0 commit comments

Comments
 (0)