Skip to content

Commit dd377dd

Browse files
committed
chore: merge master and fix consumer test after algorithm CEL rule added in #406
2 parents 271976c + 0283a04 commit dd377dd

26 files changed

Lines changed: 1750 additions & 106 deletions

api/adc/plugin_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ type JwtAuthConsumerConfig struct {
6868
Key string `json:"key" yaml:"key"`
6969
Secret string `json:"secret,omitempty" yaml:"secret,omitempty"`
7070
PublicKey string `json:"public_key,omitempty" yaml:"public_key,omitempty"`
71-
PrivateKey string `json:"private_key" yaml:"private_key,omitempty"`
71+
PrivateKey string `json:"private_key,omitempty" yaml:"private_key,omitempty"`
7272
Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"`
7373
Exp int64 `json:"exp,omitempty" yaml:"exp,omitempty"`
7474
Base64Secret bool `json:"base64_secret,omitempty" yaml:"base64_secret,omitempty"`

api/v2/apisixconsumer_types.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ type ApisixConsumerJwtAuth struct {
130130
}
131131

132132
// ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
133+
// For asymmetric algorithms (RS*, ES*, PS*, EdDSA), at least one of public_key
134+
// or private_key must be provided. Symmetric algorithms (HS256, HS384, HS512)
135+
// and unset algorithm do not require any key field.
136+
//
137+
// +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"
133138
type ApisixConsumerJwtAuthValue struct {
134139
// Key is the unique identifier for the JWT credential.
135140
Key string `json:"key" yaml:"key"`
@@ -138,10 +143,9 @@ type ApisixConsumerJwtAuthValue struct {
138143
// PublicKey is the public key used to verify JWT signatures (for asymmetric algorithms).
139144
PublicKey string `json:"public_key,omitempty" yaml:"public_key,omitempty"`
140145
// PrivateKey is the private key used to sign the JWT (for asymmetric algorithms).
141-
PrivateKey string `json:"private_key" yaml:"private_key,omitempty"`
146+
PrivateKey string `json:"private_key,omitempty" yaml:"private_key,omitempty"`
142147
// Algorithm specifies the signing algorithm.
143148
// Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`.
144-
// Currently APISIX only supports `HS256`, `HS512`, `RS256`, and `ES256`. API7 Enterprise supports all algorithms.
145149
Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"`
146150
// Exp is the token expiration period in seconds.
147151
Exp int64 `json:"exp,omitempty" yaml:"exp,omitempty"`

api/v2/apisixconsumer_validation_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222

2323
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
2425

2526
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
2627
)
@@ -142,7 +143,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithBothKeys(t *testing.T) {
142143
}
143144

144145
// TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey verifies that RS256
145-
// without any key is allowed in API7 Enterprise (supports all algorithms without key constraints).
146+
// without any key is rejected by the CRD validation rule.
146147
func TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
147148
v := loadApisixConsumerSchema(t)
148149
ac := &apisixv2.ApisixConsumer{
@@ -157,8 +158,9 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
157158
},
158159
},
159160
}
160-
// API7 Enterprise supports all algorithms; no key constraint enforced by CRD.
161-
assert.NoError(t, v.validateObject(t, ac))
161+
err := v.validateObject(t, ac)
162+
require.Error(t, err)
163+
assert.Contains(t, err.Error(), "algorithms other than HS256/HS384/HS512")
162164
}
163165

164166
// TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric verifies that an

