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

shreemaan-abhishek 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 0207b159 fix: accept whitespace-separated hmac-auth signed_headers and 
document the format (#2824)
0207b159 is described below

commit 0207b159383f83ac0b386680b604efe7ec7c1e96
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Mon Jul 27 12:15:19 2026 +0800

    fix: accept whitespace-separated hmac-auth signed_headers and document the 
format (#2824)
---
 api/v2/apisixconsumer_types.go                     |  2 +
 .../bases/apisix.apache.org_apisixconsumers.yaml   |  6 +-
 docs/en/latest/reference/api-reference.md          |  2 +-
 internal/adc/translator/apisixconsumer.go          | 12 ++--
 internal/adc/translator/apisixconsumer_test.go     | 82 ++++++++++++++--------
 5 files changed, 65 insertions(+), 39 deletions(-)

diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go
index 03d1cfce..8fa7c6f3 100644
--- a/api/v2/apisixconsumer_types.go
+++ b/api/v2/apisixconsumer_types.go
@@ -164,6 +164,8 @@ type ApisixConsumerJwtAuthValue struct {
 // ApisixConsumerHMACAuth defines configuration for the HMAC authentication.
 type ApisixConsumerHMACAuth struct {
        // SecretRef references a Kubernetes Secret containing the HMAC 
credentials.
+       // Unlike Value, the Secret stores signed_headers as a single string 
listing the
+       // header names separated by commas or whitespace, for example "X-Date, 
Host".
        SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty" 
yaml:"secretRef,omitempty"`
        // Value specifies HMAC authentication credentials.
        Value *ApisixConsumerHMACAuthValue `json:"value,omitempty" 
yaml:"value,omitempty"`
diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml 
b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
index 4b004137..3dd21c64 100644
--- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
@@ -82,8 +82,10 @@ spec:
                     description: HMACAuth configures the HMAC authentication 
details.
                     properties:
                       secretRef:
-                        description: SecretRef references a Kubernetes Secret 
containing
-                          the HMAC credentials.
+                        description: |-
+                          SecretRef references a Kubernetes Secret containing 
the HMAC credentials.
+                          Unlike Value, the Secret stores signed_headers as a 
single string listing the
+                          header names separated by commas or whitespace, for 
example "X-Date, Host".
                         properties:
                           name:
                             default: ""
diff --git a/docs/en/latest/reference/api-reference.md 
b/docs/en/latest/reference/api-reference.md
index 8a02cfdf..5ccedfde 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -896,7 +896,7 @@ ApisixConsumerHMACAuth defines configuration for the HMAC 
authentication.
 
 | Field | Description |
 | --- | --- |
-| `secretRef` 
_[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_
 | SecretRef references a Kubernetes Secret containing the HMAC credentials. |
+| `secretRef` 
_[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_
 | SecretRef references a Kubernetes Secret containing the HMAC credentials. 
Unlike Value, the Secret stores signed_headers as a single string listing the 
header names separated by commas or whitespace, for example "X-Date, Host". |
 | `value` _[ApisixConsumerHMACAuthValue](#apisixconsumerhmacauthvalue)_ | 
Value specifies HMAC authentication credentials. |
 
 
diff --git a/internal/adc/translator/apisixconsumer.go 
b/internal/adc/translator/apisixconsumer.go
index aad8b359..51a98c73 100644
--- a/internal/adc/translator/apisixconsumer.go
+++ b/internal/adc/translator/apisixconsumer.go
@@ -21,6 +21,7 @@ import (
        "fmt"
        "strconv"
        "strings"
+       "unicode"
 
        "github.com/pkg/errors"
        k8stypes "k8s.io/apimachinery/pkg/types"
@@ -320,14 +321,11 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx 
*provider.TranslateCon
                clockSkew = _hmacAuthClockSkewDefaultValue
        }
 
-       // comma-separated header names, not raw bytes
+       // header names are RFC 7230 tokens, so a comma or any space is always 
a separator
        signedHeadersRaw := sec.Data["signed_headers"]
-       var signedHeaders []string
-       for _, h := range strings.Split(string(signedHeadersRaw), ",") {
-               if h = strings.TrimSpace(h); h != "" {
-                       signedHeaders = append(signedHeaders, h)
-               }
-       }
+       signedHeaders := strings.FieldsFunc(string(signedHeadersRaw), func(r 
rune) bool {
+               return r == ',' || unicode.IsSpace(r)
+       })
 
        var keepHeader bool
        keepHeaderRaw, ok := sec.Data["keep_headers"]
diff --git a/internal/adc/translator/apisixconsumer_test.go 
b/internal/adc/translator/apisixconsumer_test.go
index e31892c6..d331c245 100644
--- a/internal/adc/translator/apisixconsumer_test.go
+++ b/internal/adc/translator/apisixconsumer_test.go
@@ -33,52 +33,76 @@ import (
        "github.com/apache/apisix-ingress-controller/internal/provider"
 )
 
-func hmacConsumerWithSecret(name string) *apiv2.ApisixConsumer {
+func hmacConsumerWithSecret(secretName string) *apiv2.ApisixConsumer {
        return &apiv2.ApisixConsumer{
                ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
                Spec: apiv2.ApisixConsumerSpec{
                        AuthParameter: &apiv2.ApisixConsumerAuthParameter{
                                HMACAuth: &apiv2.ApisixConsumerHMACAuth{
-                                       SecretRef: 
&corev1.LocalObjectReference{Name: name},
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: secretName},
                                },
                        },
                },
        }
 }
 
+func hmacSecret(data map[string][]byte) *corev1.Secret {
+       data["key_id"] = []byte("my-key")
+       data["secret_key"] = []byte("my-secret")
+       return &corev1.Secret{Data: data}
+}
+
 func TestTranslateApisixConsumer_HMACAuthSignedHeadersFromSecret(t *testing.T) 
{
-       translator := NewTranslator(logr.Discard(), "")
-       tctx := provider.NewDefaultTranslateContext(context.Background())
-       tctx.Secrets[k8stypes.NamespacedName{Namespace: "default", Name: 
"hmac"}] = &corev1.Secret{
-               Data: map[string][]byte{
-                       "key_id":         []byte("my-key"),
-                       "secret_key":     []byte("my-secret"),
-                       "signed_headers": []byte("X-Date, Host"),
-               },
-       }
+       for _, tc := range []struct {
+               name     string
+               raw      string
+               expected []string
+       }{
+               {name: "comma separated", raw: "X-Date,Host", expected: 
[]string{"X-Date", "Host"}},
+               {name: "padding and empty entries", raw: " X-Date, , Host, ", 
expected: []string{"X-Date", "Host"}},
+               {name: "newline separated", raw: "X-Date\nHost", expected: 
[]string{"X-Date", "Host"}},
+               {name: "space separated", raw: "X-Date Host", expected: 
[]string{"X-Date", "Host"}},
+               {name: "single header", raw: "X-Date", expected: 
[]string{"X-Date"}},
+               {name: "empty", raw: "", expected: []string{}},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       translator := NewTranslator(logr.Discard(), "")
+                       tctx := 
provider.NewDefaultTranslateContext(context.Background())
+                       tctx.Secrets[k8stypes.NamespacedName{Namespace: 
"default", Name: "hmac"}] = hmacSecret(map[string][]byte{
+                               "signed_headers": []byte(tc.raw),
+                       })
 
-       result, err := translator.TranslateApisixConsumer(tctx, 
hmacConsumerWithSecret("hmac"))
-       require.NoError(t, err)
-       require.Len(t, result.Consumers, 1)
+                       result, err := translator.TranslateApisixConsumer(tctx, 
hmacConsumerWithSecret("hmac"))
+                       require.NoError(t, err)
+                       require.Len(t, result.Consumers, 1)
 
-       cfg := 
result.Consumers[0].Plugins["hmac-auth"].(*adctypes.HMACAuthConsumerConfig)
-       require.Equal(t, []string{"X-Date", "Host"}, cfg.SignedHeaders)
+                       cfg := 
result.Consumers[0].Plugins["hmac-auth"].(*adctypes.HMACAuthConsumerConfig)
+                       require.Equal(t, tc.expected, cfg.SignedHeaders)
+               })
+       }
 }
 
-func TestTranslateApisixConsumer_HMACAuthRejectsInvalidClockSkew(t *testing.T) 
{
-       translator := NewTranslator(logr.Discard(), "")
-       tctx := provider.NewDefaultTranslateContext(context.Background())
-       tctx.Secrets[k8stypes.NamespacedName{Namespace: "default", Name: 
"hmac"}] = &corev1.Secret{
-               Data: map[string][]byte{
-                       "key_id":     []byte("my-key"),
-                       "secret_key": []byte("my-secret"),
-                       "clock_skew": []byte("3O0"), // typo: letter O
-               },
-       }
+func TestTranslateApisixConsumer_HMACAuthRejectsUnparseableNumbers(t 
*testing.T) {
+       for _, tc := range []struct {
+               key string
+               raw string
+       }{
+               {key: "clock_skew", raw: "3O0"}, // typo: letter O
+               {key: "max_req_body", raw: "invalid"},
+       } {
+               t.Run(tc.key, func(t *testing.T) {
+                       translator := NewTranslator(logr.Discard(), "")
+                       tctx := 
provider.NewDefaultTranslateContext(context.Background())
+                       tctx.Secrets[k8stypes.NamespacedName{Namespace: 
"default", Name: "hmac"}] = hmacSecret(map[string][]byte{
+                               tc.key: []byte(tc.raw),
+                       })
 
-       _, err := translator.TranslateApisixConsumer(tctx, 
hmacConsumerWithSecret("hmac"))
-       require.Error(t, err)
-       require.Contains(t, err.Error(), "clock_skew")
+                       _, err := translator.TranslateApisixConsumer(tctx, 
hmacConsumerWithSecret("hmac"))
+                       require.Error(t, err)
+                       require.Contains(t, err.Error(), tc.key)
+                       require.Contains(t, err.Error(), "default/hmac")
+               })
+       }
 }
 
 func 
TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerLabels(t
 *testing.T) {

Reply via email to