blob: 0998dbf5baf9c340dd9af4f7a8947e1e513d89b1 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed 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 kube
import (
"fmt"
"testing"
)
import (
authorizationv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)
import (
cluster2 "github.com/apache/dubbo-go-pixiu/pkg/cluster"
"github.com/apache/dubbo-go-pixiu/pkg/kube"
"github.com/apache/dubbo-go-pixiu/pkg/kube/multicluster"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
func makeSecret(name string, data map[string]string, secretType corev1.SecretType) *corev1.Secret {
bdata := map[string][]byte{}
for k, v := range data {
bdata[k] = []byte(v)
}
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Data: bdata,
Type: secretType,
}
}
var (
genericCert = makeSecret("generic", map[string]string{
GenericScrtCert: "generic-cert", GenericScrtKey: "generic-key",
}, corev1.SecretTypeTLS)
genericMtlsCert = makeSecret("generic-mtls", map[string]string{
GenericScrtCert: "generic-mtls-cert", GenericScrtKey: "generic-mtls-key", GenericScrtCaCert: "generic-mtls-ca",
}, corev1.SecretTypeTLS)
genericMtlsCertSplit = makeSecret("generic-mtls-split", map[string]string{
GenericScrtCert: "generic-mtls-split-cert", GenericScrtKey: "generic-mtls-split-key",
}, corev1.SecretTypeTLS)
genericMtlsCertSplitCa = makeSecret("generic-mtls-split-cacert", map[string]string{
GenericScrtCaCert: "generic-mtls-split-ca",
}, corev1.SecretTypeTLS)
overlapping = makeSecret("overlap", map[string]string{
GenericScrtCert: "cert", GenericScrtKey: "key", GenericScrtCaCert: "main-ca",
}, corev1.SecretTypeTLS)
overlappingCa = makeSecret("overlap-cacert", map[string]string{
GenericScrtCaCert: "split-ca",
}, corev1.SecretTypeTLS)
tlsCert = makeSecret("tls", map[string]string{
TLSSecretCert: "tls-cert", TLSSecretKey: "tls-key",
}, corev1.SecretTypeTLS)
tlsMtlsCert = makeSecret("tls-mtls", map[string]string{
TLSSecretCert: "tls-mtls-cert", TLSSecretKey: "tls-mtls-key", TLSSecretCaCert: "tls-mtls-ca",
}, corev1.SecretTypeTLS)
tlsMtlsCertSplit = makeSecret("tls-mtls-split", map[string]string{
TLSSecretCert: "tls-mtls-split-cert", TLSSecretKey: "tls-mtls-split-key",
}, corev1.SecretTypeTLS)
tlsMtlsCertSplitCa = makeSecret("tls-mtls-split-cacert", map[string]string{
TLSSecretCaCert: "tls-mtls-split-ca",
}, corev1.SecretTypeTLS)
emptyCert = makeSecret("empty-cert", map[string]string{
TLSSecretCert: "", TLSSecretKey: "tls-key",
}, corev1.SecretTypeTLS)
wrongKeys = makeSecret("wrong-keys", map[string]string{
"foo-bar": "my-cert", TLSSecretKey: "tls-key",
}, corev1.SecretTypeTLS)
dockerjson = makeSecret("docker-json", map[string]string{
corev1.DockerConfigJsonKey: "docker-cred",
}, corev1.SecretTypeDockerConfigJson)
badDockerjson = makeSecret("bad-docker-json", map[string]string{
"docker-key": "docker-cred",
}, corev1.SecretTypeDockerConfigJson)
)
func TestSecretsController(t *testing.T) {
secrets := []runtime.Object{
genericCert,
genericMtlsCert,
genericMtlsCertSplit,
genericMtlsCertSplitCa,
overlapping,
overlappingCa,
tlsCert,
tlsMtlsCert,
tlsMtlsCertSplit,
tlsMtlsCertSplitCa,
emptyCert,
wrongKeys,
}
client := kube.NewFakeClient(secrets...)
sc := NewCredentialsController(client, "")
stop := make(chan struct{})
t.Cleanup(func() {
close(stop)
})
client.RunAndWait(stop)
cases := []struct {
name string
namespace string
cert string
key string
caCert string
expectedError string
expectedCAError string
}{
{
name: "generic",
namespace: "default",
cert: "generic-cert",
key: "generic-key",
expectedCAError: "found secret, but didn't have expected keys cacert or ca.crt; found: cert, key",
},
{
name: "generic-mtls",
namespace: "default",
cert: "generic-mtls-cert",
key: "generic-mtls-key",
caCert: "generic-mtls-ca",
},
{
name: "generic-mtls-split",
namespace: "default",
cert: "generic-mtls-split-cert",
key: "generic-mtls-split-key",
expectedCAError: "found secret, but didn't have expected keys cacert or ca.crt; found: cert, key",
},
{
name: "generic-mtls-split-cacert",
namespace: "default",
caCert: "generic-mtls-split-ca",
expectedError: "found secret, but didn't have expected keys (cert and key) or (tls.crt and tls.key); found: cacert",
},
// The -cacert secret has precedence
{
name: "overlap-cacert",
namespace: "default",
caCert: "split-ca",
expectedError: "found secret, but didn't have expected keys (cert and key) or (tls.crt and tls.key); found: cacert",
},
{
name: "tls",
namespace: "default",
cert: "tls-cert",
key: "tls-key",
expectedCAError: "found secret, but didn't have expected keys cacert or ca.crt; found: tls.crt, tls.key",
},
{
name: "tls-mtls",
namespace: "default",
cert: "tls-mtls-cert",
key: "tls-mtls-key",
caCert: "tls-mtls-ca",
},
{
name: "tls-mtls-split",
namespace: "default",
cert: "tls-mtls-split-cert",
key: "tls-mtls-split-key",
expectedCAError: "found secret, but didn't have expected keys cacert or ca.crt; found: tls.crt, tls.key",
},
{
name: "tls-mtls-split-cacert",
namespace: "default",
caCert: "tls-mtls-split-ca",
expectedError: "found secret, but didn't have expected keys (cert and key) or (tls.crt and tls.key); found: ca.crt",
},
{
name: "generic",
namespace: "wrong-namespace",
expectedError: `secret wrong-namespace/generic not found`,
expectedCAError: `secret wrong-namespace/generic not found`,
},
{
name: "empty-cert",
namespace: "default",
expectedError: `found keys "tls.crt" and "tls.key", but they were empty`,
expectedCAError: "found secret, but didn't have expected keys cacert or ca.crt; found: tls.crt, tls.key",
},
{
name: "wrong-keys",
namespace: "default",
expectedError: `found secret, but didn't have expected keys (cert and key) or (tls.crt and tls.key); found: foo-bar, tls.key`,
expectedCAError: "found secret, but didn't have expected keys cacert or ca.crt; found: foo-bar, tls.key",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
key, cert, err := sc.GetKeyAndCert(tt.name, tt.namespace)
if tt.key != string(key) {
t.Errorf("got key %q, wanted %q", string(key), tt.key)
}
if tt.cert != string(cert) {
t.Errorf("got cert %q, wanted %q", string(cert), tt.cert)
}
if tt.expectedError != errString(err) {
t.Errorf("got err %q, wanted %q", errString(err), tt.expectedError)
}
caCert, err := sc.GetCaCert(tt.name, tt.namespace)
if tt.caCert != string(caCert) {
t.Errorf("got caCert %q, wanted %q", string(caCert), tt.caCert)
}
if tt.expectedCAError != errString(err) {
t.Errorf("got ca err %q, wanted %q", errString(err), tt.expectedCAError)
}
})
}
}
func TestDockerCredentials(t *testing.T) {
secrets := []runtime.Object{
dockerjson,
badDockerjson,
genericCert,
}
client := kube.NewFakeClient(secrets...)
sc := NewCredentialsController(client, "")
stop := make(chan struct{})
t.Cleanup(func() {
close(stop)
})
client.RunAndWait(stop)
cases := []struct {
name string
namespace string
expectedType corev1.SecretType
expectedDockerCred string
expectedDockerError string
}{
{
name: "docker-json",
namespace: "default",
expectedType: corev1.SecretTypeDockerConfigJson,
expectedDockerCred: "docker-cred",
},
{
name: "bad-docker-json",
namespace: "default",
expectedDockerError: "cannot find docker config at secret default/bad-docker-json",
},
{
name: "wrong-name",
namespace: "default",
expectedDockerError: "secret default/wrong-name not found",
},
{
name: "generic",
namespace: "default",
expectedDockerError: "type of secret default/generic is not kubernetes.io/dockerconfigjson",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
dockerCred, err := sc.GetDockerCredential(tt.name, tt.namespace)
if tt.expectedDockerCred != "" && tt.expectedDockerCred != string(dockerCred) {
t.Errorf("got docker credential %q, want %q", string(dockerCred), tt.expectedDockerCred)
}
if tt.expectedDockerError != "" && tt.expectedDockerError != errString(err) {
t.Errorf("got docker err %q, wanted %q", errString(err), tt.expectedDockerError)
}
})
}
}
func errString(e error) string {
if e == nil {
return ""
}
return e.Error()
}
func allowIdentities(c kube.Client, identities ...string) {
allowed := sets.New(identities...)
c.Kube().(*fake.Clientset).Fake.PrependReactor("create", "subjectaccessreviews", func(action k8stesting.Action) (bool, runtime.Object, error) {
a := action.(k8stesting.CreateAction).GetObject().(*authorizationv1.SubjectAccessReview)
if allowed.Contains(a.Spec.User) {
return true, &authorizationv1.SubjectAccessReview{
Status: authorizationv1.SubjectAccessReviewStatus{
Allowed: true,
},
}, nil
}
return true, &authorizationv1.SubjectAccessReview{
Status: authorizationv1.SubjectAccessReviewStatus{
Allowed: false,
Reason: fmt.Sprintf("user %s cannot access secrets", a.Spec.User),
},
}, nil
})
}
func TestForCluster(t *testing.T) {
localClient := kube.NewFakeClient()
remoteClient := kube.NewFakeClient()
sc := NewMulticluster("local")
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "local", Client: localClient}, nil)
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "remote", Client: remoteClient}, nil)
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "remote2", Client: remoteClient}, nil)
cases := []struct {
cluster cluster2.ID
allowed bool
}{
{"local", true},
{"remote", true},
{"remote2", true},
{"invalid", false},
}
for _, tt := range cases {
t.Run(string(tt.cluster), func(t *testing.T) {
_, err := sc.ForCluster(tt.cluster)
if (err == nil) != tt.allowed {
t.Fatalf("expected allowed=%v, got err=%v", tt.allowed, err)
}
})
}
}
func TestAuthorize(t *testing.T) {
localClient := kube.NewFakeClient()
remoteClient := kube.NewFakeClient()
allowIdentities(localClient, "system:serviceaccount:ns-local:sa-allowed")
allowIdentities(remoteClient, "system:serviceaccount:ns-remote:sa-allowed")
sc := NewMulticluster("local")
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "local", Client: localClient}, nil)
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "remote", Client: remoteClient}, nil)
cases := []struct {
sa string
ns string
cluster cluster2.ID
allowed bool
}{
{"sa-denied", "ns-local", "local", false},
{"sa-allowed", "ns-local", "local", true},
{"sa-denied", "ns-local", "remote", false},
{"sa-allowed", "ns-local", "remote", false},
{"sa-denied", "ns-remote", "local", false},
{"sa-allowed", "ns-remote", "local", false},
{"sa-denied", "ns-remote", "remote", false},
{"sa-allowed", "ns-remote", "remote", true},
}
for _, tt := range cases {
t.Run(fmt.Sprintf("%v/%v/%v", tt.sa, tt.ns, tt.cluster), func(t *testing.T) {
con, err := sc.ForCluster(tt.cluster)
if err != nil {
t.Fatal(err)
}
got := con.Authorize(tt.sa, tt.ns)
if (got == nil) != tt.allowed {
t.Fatalf("expected allowed=%v, got error=%v", tt.allowed, got)
}
})
}
}
func TestSecretsControllerMulticluster(t *testing.T) {
stop := make(chan struct{})
defer close(stop)
secretsLocal := []runtime.Object{
tlsCert,
tlsMtlsCert,
tlsMtlsCertSplit,
tlsMtlsCertSplitCa,
}
tlsCertModified := makeSecret("tls", map[string]string{
TLSSecretCert: "tls-cert-mod", TLSSecretKey: "tls-key",
}, corev1.SecretTypeTLS)
secretsRemote := []runtime.Object{
tlsCertModified,
genericCert,
genericMtlsCert,
genericMtlsCertSplit,
genericMtlsCertSplitCa,
}
localClient := kube.NewFakeClient(secretsLocal...)
remoteClient := kube.NewFakeClient(secretsRemote...)
otherRemoteClient := kube.NewFakeClient()
sc := NewMulticluster("local")
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "local", Client: localClient}, nil)
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "remote", Client: remoteClient}, nil)
_ = sc.ClusterAdded(&multicluster.Cluster{ID: "other", Client: otherRemoteClient}, nil)
// normally the remote secrets controller would start these
localClient.RunAndWait(stop)
remoteClient.RunAndWait(stop)
otherRemoteClient.RunAndWait(stop)
cases := []struct {
name string
namespace string
cluster cluster2.ID
cert string
key string
caCert string
}{
// From local cluster
// These are only in remote cluster, we do not have access
{"generic", "default", "local", "", "", ""},
{"generic-mtls", "default", "local", "", "", ""},
{"generic-mtls-split", "default", "local", "", "", ""},
{"generic-mtls-split-cacert", "default", "local", "", "", ""},
// These are in local cluster, we can access
{"tls", "default", "local", "tls-cert", "tls-key", ""},
{"tls-mtls", "default", "local", "tls-mtls-cert", "tls-mtls-key", "tls-mtls-ca"},
{"tls-mtls-split", "default", "local", "tls-mtls-split-cert", "tls-mtls-split-key", ""},
{"tls-mtls-split-cacert", "default", "local", "", "", "tls-mtls-split-ca"},
{"generic", "wrong-namespace", "local", "", "", ""},
// From remote cluster
// We can access all credentials - local and remote
{"generic", "default", "remote", "generic-cert", "generic-key", ""},
{"generic-mtls", "default", "remote", "generic-mtls-cert", "generic-mtls-key", "generic-mtls-ca"},
{"generic-mtls-split", "default", "remote", "generic-mtls-split-cert", "generic-mtls-split-key", ""},
{"generic-mtls-split-cacert", "default", "remote", "", "", "generic-mtls-split-ca"},
// This is present in local and remote, but with a different value. We have the remote.
{"tls", "default", "remote", "tls-cert-mod", "tls-key", ""},
{"tls-mtls", "default", "remote", "tls-mtls-cert", "tls-mtls-key", "tls-mtls-ca"},
{"tls-mtls-split", "default", "remote", "tls-mtls-split-cert", "tls-mtls-split-key", ""},
{"tls-mtls-split-cacert", "default", "remote", "", "", "tls-mtls-split-ca"},
{"generic", "wrong-namespace", "remote", "", "", ""},
// From other remote cluster
// We have no in cluster credentials; can only access those in config cluster
{"generic", "default", "other", "", "", ""},
{"generic-mtls", "default", "other", "", "", ""},
{"generic-mtls-split", "default", "other", "", "", ""},
{"generic-mtls-split-cacert", "default", "other", "", "", ""},
{"tls", "default", "other", "tls-cert", "tls-key", ""},
{"tls-mtls", "default", "other", "tls-mtls-cert", "tls-mtls-key", "tls-mtls-ca"},
{"tls-mtls-split", "default", "other", "tls-mtls-split-cert", "tls-mtls-split-key", ""},
{"tls-mtls-split-cacert", "default", "other", "", "", "tls-mtls-split-ca"},
{"generic", "wrong-namespace", "other", "", "", ""},
}
for _, tt := range cases {
t.Run(fmt.Sprintf("%s-%v", tt.name, tt.cluster), func(t *testing.T) {
con, err := sc.ForCluster(tt.cluster)
if err != nil {
t.Fatal(err)
}
key, cert, _ := con.GetKeyAndCert(tt.name, tt.namespace)
if tt.key != string(key) {
t.Errorf("got key %q, wanted %q", string(key), tt.key)
}
if tt.cert != string(cert) {
t.Errorf("got cert %q, wanted %q", string(cert), tt.cert)
}
caCert, err := con.GetCaCert(tt.name, tt.namespace)
if tt.caCert != string(caCert) {
t.Errorf("got caCert %q, wanted %q with err %v", string(caCert), tt.caCert, err)
}
})
}
}