config/crd-nocel/apisix.apache.org_v2.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ spec:
180180
description: |-
181181
Algorithm specifies the signing algorithm.
182182
Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`.
183-
Currently APISIX only supports `HS256`, `HS512`, `RS256`, and `ES256`. API7 Enterprise supports all algorithms.
184183
type: string
185184
base64_secret:
186185
description: Base64Secret indicates whether the secret

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ spec:
177177
description: |-
178178
Algorithm specifies the signing algorithm.
179179
Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`.
180-
Currently APISIX only supports `HS256`, `HS512`, `RS256`, and `ES256`. API7 Enterprise supports all algorithms.
181180
type: string
182181
base64_secret:
183182
description: Base64Secret indicates whether the secret
@@ -210,8 +209,15 @@ spec:
210209
type: string
211210
required:
212211
- key
213-
- private_key
214212
type: object
213+
x-kubernetes-validations:
214+
- message: algorithms other than HS256/HS384/HS512 require
215+
at least one non-empty public_key or private_key
216+
rule: '!has(self.algorithm) || size(self.algorithm) == 0
217+
|| self.algorithm in [''HS256'',''HS384'',''HS512''] ||
218+
(has(self.public_key) && size(self.public_key.trim())
219+
> 0) || (has(self.private_key) && size(self.private_key.trim())
220+
> 0)'
215221
type: object
216222
keyAuth:
217223
description: KeyAuth configures the key authentication details.

docs/en/latest/reference/api-reference.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,9 @@ _Appears in:_
781781

782782

783783
ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
784+
For asymmetric algorithms (RS*, ES*, PS*, EdDSA), at least one of public_key
785+
or private_key must be provided. Symmetric algorithms (HS256, HS384, HS512)
786+
and unset algorithm do not require any key field.
784787

785788

786789

