Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/v2/apisixconsumer_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,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"`
Expand Down
6 changes: 4 additions & 2 deletions config/crd/bases/apisix.apache.org_apisixconsumers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down
2 changes: 1 addition & 1 deletion docs/en/latest/reference/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |


Expand Down
28 changes: 22 additions & 6 deletions internal/adc/translator/apisixconsumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ package translator
import (
"fmt"
"strconv"
"strings"
"unicode"

"github.com/pkg/errors"
k8stypes "k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -307,16 +309,23 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon
}

clockSkewRaw := sec.Data["clock_skew"]
clockSkew, _ := strconv.ParseInt(string(clockSkewRaw), 10, 64)
var clockSkew int64
if len(clockSkewRaw) > 0 {
var err error
clockSkew, err = strconv.ParseInt(string(clockSkewRaw), 10, 64)
if err != nil {
return nil, fmt.Errorf("hmac-auth: invalid clock_skew %q in secret %s/%s: %w", string(clockSkewRaw), consumerNamespace, cfg.SecretRef.Name, err)
}
Comment thread
jarvis9443 marked this conversation as resolved.
}
if clockSkew < 0 {
clockSkew = _hmacAuthClockSkewDefaultValue
}

// header names are RFC 7230 tokens, so a comma or any space is always a separator
signedHeadersRaw := sec.Data["signed_headers"]
signedHeaders := make([]string, 0, len(signedHeadersRaw))
for _, b := range signedHeadersRaw {
signedHeaders = append(signedHeaders, string(b))
}
signedHeaders := strings.FieldsFunc(string(signedHeadersRaw), func(r rune) bool {
return r == ',' || unicode.IsSpace(r)
})

var keepHeader bool
keepHeaderRaw, ok := sec.Data["keep_headers"]
Expand Down Expand Up @@ -355,7 +364,14 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon
}

maxReqBodyRaw := sec.Data["max_req_body"]
maxReqBody, _ := strconv.ParseInt(string(maxReqBodyRaw), 10, 64)
var maxReqBody int64
if len(maxReqBodyRaw) > 0 {
var err error
maxReqBody, err = strconv.ParseInt(string(maxReqBodyRaw), 10, 64)
if err != nil {
return nil, fmt.Errorf("hmac-auth: invalid max_req_body %q in secret %s/%s: %w", string(maxReqBodyRaw), consumerNamespace, cfg.SecretRef.Name, err)
}
}
if maxReqBody < 0 {
maxReqBody = _hmacAuthMaxReqBodyDefaultValue
}
Expand Down
75 changes: 75 additions & 0 deletions internal/adc/translator/apisixconsumer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,88 @@ import (

"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"

adctypes "github.com/apache/apisix-ingress-controller/api/adc"
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/controller/label"
"github.com/apache/apisix-ingress-controller/internal/provider"
)

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: 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) {
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)

cfg := result.Consumers[0].Plugins["hmac-auth"].(*adctypes.HMACAuthConsumerConfig)
require.Equal(t, tc.expected, cfg.SignedHeaders)
})
}
}

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(), tc.key)
require.Contains(t, err.Error(), "default/hmac")
})
}
}

func TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerLabels(t *testing.T) {
translator := NewTranslator(logr.Discard())
tctx := provider.NewDefaultTranslateContext(context.Background())
Expand Down
Loading