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
22 changes: 22 additions & 0 deletions internal/controller/consumer_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/apache/apisix-ingress-controller/api/v1alpha1"
"github.com/apache/apisix-ingress-controller/internal/controller/config"
Expand Down Expand Up @@ -253,6 +254,27 @@ func (r *ConsumerReconciler) processSpec(ctx context.Context, tctx *provider.Tra
if credential.SecretRef.Namespace != nil {
ns = *credential.SecretRef.Namespace
}
// A cross-namespace SecretRef needs a ReferenceGrant, same as routes.
secretNS := gatewayv1.Namespace(ns)
if permitted := checkReferenceGrant(ctx,
r.Client,
v1beta1.ReferenceGrantFrom{
Group: v1beta1.Group(v1alpha1.GroupVersion.Group),
Kind: v1beta1.Kind(internaltypes.KindConsumer),
Namespace: v1beta1.Namespace(consumer.GetNamespace()),
},
gatewayv1.ObjectReference{
Group: corev1.GroupName,
Kind: KindSecret,
Name: gatewayv1.ObjectName(credential.SecretRef.Name),
Namespace: &secretNS,
},
); !permitted {
r.Log.Error(nil, "cross-namespace secret reference not permitted by any ReferenceGrant",
"consumer", utils.NamespacedName(consumer), "secret", client.ObjectKey{Namespace: ns, Name: credential.SecretRef.Name})
return fmt.Errorf("cross-namespace secret reference from Consumer %s/%s to Secret %s/%s is not permitted by any ReferenceGrant",
consumer.GetNamespace(), consumer.GetName(), ns, credential.SecretRef.Name)
}
secret := corev1.Secret{}
if err := r.Get(ctx, client.ObjectKey{
Name: credential.SecretRef.Name,
Expand Down
148 changes: 148 additions & 0 deletions internal/controller/consumer_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package controller

import (
"context"
"testing"

"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/apache/apisix-ingress-controller/api/v1alpha1"
"github.com/apache/apisix-ingress-controller/internal/provider"
)

const (
consumerNS = "team-a"
secretNS = "team-b"
)

func buildConsumerReconciler(t *testing.T, objs ...runtime.Object) *ConsumerReconciler {
t.Helper()

scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, v1alpha1.AddToScheme(scheme))
require.NoError(t, gatewayv1.Install(scheme))
require.NoError(t, v1beta1.Install(scheme))

cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
return &ConsumerReconciler{Client: cli, Log: logr.Discard()}
}

func crossNamespaceConsumer() *v1alpha1.Consumer {
target := secretNS
return &v1alpha1.Consumer{
ObjectMeta: metav1.ObjectMeta{Name: "attacker", Namespace: consumerNS},
Spec: v1alpha1.ConsumerSpec{
Credentials: []v1alpha1.Credential{{
Name: "cred",
Type: "key-auth",
SecretRef: &v1alpha1.SecretReference{Name: "victim-secret", Namespace: &target},
}},
},
}
}

func victimSecret() *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "victim-secret", Namespace: secretNS},
Data: map[string][]byte{"key": []byte("victim-key")},
}
}

func secretGrant() *v1beta1.ReferenceGrant {
return &v1beta1.ReferenceGrant{
ObjectMeta: metav1.ObjectMeta{Name: "allow-consumer", Namespace: secretNS},
Spec: v1beta1.ReferenceGrantSpec{
From: []v1beta1.ReferenceGrantFrom{{
Group: v1beta1.Group(v1alpha1.GroupVersion.Group),
Kind: "Consumer",
Namespace: consumerNS,
}},
To: []v1beta1.ReferenceGrantTo{{
Group: "",
Kind: "Secret",
}},
},
}
}

// Without a ReferenceGrant the cross-namespace secret must not be bound.
func TestProcessSpec_CrossNamespaceSecretRef_DeniedWithoutGrant(t *testing.T) {
SetEnableReferenceGrant(true)
defer SetEnableReferenceGrant(false)

r := buildConsumerReconciler(t, victimSecret())
consumer := crossNamespaceConsumer()
tctx := provider.NewDefaultTranslateContext(context.Background())

err := r.processSpec(context.Background(), tctx, consumer)
require.Error(t, err)
require.Empty(t, tctx.Secrets, "foreign secret must not be loaded without a ReferenceGrant")
}

// A matching ReferenceGrant permits the cross-namespace secret.
func TestProcessSpec_CrossNamespaceSecretRef_AllowedWithGrant(t *testing.T) {
SetEnableReferenceGrant(true)
defer SetEnableReferenceGrant(false)

r := buildConsumerReconciler(t, victimSecret(), secretGrant())
consumer := crossNamespaceConsumer()
tctx := provider.NewDefaultTranslateContext(context.Background())

err := r.processSpec(context.Background(), tctx, consumer)
require.NoError(t, err)
require.Contains(t, tctx.Secrets, types.NamespacedName{Namespace: secretNS, Name: "victim-secret"})
}