@@ -790,7 +793,7 @@ ApisixConsumerJwtAuthValue defines configuration for JWT authentication.
790793
| `secret` _string_ | Secret is the shared secret used to sign the JWT (for symmetric algorithms). |
791794
| `public_key` _string_ | PublicKey is the public key used to verify JWT signatures (for asymmetric algorithms). |
792795
| `private_key` _string_ | PrivateKey is the private key used to sign the JWT (for asymmetric algorithms). |
793-
| `algorithm` _string_ | 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. |
796+
| `algorithm` _string_ | Algorithm specifies the signing algorithm. Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`. |
794797
| `exp` _integer_ | Exp is the token expiration period in seconds. |
795798
| `base64_secret` _boolean_ | Base64Secret indicates whether the secret is base64-encoded. |
796799
| `lifetime_grace_period` _integer_ | LifetimeGracePeriod is the allowed clock skew in seconds for token expiration. |

internal/adc/client/client.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,43 @@ func (c *Client) DeleteConfig(ctx context.Context, args Task) error {
174174
return err
175175
}
176176

177+
func (c *Client) Validate(ctx context.Context, task Task) error {
178+
if len(task.Configs) == 0 || task.Resources == nil {
179+
return nil
180+
}
181+
182+
fileIOStart := time.Now()
183+
syncFilePath, cleanup, err := prepareSyncFile(task.Resources)
184+
if err != nil {
185+
pkgmetrics.RecordFileIODuration("prepare_sync_file", "failure", time.Since(fileIOStart).Seconds())
186+
return err
187+
}
188+
pkgmetrics.RecordFileIODuration("prepare_sync_file", adctypes.StatusSuccess, time.Since(fileIOStart).Seconds())
189+
defer cleanup()
190+
191+
args := BuildADCExecuteArgs(syncFilePath, task.Labels, task.ResourceTypes)
192+
193+
var errs types.ADCValidationErrors
194+
for _, config := range task.Configs {
195+
if config.BackendType == "" {
196+
config.BackendType = c.defaultMode
197+
}
198+
if err := c.executor.Validate(ctx, config, args); err != nil {
199+
var validationErr types.ADCValidationError
200+
if errors.As(err, &validationErr) {
201+
errs.Errors = append(errs.Errors, validationErr)
202+
continue
203+
}
204+
return err
205+
}
206+
}
207+
208+
if len(errs.Errors) > 0 {
209+
return errs
210+
}
211+
return nil
212+
}
213+
177214
func (c *Client) Sync(ctx context.Context) (map[string]types.ADCExecutionErrors, error) {
178215
c.syncMu.Lock()
179216
defer c.syncMu.Unlock()

internal/adc/client/executor.go

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343

4444
type ADCExecutor interface {
4545
Execute(ctx context.Context, config adctypes.Config, args []string) error
46+
Validate(ctx context.Context, config adctypes.Config, args []string) error
4647
}
4748

4849
func BuildADCExecuteArgs(filePath string, labels map[string]string, types []string) []string {
@@ -81,6 +82,12 @@ type ADCServerOpts struct {
8182
CacheKey string `json:"cacheKey"`
8283
}
8384

85+
type ADCValidateResult struct {
86+
Success *bool `json:"success,omitempty"`
87+
ErrorMessage string `json:"message,omitempty"`
88+
Errors []types.ADCValidationDetail `json:"errors,omitempty"`
89+
}
90+
8491
// HTTPADCExecutor implements ADCExecutor interface using HTTP calls to ADC Server
8592
type HTTPADCExecutor struct {
8693
httpClient *http.Client
@@ -123,6 +130,10 @@ func (e *HTTPADCExecutor) Execute(ctx context.Context, config adctypes.Config, a
123130
return e.runHTTPSync(ctx, config, args)
124131
}
125132

133+
func (e *HTTPADCExecutor) Validate(ctx context.Context, config adctypes.Config, args []string) error {
134+
return e.runHTTPValidate(ctx, config, args)
135+
}
136+
126137
// runHTTPSync performs HTTP sync to ADC Server for each server address
127138
func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config adctypes.Config, args []string) error {
128139
var execErrs = types.ADCExecutionError{
@@ -157,6 +168,38 @@ func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config adctypes.Confi
157168
return nil
158169
}
159170

171+
func (e *HTTPADCExecutor) runHTTPValidate(ctx context.Context, config adctypes.Config, args []string) error {
172+
var validationErr = types.ADCValidationError{
173+
Name: config.Name,
174+
}
175+
var infraErrs []error
176+
177+
serverAddrs := func() []string {
178+
return config.ServerAddrs
179+
}()
180+
e.log.V(1).Info("running http validate", "serverAddrs", serverAddrs)
181+
182+
for _, addr := range serverAddrs {
183+
if err := e.runHTTPValidateForSingleServer(ctx, addr, config, args); err != nil {
184+
e.log.Error(err, "failed to run http validate for server", "server", addr)
185+
var validationServerErr types.ADCValidationServerAddrError
186+
if errors.As(err, &validationServerErr) {
187+
validationErr.FailedErrors = append(validationErr.FailedErrors, validationServerErr)
188+
continue
189+
}
190+
infraErrs = append(infraErrs, err)
191+
}
192+
}
193+
194+
if len(validationErr.FailedErrors) > 0 {
195+
return validationErr
196+
}
197+
if len(infraErrs) > 0 {
198+
return errors.Join(infraErrs...)
199+
}
200+
return nil
201+
}
202+
160203
// runHTTPSyncForSingleServer performs HTTP sync to a single ADC Server
161204
func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, serverAddr string, config adctypes.Config, args []string) error {
162205
ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
@@ -175,7 +218,7 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, server
175218
}
176219

177220
// Build HTTP request
178-
req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources)
221+
req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources, http.MethodPut, "/sync")
179222
if err != nil {
180223
return fmt.Errorf("failed to build HTTP request: %w", err)
181224
}
@@ -195,6 +238,38 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, server
195238
return e.handleHTTPResponse(resp, serverAddr)
196239
}
197240

241+
func (e *HTTPADCExecutor) runHTTPValidateForSingleServer(ctx context.Context, serverAddr string, config adctypes.Config, args []string) error {
242+
ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
243+
defer cancel()
244+
245+
labels, types, filePath, err := e.parseArgs(args)
246+
if err != nil {
247+
return fmt.Errorf("failed to parse args: %w", err)
248+
}
249+
250+
resources, err := e.loadResourcesFromFile(filePath)
251+
if err != nil {
252+
return fmt.Errorf("failed to load resources from file %s: %w", filePath, err)
253+
}
254+
255+
req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources, http.MethodPut, "/validate")
256+
if err != nil {
257+
return fmt.Errorf("failed to build validate request: %w", err)
258+
}
259+
260+
resp, err := e.httpClient.Do(req)
261+
if err != nil {
262+
return fmt.Errorf("failed to send HTTP request: %w", err)
263+
}
264+
defer func() {
265+
if closeErr := resp.Body.Close(); closeErr != nil {
266+
e.log.Error(closeErr, "failed to close response body")
267+
}
268+
}()
269+
270+
return e.handleHTTPValidateResponse(resp, serverAddr)
271+
}
272+
198273
// parseArgs parses the command line arguments to extract labels, types, and file path
199274
func (e *HTTPADCExecutor) parseArgs(args []string) (map[string]string, []string, string, error) {
200275
labels := make(map[string]string)
@@ -248,7 +323,7 @@ func (e *HTTPADCExecutor) loadResourcesFromFile(filePath string) (*adctypes.Reso
248323
}
249324

250325
// buildHTTPRequest builds the HTTP request for ADC Server
251-
func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr string, config adctypes.Config, labels map[string]string, types []string, resources *adctypes.Resources) (*http.Request, error) {
326+
func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr string, config adctypes.Config, labels map[string]string, types []string, resources *adctypes.Resources, method string, path string) (*http.Request, error) {
252327
// Prepare request body
253328
tlsVerify := config.TlsVerify
254329
reqBody := ADCServerRequest{
@@ -274,7 +349,7 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin
274349
}
275350

276351
e.log.V(1).Info("sending HTTP request to ADC Server",
277-
"url", e.serverURL+"/sync",
352+
"url", e.serverURL+path,
278353
"server", serverAddr,
279354
"mode", config.BackendType,
280355
"cacheKey", config.Name,
@@ -284,7 +359,7 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin
284359
)
285360

286361
// Create HTTP request
287-
req, err := http.NewRequestWithContext(ctx, "PUT", e.serverURL+"/sync", bytes.NewBuffer(jsonData))
362+
req, err := http.NewRequestWithContext(ctx, method, e.serverURL+path, bytes.NewBuffer(jsonData))
288363
if err != nil {
289364
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
290365
}
@@ -357,3 +432,63 @@ func (e *HTTPADCExecutor) handleHTTPResponse(resp *http.Response, serverAddr str
357432
e.log.V(1).Info("ADC Server sync success", "result", result)
358433
return nil
359434
}
435+
436+
func (e *HTTPADCExecutor) handleHTTPValidateResponse(resp *http.Response, serverAddr string) error {
437+
body, err := io.ReadAll(resp.Body)
438+
if err != nil {
439+
return fmt.Errorf("failed to read response body: %w", err)
440+
}
441+
442+
e.log.V(1).Info("received HTTP validate response from ADC Server",
443+
"server", serverAddr,
444+
"status", resp.StatusCode,
445+
"response", string(body),
446+
)
447+
448+
parseValidationResult := func() *ADCValidateResult {
449+
if len(body) == 0 {
450+
return nil
451+
}
452+
var result ADCValidateResult
453+
if err := json.Unmarshal(body, &result); err != nil {
454+
return nil
455+
}
456+
return &result
457+
}
458+
459+
if resp.StatusCode == http.StatusBadRequest {
460+
result := parseValidationResult()
461+
errMsg := string(body)
462+
if result != nil && result.ErrorMessage != "" {
463+
errMsg = result.ErrorMessage
464+
}
465+
return types.ADCValidationServerAddrError{
466+
ServerAddr: serverAddr,
467+
Err: errMsg,
468+
ValidationErrors: func() []types.ADCValidationDetail {
469+
if result == nil {
470+
return nil
471+
}
472+
return result.Errors
473+
}(),
474+
}
475+
}
476+
477+
if resp.StatusCode/100 != 2 {
478+
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
479+
}
480+
481+
if result := parseValidationResult(); result != nil && result.Success != nil && !*result.Success {
482+
errMsg := result.ErrorMessage
483+
if errMsg == "" {
484+
errMsg = "ADC validation failed"
485+
}
486+
return types.ADCValidationServerAddrError{
487+
ServerAddr: serverAddr,
488+
Err: errMsg,
489+
ValidationErrors: result.Errors,
490+
}
491+
}
492+
493+
return nil
494+
}

0 commit comments

Comments
 (0)