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

AlinsRan 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 336f4270 feat: support plugins field in ApisixConsumer (#2761)
336f4270 is described below

commit 336f427059cae1f9eaf62895da204a89a22562f3
Author: AlinsRan <[email protected]>
AuthorDate: Wed May 13 14:16:23 2026 +0800

    feat: support plugins field in ApisixConsumer (#2761)
---
 api/v2/apisixconsumer_types.go                     |   8 +-
 api/v2/apisixconsumer_validation_test.go           |  24 +--
 api/v2/zz_generated.deepcopy.go                    |  13 +-
 .../bases/apisix.apache.org_apisixconsumers.yaml   |  28 +++-
 docs/en/latest/reference/api-reference.md          |   2 +
 internal/adc/translator/apisixconsumer.go          | 100 +++++++----
 internal/controller/apisixconsumer_controller.go   |  77 ++++++---
 internal/controller/indexer/indexer.go             |  35 ++--
 internal/webhook/v1/apisixconsumer_webhook.go      |  37 ++--
 internal/webhook/v1/apisixconsumer_webhook_test.go |  10 +-
 test/e2e/crds/v2/consumer.go                       | 186 +++++++++++++++++++++
 11 files changed, 404 insertions(+), 116 deletions(-)

diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go
index 7a05f9ca..03d1cfce 100644
--- a/api/v2/apisixconsumer_types.go
+++ b/api/v2/apisixconsumer_types.go
@@ -29,7 +29,13 @@ type ApisixConsumerSpec struct {
        IngressClassName string `json:"ingressClassName,omitempty" 
yaml:"ingressClassName,omitempty"`
 
        // AuthParameter defines the authentication credentials and 
configuration for this consumer.
-       AuthParameter ApisixConsumerAuthParameter `json:"authParameter" 
yaml:"authParameter"`
+       // +kubebuilder:validation:Optional
+       AuthParameter *ApisixConsumerAuthParameter 
`json:"authParameter,omitempty" yaml:"authParameter,omitempty"`
+
+       // Plugins lists additional consumer-scoped plugins to attach to this 
consumer.
+       // These plugins are applied alongside any authentication plugin 
derived from AuthParameter.
+       // An enabled plugin with the same name as the auth plugin derived from 
AuthParameter takes precedence.
+       Plugins []ApisixRoutePlugin `json:"plugins,omitempty" 
yaml:"plugins,omitempty"`
 }
 
 // ApisixConsumerStatus defines the observed state of ApisixConsumer.
diff --git a/api/v2/apisixconsumer_validation_test.go 
b/api/v2/apisixconsumer_validation_test.go
index 88fdd1d6..5c421315 100644
--- a/api/v2/apisixconsumer_validation_test.go
+++ b/api/v2/apisixconsumer_validation_test.go
@@ -109,7 +109,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS256(t 
*testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -130,7 +130,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey(t *testing
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -150,7 +150,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS512(t 
*testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -168,7 +168,7 @@ func 
TestApisixConsumer_JwtAuth_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:    "my-key",
@@ -185,7 +185,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricRS256WithPublicKey(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -203,7 +203,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricRS256WithPrivateKey(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:        "my-key",
@@ -221,7 +221,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricRS256WithBothKeys(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:        "my-key",
@@ -240,7 +240,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -259,7 +259,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricES256WithoutAnyKey(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -278,7 +278,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -297,7 +297,7 @@ func 
TestApisixConsumer_JwtAuth_AsymmetricWithEmptyPublicKey(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:       "my-key",
@@ -321,7 +321,7 @@ func 
TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric(t *testing.T) {
        v := loadApisixConsumerSchema(t)
        ac := &apisixv2.ApisixConsumer{
                Spec: apisixv2.ApisixConsumerSpec{
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
                                        Value: 
&apisixv2.ApisixConsumerJwtAuthValue{
                                                Key:    "my-key",
diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go
index 73be4b23..8e659cd9 100644
--- a/api/v2/zz_generated.deepcopy.go
+++ b/api/v2/zz_generated.deepcopy.go
@@ -406,7 +406,18 @@ func (in *ApisixConsumerList) DeepCopyObject() 
runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
 func (in *ApisixConsumerSpec) DeepCopyInto(out *ApisixConsumerSpec) {
        *out = *in
-       in.AuthParameter.DeepCopyInto(&out.AuthParameter)
+       if in.AuthParameter != nil {
+               in, out := &in.AuthParameter, &out.AuthParameter
+               *out = new(ApisixConsumerAuthParameter)
+               (*in).DeepCopyInto(*out)
+       }
+       if in.Plugins != nil {
+               in, out := &in.Plugins, &out.Plugins
+               *out = make([]ApisixRoutePlugin, len(*in))
+               for i := range *in {
+                       (*in)[i].DeepCopyInto(&(*out)[i])
+               }
+       }
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new ApisixConsumerSpec.
diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml 
b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
index db0ec861..4b004137 100644
--- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
@@ -319,8 +319,32 @@ spec:
                   IngressClassName is the name of an IngressClass cluster 
resource.
                   The controller uses this field to decide whether the 
resource should be managed.
                 type: string
-            required:
-            - authParameter
+              plugins:
+                description: |-
+                  Plugins lists additional consumer-scoped plugins to attach 
to this consumer.
+                  These plugins are applied alongside any authentication 
plugin derived from AuthParameter.
+                  An enabled plugin with the same name as the auth plugin 
derived from AuthParameter takes precedence.
+                items:
+                  description: ApisixRoutePlugin represents an APISIX plugin.
+                  properties:
+                    config:
+                      description: Plugin configuration.
+                      x-kubernetes-preserve-unknown-fields: true
+                    enable:
+                      default: true
+                      description: Whether this plugin is in use, default is 
true.
+                      type: boolean
+                    name:
+                      description: The plugin name.
+                      type: string
+                    secretRef:
+                      description: Plugin configuration secretRef.
+                      type: string
+                  required:
+                  - enable
+                  - name
+                  type: object
+                type: array
             type: object
           status:
             description: ApisixStatus is the status report for Apisix ingress 
Resources
diff --git a/docs/en/latest/reference/api-reference.md 
b/docs/en/latest/reference/api-reference.md
index 4ee8a633..ce700a73 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -875,6 +875,7 @@ ApisixConsumerSpec defines the desired state of 
ApisixConsumer.
 | --- | --- |
 | `ingressClassName` _string_ | IngressClassName is the name of an 
IngressClass cluster resource. The controller uses this field to decide whether 
the resource should be managed. |
 | `authParameter` 
_[ApisixConsumerAuthParameter](#apisixconsumerauthparameter)_ | AuthParameter 
defines the authentication credentials and configuration for this consumer. |
+| `plugins` _[ApisixRoutePlugin](#apisixrouteplugin) array_ | Plugins lists 
additional consumer-scoped plugins to attach to this consumer. These plugins 
are applied alongside any authentication plugin derived from AuthParameter. An 
enabled plugin with the same name as the auth plugin derived from AuthParameter 
takes precedence. |
 
 
 _Appears in:_
@@ -1163,6 +1164,7 @@ ApisixRoutePlugin represents an APISIX plugin.
 
 
 _Appears in:_
+- [ApisixConsumerSpec](#apisixconsumerspec)
 - [ApisixGlobalRuleSpec](#apisixglobalrulespec)
 - [ApisixPluginConfigSpec](#apisixpluginconfigspec)
 - [ApisixRouteHTTP](#apisixroutehttp)
diff --git a/internal/adc/translator/apisixconsumer.go 
b/internal/adc/translator/apisixconsumer.go
index 823d65bb..3cae6e08 100644
--- a/internal/adc/translator/apisixconsumer.go
+++ b/internal/adc/translator/apisixconsumer.go
@@ -55,42 +55,54 @@ const (
 func (t *Translator) TranslateApisixConsumer(tctx *provider.TranslateContext, 
ac *v2.ApisixConsumer) (*TranslateResult, error) {
        result := &TranslateResult{}
        plugins := make(adctypes.Plugins)
-       if ac.Spec.AuthParameter.KeyAuth != nil {
-               cfg, err := t.translateConsumerKeyAuthPlugin(tctx, 
ac.Namespace, ac.Spec.AuthParameter.KeyAuth)
-               if err != nil {
-                       return nil, fmt.Errorf("invalid key auth config: %s", 
err)
+       if ap := ac.Spec.AuthParameter; ap != nil {
+               if ap.KeyAuth != nil {
+                       cfg, err := t.translateConsumerKeyAuthPlugin(tctx, 
ac.Namespace, ap.KeyAuth)
+                       if err != nil {
+                               return nil, fmt.Errorf("invalid key auth 
config: %s", err)
+                       }
+                       plugins["key-auth"] = cfg
+               } else if ap.BasicAuth != nil {
+                       cfg, err := t.translateConsumerBasicAuthPlugin(tctx, 
ac.Namespace, ap.BasicAuth)
+                       if err != nil {
+                               return nil, fmt.Errorf("invalid basic auth 
config: %s", err)
+                       }
+                       plugins["basic-auth"] = cfg
+               } else if ap.JwtAuth != nil {
+                       cfg, err := t.translateConsumerJwtAuthPlugin(tctx, 
ac.Namespace, ap.JwtAuth)
+                       if err != nil {
+                               return nil, fmt.Errorf("invalid jwt auth 
config: %s", err)
+                       }
+                       plugins["jwt-auth"] = cfg
+               } else if ap.WolfRBAC != nil {
+                       cfg, err := t.translateConsumerWolfRBACPlugin(tctx, 
ac.Namespace, ap.WolfRBAC)
+                       if err != nil {
+                               return nil, fmt.Errorf("invalid wolf rbac 
config: %s", err)
+                       }
+                       plugins["wolf-rbac"] = cfg
+               } else if ap.HMACAuth != nil {
+                       cfg, err := t.translateConsumerHMACAuthPlugin(tctx, 
ac.Namespace, ap.HMACAuth)
+                       if err != nil {
+                               return nil, fmt.Errorf("invalid hmac auth 
config: %s", err)
+                       }
+                       plugins["hmac-auth"] = cfg
+               } else if ap.LDAPAuth != nil {
+                       cfg, err := t.translateConsumerLDAPAuthPlugin(tctx, 
ac.Namespace, ap.LDAPAuth)
+                       if err != nil {
+                               return nil, fmt.Errorf("invalid ldap auth 
config: %s", err)
+                       }
+                       plugins["ldap-auth"] = cfg
                }
-               plugins["key-auth"] = cfg
-       } else if ac.Spec.AuthParameter.BasicAuth != nil {
-               cfg, err := t.translateConsumerBasicAuthPlugin(tctx, 
ac.Namespace, ac.Spec.AuthParameter.BasicAuth)
-               if err != nil {
-                       return nil, fmt.Errorf("invalid basic auth config: %s", 
err)
-               }
-               plugins["basic-auth"] = cfg
-       } else if ac.Spec.AuthParameter.JwtAuth != nil {
-               cfg, err := t.translateConsumerJwtAuthPlugin(tctx, 
ac.Namespace, ac.Spec.AuthParameter.JwtAuth)
-               if err != nil {
-                       return nil, fmt.Errorf("invalid jwt auth config: %s", 
err)
-               }
-               plugins["jwt-auth"] = cfg
-       } else if ac.Spec.AuthParameter.WolfRBAC != nil {
-               cfg, err := t.translateConsumerWolfRBACPlugin(tctx, 
ac.Namespace, ac.Spec.AuthParameter.WolfRBAC)
-               if err != nil {
-                       return nil, fmt.Errorf("invalid wolf rbac config: %s", 
err)
-               }
-               plugins["wolf-rbac"] = cfg
-       } else if ac.Spec.AuthParameter.HMACAuth != nil {
-               cfg, err := t.translateConsumerHMACAuthPlugin(tctx, 
ac.Namespace, ac.Spec.AuthParameter.HMACAuth)
-               if err != nil {
-                       return nil, fmt.Errorf("invalid hmac auth config: %s", 
err)
-               }
-               plugins["hmac-auth"] = cfg
-       } else if ac.Spec.AuthParameter.LDAPAuth != nil {
-               cfg, err := t.translateConsumerLDAPAuthPlugin(tctx, 
ac.Namespace, ac.Spec.AuthParameter.LDAPAuth)
-               if err != nil {
-                       return nil, fmt.Errorf("invalid ldap auth config: %s", 
err)
+       }
+
+       // Merge generic consumer-scoped plugins. Only enabled entries are 
merged;
+       // an enabled plugin with the same name as an auth plugin derived from 
authParameter takes precedence.
+       for _, plugin := range ac.Spec.Plugins {
+               if !plugin.Enable {
+                       continue
                }
-               plugins["ldap-auth"] = cfg
+               config := t.buildPluginConfig(plugin, ac.Namespace, 
tctx.Secrets)
+               plugins[plugin.Name] = config
        }
 
        username := adctypes.ComposeConsumerName(ac.Namespace, ac.Name)
@@ -107,7 +119,9 @@ func (t *Translator) translateConsumerKeyAuthPlugin(tctx 
*provider.TranslateCont
        if cfg.Value != nil {
                return &adctypes.KeyAuthConsumerConfig{Key: cfg.Value.Key}, nil
        }
-
+       if cfg.SecretRef == nil {
+               return nil, fmt.Errorf("key-auth: either value or secretRef 
must be specified")
+       }
        sec := tctx.Secrets[k8stypes.NamespacedName{
                Namespace: consumerNamespace,
                Name:      cfg.SecretRef.Name,
@@ -129,7 +143,9 @@ func (t *Translator) translateConsumerBasicAuthPlugin(tctx 
*provider.TranslateCo
                        Password: cfg.Value.Password,
                }, nil
        }
-
+       if cfg.SecretRef == nil {
+               return nil, fmt.Errorf("basic-auth: either value or secretRef 
must be specified")
+       }
        sec := tctx.Secrets[k8stypes.NamespacedName{
                Namespace: consumerNamespace,
                Name:      cfg.SecretRef.Name,
@@ -159,6 +175,9 @@ func (t *Translator) translateConsumerWolfRBACPlugin(tctx 
*provider.TranslateCon
                        HeaderPrefix: cfg.Value.HeaderPrefix,
                }, nil
        }
+       if cfg.SecretRef == nil {
+               return nil, fmt.Errorf("wolf-rbac: either value or secretRef 
must be specified")
+       }
        sec := tctx.Secrets[k8stypes.NamespacedName{
                Namespace: consumerNamespace,
                Name:      cfg.SecretRef.Name,
@@ -194,6 +213,9 @@ func (t *Translator) translateConsumerJwtAuthPlugin(tctx 
*provider.TranslateCont
                }, nil
        }
 
+       if cfg.SecretRef == nil {
+               return nil, fmt.Errorf("jwt-auth: either value or secretRef 
must be specified")
+       }
        sec := tctx.Secrets[k8stypes.NamespacedName{
                Namespace: consumerNamespace,
                Name:      cfg.SecretRef.Name,
@@ -251,6 +273,9 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx 
*provider.TranslateCon
                }, nil
        }
 
+       if cfg.SecretRef == nil {
+               return nil, fmt.Errorf("hmac-auth: either value or secretRef 
must be specified")
+       }
        sec := tctx.Secrets[k8stypes.NamespacedName{
                Namespace: consumerNamespace,
                Name:      cfg.SecretRef.Name,
@@ -357,6 +382,9 @@ func (t *Translator) translateConsumerLDAPAuthPlugin(tctx 
*provider.TranslateCon
                }, nil
        }
 
+       if cfg.SecretRef == nil {
+               return nil, fmt.Errorf("ldap-auth: either value or secretRef 
must be specified")
+       }
        sec := tctx.Secrets[k8stypes.NamespacedName{
                Namespace: consumerNamespace,
                Name:      cfg.SecretRef.Name,
diff --git a/internal/controller/apisixconsumer_controller.go 
b/internal/controller/apisixconsumer_controller.go
index c40345a5..da3594df 100644
--- a/internal/controller/apisixconsumer_controller.go
+++ b/internal/controller/apisixconsumer_controller.go
@@ -184,39 +184,62 @@ func (r *ApisixConsumerReconciler) 
listApisixConsumerForSecret(ctx context.Conte
 
 func (r *ApisixConsumerReconciler) processSpec(ctx context.Context, tctx 
*provider.TranslateContext, ac *apiv2.ApisixConsumer) error {
        var secretRef *corev1.LocalObjectReference
-       if ac.Spec.AuthParameter.KeyAuth != nil {
-               secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
-       } else if ac.Spec.AuthParameter.BasicAuth != nil {
-               secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
-       } else if ac.Spec.AuthParameter.JwtAuth != nil {
-               secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
-       } else if ac.Spec.AuthParameter.WolfRBAC != nil {
-               secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
-       } else if ac.Spec.AuthParameter.HMACAuth != nil {
-               secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
-       } else if ac.Spec.AuthParameter.LDAPAuth != nil {
-               secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
-       }
-       if secretRef == nil {
-               return nil
+       if ap := ac.Spec.AuthParameter; ap != nil {
+               if ap.KeyAuth != nil {
+                       secretRef = ap.KeyAuth.SecretRef
+               } else if ap.BasicAuth != nil {
+                       secretRef = ap.BasicAuth.SecretRef
+               } else if ap.JwtAuth != nil {
+                       secretRef = ap.JwtAuth.SecretRef
+               } else if ap.WolfRBAC != nil {
+                       secretRef = ap.WolfRBAC.SecretRef
+               } else if ap.HMACAuth != nil {
+                       secretRef = ap.HMACAuth.SecretRef
+               } else if ap.LDAPAuth != nil {
+                       secretRef = ap.LDAPAuth.SecretRef
+               }
        }
-
-       namespacedName := types.NamespacedName{
-               Name:      secretRef.Name,
-               Namespace: ac.Namespace,
+       if secretRef != nil && secretRef.Name != "" {
+               namespacedName := types.NamespacedName{
+                       Name:      secretRef.Name,
+                       Namespace: ac.Namespace,
+               }
+               secret := &corev1.Secret{}
+               if err := r.Get(ctx, namespacedName, secret); err != nil {
+                       if k8serrors.IsNotFound(err) {
+                               r.Log.Info("secret not found", "secret", 
namespacedName)
+                       } else {
+                               r.Log.Error(err, "failed to get secret", 
"secret", namespacedName)
+                               return err
+                       }
+               } else {
+                       tctx.Secrets[namespacedName] = secret
+               }
        }
 
-       secret := &corev1.Secret{}
-       if err := r.Get(ctx, namespacedName, secret); err != nil {
-               if k8serrors.IsNotFound(err) {
-                       r.Log.Info("secret not found", "secret", namespacedName)
-                       return nil
+       for _, plugin := range ac.Spec.Plugins {
+               if !plugin.Enable || plugin.SecretRef == "" {
+                       continue
+               }
+               namespacedName := types.NamespacedName{
+                       Name:      plugin.SecretRef,
+                       Namespace: ac.Namespace,
+               }
+               if _, loaded := tctx.Secrets[namespacedName]; loaded {
+                       continue
+               }
+               secret := &corev1.Secret{}
+               if err := r.Get(ctx, namespacedName, secret); err != nil {
+                       if k8serrors.IsNotFound(err) {
+                               r.Log.Info("secret not found for plugin", 
"plugin", plugin.Name, "secret", namespacedName)
+                       } else {
+                               r.Log.Error(err, "failed to get secret for 
plugin", "plugin", plugin.Name, "secret", namespacedName)
+                               return err
+                       }
                } else {
-                       r.Log.Error(err, "failed to get secret", "secret", 
namespacedName)
-                       return err
+                       tctx.Secrets[namespacedName] = secret
                }
        }
-       tctx.Secrets[namespacedName] = secret
        return nil
 }
 
diff --git a/internal/controller/indexer/indexer.go 
b/internal/controller/indexer/indexer.go
index 6f0d66a4..5172e487 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -879,22 +879,29 @@ func ApisixPluginConfigSecretIndexFunc(obj client.Object) 
(keys []string) {
 func ApisixConsumerSecretIndexFunc(rawObj client.Object) (keys []string) {
        ac := rawObj.(*apiv2.ApisixConsumer)
        var secretRef *corev1.LocalObjectReference
-       if ac.Spec.AuthParameter.KeyAuth != nil {
-               secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
-       } else if ac.Spec.AuthParameter.BasicAuth != nil {
-               secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
-       } else if ac.Spec.AuthParameter.JwtAuth != nil {
-               secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
-       } else if ac.Spec.AuthParameter.WolfRBAC != nil {
-               secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
-       } else if ac.Spec.AuthParameter.HMACAuth != nil {
-               secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
-       } else if ac.Spec.AuthParameter.LDAPAuth != nil {
-               secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
-       }
-       if secretRef != nil {
+       if ap := ac.Spec.AuthParameter; ap != nil {
+               if ap.KeyAuth != nil {
+                       secretRef = ap.KeyAuth.SecretRef
+               } else if ap.BasicAuth != nil {
+                       secretRef = ap.BasicAuth.SecretRef
+               } else if ap.JwtAuth != nil {
+                       secretRef = ap.JwtAuth.SecretRef
+               } else if ap.WolfRBAC != nil {
+                       secretRef = ap.WolfRBAC.SecretRef
+               } else if ap.HMACAuth != nil {
+                       secretRef = ap.HMACAuth.SecretRef
+               } else if ap.LDAPAuth != nil {
+                       secretRef = ap.LDAPAuth.SecretRef
+               }
+       }
+       if secretRef != nil && secretRef.Name != "" {
                keys = append(keys, GenIndexKey(ac.GetNamespace(), 
secretRef.Name))
        }
+       for _, plugin := range ac.Spec.Plugins {
+               if plugin.Enable && plugin.SecretRef != "" {
+                       keys = append(keys, GenIndexKey(ac.GetNamespace(), 
plugin.SecretRef))
+               }
+       }
        return
 }
 
diff --git a/internal/webhook/v1/apisixconsumer_webhook.go 
b/internal/webhook/v1/apisixconsumer_webhook.go
index 796491f5..59212015 100644
--- a/internal/webhook/v1/apisixconsumer_webhook.go
+++ b/internal/webhook/v1/apisixconsumer_webhook.go
@@ -128,24 +128,25 @@ func (v *ApisixConsumerCustomValidator) 
collectWarnings(ctx context.Context, con
                })...)
        }
 
-       params := consumer.Spec.AuthParameter
-       if params.BasicAuth != nil {
-               addSecretWarning(params.BasicAuth.SecretRef)
-       }
-       if params.KeyAuth != nil {
-               addSecretWarning(params.KeyAuth.SecretRef)
-       }
-       if params.WolfRBAC != nil {
-               addSecretWarning(params.WolfRBAC.SecretRef)
-       }
-       if params.JwtAuth != nil {
-               addSecretWarning(params.JwtAuth.SecretRef)
-       }
-       if params.HMACAuth != nil {
-               addSecretWarning(params.HMACAuth.SecretRef)
-       }
-       if params.LDAPAuth != nil {
-               addSecretWarning(params.LDAPAuth.SecretRef)
+       if params := consumer.Spec.AuthParameter; params != nil {
+               if params.BasicAuth != nil {
+                       addSecretWarning(params.BasicAuth.SecretRef)
+               }
+               if params.KeyAuth != nil {
+                       addSecretWarning(params.KeyAuth.SecretRef)
+               }
+               if params.WolfRBAC != nil {
+                       addSecretWarning(params.WolfRBAC.SecretRef)
+               }
+               if params.JwtAuth != nil {
+                       addSecretWarning(params.JwtAuth.SecretRef)
+               }
+               if params.HMACAuth != nil {
+                       addSecretWarning(params.HMACAuth.SecretRef)
+               }
+               if params.LDAPAuth != nil {
+                       addSecretWarning(params.LDAPAuth.SecretRef)
+               }
        }
 
        return warnings
diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go 
b/internal/webhook/v1/apisixconsumer_webhook_test.go
index e1be420d..b1e0f5c7 100644
--- a/internal/webhook/v1/apisixconsumer_webhook_test.go
+++ b/internal/webhook/v1/apisixconsumer_webhook_test.go
@@ -80,7 +80,7 @@ func TestApisixConsumerValidator_MissingBasicAuthSecret(t 
*testing.T) {
                },
                Spec: apisixv2.ApisixConsumerSpec{
                        IngressClassName: "apisix",
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
                                        SecretRef: 
&corev1.LocalObjectReference{Name: "basic-auth"},
                                },
@@ -104,7 +104,7 @@ func TestApisixConsumerValidator_MultipleSecretWarnings(t 
*testing.T) {
                },
                Spec: apisixv2.ApisixConsumerSpec{
                        IngressClassName: "apisix",
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
                                        SecretRef: 
&corev1.LocalObjectReference{Name: "basic-auth"},
                                },
@@ -144,7 +144,7 @@ func 
TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) {
                },
                Spec: apisixv2.ApisixConsumerSpec{
                        IngressClassName: "apisix",
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
                                        SecretRef: 
&corev1.LocalObjectReference{Name: "key-auth"},
                                },
@@ -181,7 +181,7 @@ func 
TestApisixConsumerValidator_DeniesOnADCValidationFailure(t *testing.T) {
                },
                Spec: apisixv2.ApisixConsumerSpec{
                        IngressClassName: "apisix",
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
                                        SecretRef: 
&corev1.LocalObjectReference{Name: "key-auth"},
                                },
@@ -220,7 +220,7 @@ func 
TestApisixConsumerValidator_UsesADCValidateEndpointForControlPlane(t *testi
                },
                Spec: apisixv2.ApisixConsumerSpec{
                        IngressClassName: managedIngressClassName,
-                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                       AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
                                KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
                                        Value: 
&apisixv2.ApisixConsumerKeyAuthValue{Key: "shared-key"},
                                },
diff --git a/test/e2e/crds/v2/consumer.go b/test/e2e/crds/v2/consumer.go
index 589e3329..9d646b80 100644
--- a/test/e2e/crds/v2/consumer.go
+++ b/test/e2e/crds/v2/consumer.go
@@ -671,4 +671,190 @@ spec:
                        Eventually(request).WithArguments("/get", "jack", 
"jackPassword").WithTimeout(5 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
                })
        })
+
+       Context("Test Consumer Plugins - authParameter with extra plugins", 
func() {
+               // Verify that a consumer with authParameter + plugins (e.g. 
limit-count) works:
+               // auth is enforced via authParameter and limit-count throttles 
authenticated traffic.
+               const (
+                       consumerWithPlugins = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: consumer-with-plugins
+spec:
+  ingressClassName: %s
+  authParameter:
+    keyAuth:
+      value:
+        key: plugin-test-key
+  plugins:
+  - name: limit-count
+    enable: true
+    config:
+      count: 2
+      time_window: 60
+      rejected_code: 429
+      key: consumer_name
+      policy: local
+`
+                       pluginRoute = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: plugin-route
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - httpbin
+      paths:
+      - /get
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: 80
+    authentication:
+      enable: true
+      type: keyAuth
+`
+               )
+
+               It("consumer-level limit-count plugin is enforced", func() {
+                       By("apply ApisixRoute")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "plugin-route"},
+                               &apiv2.ApisixRoute{}, fmt.Sprintf(pluginRoute, 
s.Namespace()))
+
+                       By("apply ApisixConsumer with plugins")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "consumer-with-plugins"},
+                               &apiv2.ApisixConsumer{}, 
fmt.Sprintf(consumerWithPlugins, s.Namespace()))
+
+                       By("unauthenticated request is rejected")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/get",
+                               Host:    "httpbin",
+                               Headers: map[string]string{"apikey": 
"wrong-key"},
+                               Check:   
scaffold.WithExpectedStatus(http.StatusUnauthorized),
+                       })
+
+                       By("first authenticated request succeeds")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/get",
+                               Host:    "httpbin",
+                               Headers: map[string]string{"apikey": 
"plugin-test-key"},
+                               Check:   
scaffold.WithExpectedStatus(http.StatusOK),
+                       })
+
+                       By("second authenticated request succeeds")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/get",
+                               Host:    "httpbin",
+                               Headers: map[string]string{"apikey": 
"plugin-test-key"},
+                               Check:   
scaffold.WithExpectedStatus(http.StatusOK),
+                       })
+
+                       By("third request is rate-limited by consumer-level 
limit-count")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/get",
+                               Host:    "httpbin",
+                               Headers: map[string]string{"apikey": 
"plugin-test-key"},
+                               Check:   
scaffold.WithExpectedStatus(http.StatusTooManyRequests),
+                       })
+
+                       By("delete ApisixConsumer")
+                       err := s.DeleteResource("ApisixConsumer", 
"consumer-with-plugins")
+                       Expect(err).ShouldNot(HaveOccurred(), "deleting 
ApisixConsumer")
+
+                       By("delete ApisixRoute")
+                       err = s.DeleteResource("ApisixRoute", "plugin-route")
+                       Expect(err).ShouldNot(HaveOccurred(), "deleting 
ApisixRoute")
+               })
+       })
+
+       Context("Test Consumer Plugins - plugins only (no authParameter)", 
func() {
+               // Verify that authParameter can be omitted entirely and auth 
can be
+               // configured directly via the plugins field.
+               const (
+                       consumerPluginsOnly = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: consumer-plugins-only
+spec:
+  ingressClassName: %s
+  plugins:
+  - name: key-auth
+    enable: true
+    config:
+      key: plugins-only-key
+`
+                       pluginsOnlyRoute = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: plugins-only-route
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - httpbin
+      paths:
+      - /get
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: 80
+    authentication:
+      enable: true
+      type: keyAuth
+`
+               )
+
+               It("auth plugin configured via plugins field only", func() {
+                       By("apply ApisixRoute")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "plugins-only-route"},
+                               &apiv2.ApisixRoute{}, 
fmt.Sprintf(pluginsOnlyRoute, s.Namespace()))
+
+                       By("apply ApisixConsumer with plugins only (no 
authParameter)")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "consumer-plugins-only"},
+                               &apiv2.ApisixConsumer{}, 
fmt.Sprintf(consumerPluginsOnly, s.Namespace()))
+
+                       By("request with wrong key is rejected")
+                       Eventually(func() int {
+                               return s.NewAPISIXClient().GET("/get").
+                                       WithHeader("apikey", "wrong-key").
+                                       WithHost("httpbin").
+                                       Expect().Raw().StatusCode
+                       }).WithTimeout(10 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized))
+
+                       By("request with correct key succeeds")
+                       Eventually(func() int {
+                               return s.NewAPISIXClient().GET("/get").
+                                       WithHeader("apikey", 
"plugins-only-key").
+                                       WithHost("httpbin").
+                                       Expect().Raw().StatusCode
+                       }).WithTimeout(10 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
+
+                       By("delete ApisixConsumer")
+                       err := s.DeleteResource("ApisixConsumer", 
"consumer-plugins-only")
+                       Expect(err).ShouldNot(HaveOccurred(), "deleting 
ApisixConsumer")
+
+                       By("request with correct key is rejected after consumer 
deletion")
+                       Eventually(func() int {
+                               return s.NewAPISIXClient().GET("/get").
+                                       WithHeader("apikey", 
"plugins-only-key").
+                                       WithHost("httpbin").
+                                       Expect().Raw().StatusCode
+                       }).WithTimeout(10 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized))
+
+                       By("delete ApisixRoute")
+                       err = s.DeleteResource("ApisixRoute", 
"plugins-only-route")
+                       Expect(err).ShouldNot(HaveOccurred(), "deleting 
ApisixRoute")
+               })
+       })
 })

Reply via email to