Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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.<br />Unlike Value, the Secret stores signed_headers as a single string listing the<br />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