diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 04400609..84eac81f 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -1345,6 +1345,25 @@ func checkReferenceGrant(ctx context.Context, cli client.Client, obj v1beta1.Ref return false } +// CheckConsumerSecretRef reports whether a Consumer in fromNamespace may reference +// the Secret at secretNN, honoring ReferenceGrant for cross-namespace references. +func CheckConsumerSecretRef(ctx context.Context, cli client.Client, fromNamespace string, secretNN k8stypes.NamespacedName) bool { + secretNS := secretNN.Namespace + return checkReferenceGrant(ctx, cli, + v1beta1.ReferenceGrantFrom{ + Group: v1beta1.Group(v1alpha1.GroupVersion.Group), + Kind: types.KindConsumer, + Namespace: v1beta1.Namespace(fromNamespace), + }, + gatewayv1.ObjectReference{ + Group: corev1.GroupName, + Kind: types.KindSecret, + Name: gatewayv1.ObjectName(secretNN.Name), + Namespace: (*gatewayv1.Namespace)(&secretNS), + }, + ) +} + func ListRequests( ctx context.Context, c client.Client, diff --git a/internal/webhook/v1/consumer_webhook.go b/internal/webhook/v1/consumer_webhook.go index 57767520..db390cf9 100644 --- a/internal/webhook/v1/consumer_webhook.go +++ b/internal/webhook/v1/consumer_webhook.go @@ -134,6 +134,14 @@ func (v *ConsumerCustomValidator) collectWarnings(ctx context.Context, consumer } visited[nn] = struct{}{} + // Don't probe cross-namespace Secrets that no ReferenceGrant permits: the + // found/not-found warning difference would leak Secret existence across + // namespaces. Emit a uniform message and skip the lookup. + if namespace != defaultNamespace && !controller.CheckConsumerSecretRef(ctx, v.Client, defaultNamespace, nn) { + warnings = append(warnings, fmt.Sprintf("Referenced Secret '%s/%s' is not accessible from this Consumer without a ReferenceGrant", nn.Namespace, nn.Name)) + continue + } + warnings = append(warnings, v.checker.Secret(ctx, reference.SecretRef{ Object: consumer, NamespacedName: nn, diff --git a/internal/webhook/v1/consumer_webhook_test.go b/internal/webhook/v1/consumer_webhook_test.go index 4dc32b84..7c4abffc 100644 --- a/internal/webhook/v1/consumer_webhook_test.go +++ b/internal/webhook/v1/consumer_webhook_test.go @@ -28,7 +28,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1beta1" + apisixv1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1" + "github.com/apache/apisix-ingress-controller/internal/controller" "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/controller/indexer" ) @@ -40,6 +43,7 @@ func buildConsumerValidator(t *testing.T, objects ...runtime.Object) *ConsumerCu require.NoError(t, clientgoscheme.AddToScheme(scheme)) require.NoError(t, apisixv1alpha1.AddToScheme(scheme)) require.NoError(t, gatewayv1.Install(scheme)) + require.NoError(t, v1beta1.Install(scheme)) managed := []runtime.Object{ &gatewayv1.GatewayClass{ @@ -89,7 +93,52 @@ func TestConsumerValidator_MissingSecretDefaultNamespace(t *testing.T) { require.Contains(t, warnings[0], "Referenced Secret 'default/jwt-secret' not found") } -func TestConsumerValidator_MissingSecretCustomNamespace(t *testing.T) { +// Cross-namespace refs without a permitting ReferenceGrant must not reveal whether +// the Secret exists: found and not-found both yield the same uniform warning. +func TestConsumerValidator_CrossNamespaceSecretOracleSuppressed(t *testing.T) { + ns := "auth" + newConsumer := func() *apisixv1alpha1.Consumer { + return &apisixv1alpha1.Consumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "demo", + Namespace: "default", + }, + Spec: apisixv1alpha1.ConsumerSpec{ + GatewayRef: apisixv1alpha1.GatewayRef{Name: "test-gateway"}, + Credentials: []apisixv1alpha1.Credential{{ + Type: "jwt-auth", + SecretRef: &apisixv1alpha1.SecretReference{ + Name: "jwt-secret", + Namespace: &ns, + }, + }}, + }, + } + } + + // Secret present in the target namespace, but no ReferenceGrant permits the ref. + present := buildConsumerValidator(t, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: "auth"}, + }) + presentWarnings, err := present.ValidateCreate(context.Background(), newConsumer()) + require.NoError(t, err) + + // Secret absent. + absent := buildConsumerValidator(t) + absentWarnings, err := absent.ValidateCreate(context.Background(), newConsumer()) + require.NoError(t, err) + + // Identical output either way: no existence oracle. + require.Equal(t, absentWarnings, presentWarnings) + require.Len(t, presentWarnings, 1) + require.Contains(t, presentWarnings[0], "Referenced Secret 'auth/jwt-secret' is not accessible from this Consumer without a ReferenceGrant") +} + +// A ReferenceGrant re-enables the normal existence check for the permitted namespace. +func TestConsumerValidator_CrossNamespaceSecretWithGrant(t *testing.T) { + controller.SetEnableReferenceGrant(true) + defer controller.SetEnableReferenceGrant(false) + ns := "auth" consumer := &apisixv1alpha1.Consumer{ ObjectMeta: metav1.ObjectMeta{ @@ -108,16 +157,38 @@ func TestConsumerValidator_MissingSecretCustomNamespace(t *testing.T) { }, } - validator := buildConsumerValidator(t) + grant := &v1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{Name: "allow-default-consumers", Namespace: "auth"}, + Spec: v1beta1.ReferenceGrantSpec{ + From: []v1beta1.ReferenceGrantFrom{{ + Group: v1beta1.Group("apisix.apache.org"), + Kind: v1beta1.Kind("Consumer"), + Namespace: v1beta1.Namespace("default"), + }}, + To: []v1beta1.ReferenceGrantTo{{ + Group: v1beta1.Group(""), + Kind: v1beta1.Kind("Secret"), + }}, + }, + } - warnings, err := validator.ValidateCreate(context.Background(), consumer) + // Grant + existing Secret: no warning. + allowed := buildConsumerValidator(t, grant, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: "auth"}, + }) + warnings, err := allowed.ValidateCreate(context.Background(), consumer) + require.NoError(t, err) + require.Empty(t, warnings) + + // Grant but Secret missing: the honest not-found warning is allowed again. + missing := buildConsumerValidator(t, grant) + warnings, err = missing.ValidateCreate(context.Background(), consumer) require.NoError(t, err) require.Len(t, warnings, 1) require.Contains(t, warnings[0], "Referenced Secret 'auth/jwt-secret' not found") } func TestConsumerValidator_NoWarnings(t *testing.T) { - ns := "auth" consumer := &apisixv1alpha1.Consumer{ ObjectMeta: metav1.ObjectMeta{ Name: "demo", @@ -128,8 +199,7 @@ func TestConsumerValidator_NoWarnings(t *testing.T) { Credentials: []apisixv1alpha1.Credential{{ Type: "jwt-auth", SecretRef: &apisixv1alpha1.SecretReference{ - Name: "jwt-secret", - Namespace: &ns, + Name: "jwt-secret", }, }, { Type: "key-auth", @@ -141,7 +211,7 @@ func TestConsumerValidator_NoWarnings(t *testing.T) { } objs := []runtime.Object{ - &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: "auth"}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: "default"}}, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "key-secret", Namespace: "default"}}, }