diff options
Diffstat (limited to 'pkg/authn/kubernetes/keychain_test.go')
-rw-r--r-- | pkg/authn/kubernetes/keychain_test.go | 586 |
1 files changed, 586 insertions, 0 deletions
diff --git a/pkg/authn/kubernetes/keychain_test.go b/pkg/authn/kubernetes/keychain_test.go new file mode 100644 index 0000000..e015771 --- /dev/null +++ b/pkg/authn/kubernetes/keychain_test.go @@ -0,0 +1,586 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// 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 kubernetes + +import ( + "context" + "crypto/md5" + "encoding/base64" + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + fakeclient "k8s.io/client-go/kubernetes/fake" +) + +var dockerSecretTypes = []secretType{ + dockerConfigJSONSecretType, + dockerCfgSecretType, +} + +type secretType struct { + name corev1.SecretType + key string + marshal func(t *testing.T, registry string, auth authn.AuthConfig) []byte +} + +func (s *secretType) Create(t *testing.T, namespace, name string, registry string, auth authn.AuthConfig) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: s.name, + Data: map[string][]byte{ + s.key: s.marshal(t, registry, auth), + }, + } +} + +var dockerConfigJSONSecretType = secretType{ + name: corev1.SecretTypeDockerConfigJson, + key: corev1.DockerConfigJsonKey, + marshal: func(t *testing.T, target string, auth authn.AuthConfig) []byte { + return toJSON(t, dockerConfigJSON{ + Auths: map[string]authn.AuthConfig{target: auth}, + }) + }, +} + +var dockerCfgSecretType = secretType{ + name: corev1.SecretTypeDockercfg, + key: corev1.DockerConfigKey, + marshal: func(t *testing.T, target string, auth authn.AuthConfig) []byte { + return toJSON(t, map[string]authn.AuthConfig{target: auth}) + }, +} + +func TestAnonymousFallback(t *testing.T) { + client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "default", + }, + }) + + kc, err := New(context.Background(), client, Options{}) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestAnonymousFallbackNoServiceAccount(t *testing.T) { + kc, err := New(context.Background(), nil, Options{ + ServiceAccountName: NoServiceAccount, + }) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestSecretNotFound(t *testing.T) { + client := fakeclient.NewSimpleClientset() + + kc, err := New(context.Background(), client, Options{ + ServiceAccountName: NoServiceAccount, + ImagePullSecrets: []string{"not-found"}, + }) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestServiceAccountNotFound(t *testing.T) { + client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "default", + }, + }) + kc, err := New(context.Background(), client, Options{ + ServiceAccountName: "not-found", + }) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestImagePullSecretAttachedServiceAccount(t *testing.T) { + username, password := "foo", "bar" + client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svcacct", + Namespace: "ns", + }, + ImagePullSecrets: []corev1.LocalObjectReference{{ + Name: "secret", + }}, + }, + dockerCfgSecretType.Create(t, "ns", "secret", "fake.registry.io", authn.AuthConfig{ + Username: username, + Password: password, + }), + ) + + kc, err := New(context.Background(), client, Options{ + Namespace: "ns", + ServiceAccountName: "svcacct", + }) + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), + &authn.Basic{Username: username, Password: password}) +} + +func TestSecretAttachedServiceAccount(t *testing.T) { + username, password := "foo", "bar" + + cases := []struct { + name string + createSecret bool + useMountSecrets bool + expected authn.Authenticator + }{ + { + name: "resolved successfully", + createSecret: true, + useMountSecrets: true, + expected: &authn.Basic{Username: username, Password: password}, + }, + { + name: "missing secret skipped", + createSecret: false, + useMountSecrets: true, + expected: &authn.Basic{}, + }, + { + name: "skip option", + createSecret: true, + useMountSecrets: false, + expected: &authn.Basic{}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + + objs := []runtime.Object{ + &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svcacct", + Namespace: "ns", + }, + Secrets: []corev1.ObjectReference{{ + Name: "secret", + }}, + }, + } + if c.createSecret { + objs = append(objs, dockerCfgSecretType.Create( + t, "ns", "secret", "fake.registry.io", authn.AuthConfig{ + Username: username, + Password: password, + })) + } + client := fakeclient.NewSimpleClientset(objs...) + + kc, err := New(context.Background(), client, Options{ + Namespace: "ns", + ServiceAccountName: "svcacct", + UseMountSecrets: c.useMountSecrets, + }) + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), c.expected) + }) + } + +} + +// Prioritze picking the first secret +func TestSecretPriority(t *testing.T) { + secrets := []corev1.Secret{ + *dockerCfgSecretType.Create(t, "ns", "secret", "fake.registry.io", authn.AuthConfig{ + Username: "user", Password: "pass", + }), + *dockerCfgSecretType.Create(t, "ns", "secret-2", "fake.registry.io", authn.AuthConfig{ + Username: "anotherUser", Password: "anotherPass", + }), + } + + kc, err := NewFromPullSecrets(context.Background(), secrets) + if err != nil { + t.Fatalf("NewFromPullSecrets() = %v", err) + } + + expectedAuth := &authn.Basic{Username: "user", Password: "pass"} + testResolve(t, kc, registry(t, "fake.registry.io"), expectedAuth) +} + +func TestResolveTargets(t *testing.T) { + // Iterate over target types + targetTypes := []authn.Resource{ + registry(t, "fake.registry.io"), + repo(t, "fake.registry.io/repo"), + } + + for _, secretType := range dockerSecretTypes { + for _, target := range targetTypes { + // Drop the . + testName := secretType.key[1:] + "_" + target.String() + + t.Run(testName, func(t *testing.T) { + auth := authn.AuthConfig{ + Password: fmt.Sprintf("%x", md5.Sum([]byte(t.Name()))), + Username: "user" + fmt.Sprintf("%x", md5.Sum([]byte(t.Name()))), + } + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *secretType.Create(t, "ns", "secret", target.String(), auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password} + testResolve(t, kc, target, authenticator) + }) + } + } +} + +func TestAuthWithScheme(t *testing.T) { + auth := authn.AuthConfig{ + Password: "password", + Username: "username", + } + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "https://fake.registry.io", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password} + testResolve(t, kc, registry(t, "fake.registry.io"), authenticator) + testResolve(t, kc, repo(t, "fake.registry.io/repo"), authenticator) +} + +func TestAuthWithPorts(t *testing.T) { + auth := authn.AuthConfig{ + Password: "password", + Username: "username", + } + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "fake.registry.io:5000", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password} + testResolve(t, kc, registry(t, "fake.registry.io:5000"), authenticator) + testResolve(t, kc, repo(t, "fake.registry.io:5000/repo"), authenticator) + + // Non-matching ports should return Anonymous + testResolve(t, kc, registry(t, "fake.registry.io:1000"), authn.Anonymous) + testResolve(t, kc, repo(t, "fake.registry.io:1000/repo"), authn.Anonymous) +} + +func TestAuthPathMatching(t *testing.T) { + rootAuth := authn.AuthConfig{Username: "root", Password: "root"} + nestedAuth := authn.AuthConfig{Username: "nested", Password: "nested"} + leafAuth := authn.AuthConfig{Username: "leaf", Password: "leaf"} + partialAuth := authn.AuthConfig{Username: "partial", Password: "partial"} + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "fake.registry.io", rootAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "fake.registry.io/nested", nestedAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-3", "fake.registry.io/nested/repo", leafAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-4", "fake.registry.io/par", partialAuth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(rootAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/nested"), authn.FromConfig(nestedAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/nested/repo"), authn.FromConfig(leafAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/nested/repo/dirt"), authn.FromConfig(leafAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/partial"), authn.FromConfig(partialAuth)) +} + +func TestAuthHostNameVariations(t *testing.T) { + rootAuth := authn.AuthConfig{Username: "root", Password: "root"} + subdomainAuth := authn.AuthConfig{Username: "sub", Password: "sub"} + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "fake.registry.io", rootAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "1.fake.registry.io", subdomainAuth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(rootAuth)) + testResolve(t, kc, registry(t, "1.fake.registry.io"), authn.FromConfig(subdomainAuth)) + + // Unrecognized subdomain uses Anonymous + testResolve(t, kc, registry(t, "2.fake.registry.io"), authn.Anonymous) +} + +func TestAuthSpecialPathsIgnored(t *testing.T) { + auth := authn.AuthConfig{Username: "root", Password: "root"} + auth2 := authn.AuthConfig{Username: "root2", Password: "root2"} + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + // Note the paths need a trailing '/' + *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "https://fake.registry.io/v1/", auth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "https://fake2.registry.io/v2/", auth2), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "fake.registry.io/repo"), authn.FromConfig(auth)) + testResolve(t, kc, registry(t, "fake2.registry.io"), authn.FromConfig(auth2)) + testResolve(t, kc, repo(t, "fake2.registry.io/repo"), authn.FromConfig(auth2)) +} + +func TestAuthDockerRegistry(t *testing.T) { + auth := authn.AuthConfig{Username: "root", Password: "root"} + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "index.docker.io", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, repo(t, "ubuntu"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "knative/serving"), authn.FromConfig(auth)) +} + +func TestAuthWithGlobs(t *testing.T) { + auth := authn.AuthConfig{Username: "root", Password: "root"} + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "*.registry.io", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "fake.registry.io/repo"), authn.FromConfig(auth)) + testResolve(t, kc, registry(t, "blah.registry.io"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "blah.registry.io/repo"), authn.FromConfig(auth)) +} + +func testResolve(t *testing.T, kc authn.Keychain, target authn.Resource, expectedAuth authn.Authenticator) { + t.Helper() + + auth, err := kc.Resolve(target) + if err != nil { + t.Errorf("Resolve(%v) = %v", target, err) + } + got, err := auth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + want, err := expectedAuth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Error("Resolve() diff (-want, +got)\n", diff) + } +} + +func toJSON(t *testing.T, obj any) []byte { + t.Helper() + + bites, err := json.Marshal(obj) + + if err != nil { + t.Fatal("unable to json marshal", err) + } + return bites +} + +func registry(t *testing.T, registry string) authn.Resource { + t.Helper() + + reg, err := name.NewRegistry(registry, name.WeakValidation) + if err != nil { + t.Fatal("failed to create registry", err) + } + return reg +} + +func repo(t *testing.T, repository string) authn.Resource { + t.Helper() + + repo, err := name.NewRepository(repository, name.WeakValidation) + if err != nil { + t.Fatal("failed to create repo", err) + } + return repo +} + +// TestDockerConfigJSON tests using secrets using the .dockerconfigjson form, +// like you might get from running: +// kubectl create secret docker-registry secret -n ns --docker-server="fake.registry.io" --docker-username="foo" --docker-password="bar" +func TestDockerConfigJSON(t *testing.T) { + username, password := "foo", "bar" + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte( + fmt.Sprintf(`{"auths":{"fake.registry.io":{"username":%q,"password":%q,"auth":%q}}}`, + username, password, + base64.StdEncoding.EncodeToString([]byte(username+":"+password))), + ), + }, + }}) + if err != nil { + t.Fatalf("NewFromPullSecrets() = %v", err) + } + + reg, err := name.NewRegistry("fake.registry.io", name.WeakValidation) + if err != nil { + t.Errorf("NewRegistry() = %v", err) + } + + auth, err := kc.Resolve(reg) + if err != nil { + t.Errorf("Resolve(%v) = %v", reg, err) + } + got, err := auth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + want, err := (&authn.Basic{Username: username, Password: password}).Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Resolve() = %v, want %v", got, want) + } +} + +func TestKubernetesAuth(t *testing.T) { + // From https://github.com/knative/serving/issues/12761#issuecomment-1097441770 + // All of these should work with K8s' docker auth parsing. + for k, ss := range map[string][]string{ + "registry.gitlab.com/dprotaso/test/nginx": { + "registry.gitlab.com", + "http://registry.gitlab.com", + "https://registry.gitlab.com", + "registry.gitlab.com/dprotaso", + "http://registry.gitlab.com/dprotaso", + "https://registry.gitlab.com/dprotaso", + "registry.gitlab.com/dprotaso/test", + "http://registry.gitlab.com/dprotaso/test", + "https://registry.gitlab.com/dprotaso/test", + "registry.gitlab.com/dprotaso/test/nginx", + "http://registry.gitlab.com/dprotaso/test/nginx", + "https://registry.gitlab.com/dprotaso/test/nginx", + }, + "dtestcontainer.azurecr.io/dave/nginx": { + "dtestcontainer.azurecr.io", + "http://dtestcontainer.azurecr.io", + "https://dtestcontainer.azurecr.io", + "dtestcontainer.azurecr.io/dave", + "http://dtestcontainer.azurecr.io/dave", + "https://dtestcontainer.azurecr.io/dave", + "dtestcontainer.azurecr.io/dave/nginx", + "http://dtestcontainer.azurecr.io/dave/nginx", + "https://dtestcontainer.azurecr.io/dave/nginx", + }} { + repo, err := name.NewRepository(k) + if err != nil { + t.Errorf("parsing %q: %v", k, err) + continue + } + + for _, s := range ss { + t.Run(fmt.Sprintf("%s - %s", k, s), func(t *testing.T) { + username, password := "foo", "bar" + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte( + fmt.Sprintf(`{"auths":{%q:{"username":%q,"password":%q,"auth":%q}}}`, + s, + username, password, + base64.StdEncoding.EncodeToString([]byte(username+":"+password))), + ), + }, + }}) + if err != nil { + t.Fatalf("NewFromPullSecrets() = %v", err) + } + auth, err := kc.Resolve(repo) + if err != nil { + t.Errorf("Resolve(%v) = %v", repo, err) + } + got, err := auth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + want, err := (&authn.Basic{Username: username, Password: password}).Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Resolve() = %v, want %v", got, want) + } + }) + } + } +} |