// Same-namespace SecretRef needs no grant.
func TestProcessSpec_SameNamespaceSecretRef_Allowed(t *testing.T) {
SetEnableReferenceGrant(true)
defer SetEnableReferenceGrant(false)

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "local-secret", Namespace: consumerNS},
Data: map[string][]byte{"key": []byte("local-key")},
}
r := buildConsumerReconciler(t, secret)
consumer := &v1alpha1.Consumer{
ObjectMeta: metav1.ObjectMeta{Name: "local", Namespace: consumerNS},
Spec: v1alpha1.ConsumerSpec{
Credentials: []v1alpha1.Credential{{
Name: "cred",
Type: "key-auth",
SecretRef: &v1alpha1.SecretReference{Name: "local-secret"},
}},
},
}
tctx := provider.NewDefaultTranslateContext(context.Background())

err := r.processSpec(context.Background(), tctx, consumer)
require.NoError(t, err)
require.Contains(t, tctx.Secrets, types.NamespacedName{Namespace: consumerNS, Name: "local-secret"})
}
69 changes: 65 additions & 4 deletions internal/webhook/v1/consumer_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,48 @@ import (
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"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"
)

// authNS is a foreign namespace used for cross-namespace secretRef tests.
const authNS = "auth"

// enableReferenceGrant turns on the cross-namespace ReferenceGrant gate for the
// duration of a test and resets it afterwards.
func enableReferenceGrant(t *testing.T) {
t.Helper()
controller.SetEnableReferenceGrant(true)
t.Cleanup(func() { controller.SetEnableReferenceGrant(false) })
}

// consumerToSecretGrant permits Consumers in fromNS to reference Secrets in grantNS.
func consumerToSecretGrant(grantNS, fromNS string) *v1beta1.ReferenceGrant {
return &v1beta1.ReferenceGrant{
ObjectMeta: metav1.ObjectMeta{Name: "allow-consumer", Namespace: grantNS},
Spec: v1beta1.ReferenceGrantSpec{
From: []v1beta1.ReferenceGrantFrom{{
Group: v1beta1.Group(apisixv1alpha1.GroupVersion.Group),
Kind: "Consumer",
Namespace: v1beta1.Namespace(fromNS),
}},
To: []v1beta1.ReferenceGrantTo{{Group: "", Kind: "Secret"}},
},
}
}

func buildConsumerValidator(t *testing.T, objects ...runtime.Object) *ConsumerCustomValidator {
t.Helper()

scheme := runtime.NewScheme()
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 @@ -90,7 +119,7 @@ func TestConsumerValidator_MissingSecretDefaultNamespace(t *testing.T) {
}

func TestConsumerValidator_MissingSecretCustomNamespace(t *testing.T) {
ns := "auth"
ns := authNS
consumer := &apisixv1alpha1.Consumer{
ObjectMeta: metav1.ObjectMeta{
Name: "demo",
Expand All @@ -108,16 +137,46 @@ func TestConsumerValidator_MissingSecretCustomNamespace(t *testing.T) {
},
}

validator := buildConsumerValidator(t)
enableReferenceGrant(t)
validator := buildConsumerValidator(t, consumerToSecretGrant(authNS, "default"))

warnings, err := validator.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")
}

// A cross-namespace secretRef without a permitting ReferenceGrant is rejected at admission.
func TestConsumerValidator_CrossNamespaceSecretRefDeniedWithoutGrant(t *testing.T) {
enableReferenceGrant(t)
ns := authNS
consumer := &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 := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: authNS}}
validator := buildConsumerValidator(t, secret)

_, err := validator.ValidateCreate(context.Background(), consumer)
require.Error(t, err)
require.Contains(t, err.Error(), "not permitted by any ReferenceGrant")
}

func TestConsumerValidator_NoWarnings(t *testing.T) {
ns := "auth"
ns := authNS
consumer := &apisixv1alpha1.Consumer{
ObjectMeta: metav1.ObjectMeta{
Name: "demo",
Expand All @@ -140,9 +199,11 @@ func TestConsumerValidator_NoWarnings(t *testing.T) {
},
}

enableReferenceGrant(t)
objs := []runtime.Object{
&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: "auth"}},
&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "jwt-secret", Namespace: authNS}},
&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "key-secret", Namespace: "default"}},
consumerToSecretGrant(authNS, "default"),
}

validator := buildConsumerValidator(t, objs...)
Expand Down
Loading