Skip to content
Open
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
19 changes: 19 additions & 0 deletions internal/controller/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
)
Comment on lines +1350 to +1364

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve ReferenceGrant lookup failures separately from denial.

checkReferenceGrant maps cli.List errors to false, so this helper makes the webhook report a missing ReferenceGrant when authorization could not be evaluated. Keep the fail-closed/no-Secret-probe behavior, but propagate or log the lookup error and emit a neutral validation warning instead.

As per coding guidelines, errors must be properly handled and not silently swallowed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/utils.go` around lines 1350 - 1364, Update
CheckConsumerSecretRef and the underlying checkReferenceGrant flow so
ReferenceGrant list failures remain distinguishable from an explicit
authorization denial. Preserve fail-closed behavior and avoid probing the
Secret, but propagate or log the lookup error and have the webhook emit a
neutral validation warning when evaluation fails instead of reporting a missing
ReferenceGrant; ensure the error is not silently swallowed.

Source: Coding guidelines

}

func ListRequests(
ctx context.Context,
c client.Client,
Expand Down
8 changes: 8 additions & 0 deletions internal/webhook/v1/consumer_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
84 changes: 77 additions & 7 deletions internal/webhook/v1/consumer_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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{
Expand Down Expand Up @@ -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)
Comment on lines +96 to +140

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise denial with ReferenceGrant evaluation enabled and restore prior state.

The oracle test currently passes when ReferenceGrant support is disabled, before grant lookup is exercised. Save the existing flag, enable it for both cross-namespace tests, and restore the saved value with cleanup; the current unconditional reset to false can leak global state into other tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/webhook/v1/consumer_webhook_test.go` around lines 96 - 140, Update
both cross-namespace tests, including
TestConsumerValidator_CrossNamespaceSecretOracleSuppressed and
TestConsumerValidator_CrossNamespaceSecretWithGrant, to save the existing
ReferenceGrant enablement state, enable it during each test, and restore the
saved value via cleanup. Replace the unconditional reset to false so global
controller state is preserved for subsequent tests.


ns := "auth"
consumer := &apisixv1alpha1.Consumer{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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"}},
}

Expand Down
Loading