From 3d3941c6d89192911c61786b8aeef1e4c93b1273 Mon Sep 17 00:00:00 2001 From: Jonathan MacMillan Date: Mon, 27 Feb 2017 14:49:04 -0800 Subject: [PATCH] Adds support for HTTP basic and token authentication to kubefed. --- federation/cluster/federation-up.sh | 4 +- federation/pkg/kubefed/init/BUILD | 1 + federation/pkg/kubefed/init/init.go | 98 ++++++++++--- federation/pkg/kubefed/init/init_test.go | 134 +++++++++++++----- hack/verify-flags/known-flags.txt | 5 + test/e2e_federation/authn.go | 172 +++++++++++++++++------ 6 files changed, 321 insertions(+), 93 deletions(-) diff --git a/federation/cluster/federation-up.sh b/federation/cluster/federation-up.sh index 21876a4d5f..edbe81813e 100755 --- a/federation/cluster/federation-up.sh +++ b/federation/cluster/federation-up.sh @@ -85,7 +85,9 @@ function init() { --dns-zone-name="${DNS_ZONE_NAME}" \ --dns-provider="${DNS_PROVIDER}" \ --image="${kube_registry}/hyperkube-amd64:${kube_version}" \ - --apiserver-arg-overrides="--storage-backend=etcd2" + --apiserver-arg-overrides="--storage-backend=etcd2" \ + --apiserver-enable-basic-auth=true \ + --apiserver-enable-token-auth=true } # join_clusters joins the clusters in the local kubeconfig to federation. The clusters diff --git a/federation/pkg/kubefed/init/BUILD b/federation/pkg/kubefed/init/BUILD index 8606b56fc2..49775d6b2b 100644 --- a/federation/pkg/kubefed/init/BUILD +++ b/federation/pkg/kubefed/init/BUILD @@ -27,6 +27,7 @@ go_library( "//vendor:github.com/spf13/pflag", "//vendor:k8s.io/apimachinery/pkg/api/resource", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + "//vendor:k8s.io/apimachinery/pkg/util/uuid", "//vendor:k8s.io/apimachinery/pkg/util/wait", "//vendor:k8s.io/client-go/tools/clientcmd", "//vendor:k8s.io/client-go/tools/clientcmd/api", diff --git a/federation/pkg/kubefed/init/init.go b/federation/pkg/kubefed/init/init.go index dd6710aaee..23719cd7ea 100644 --- a/federation/pkg/kubefed/init/init.go +++ b/federation/pkg/kubefed/init/init.go @@ -43,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -148,6 +149,8 @@ type initFederationOptions struct { apiServerServiceTypeString string apiServerServiceType v1.ServiceType apiServerAdvertiseAddress string + apiServerEnableHTTPBasicAuth bool + apiServerEnableTokenAuth bool } func (o *initFederationOptions) Bind(flags *pflag.FlagSet) { @@ -164,6 +167,8 @@ func (o *initFederationOptions) Bind(flags *pflag.FlagSet) { flags.StringVar(&o.controllerManagerOverridesString, "controllermanager-arg-overrides", "", "comma separated list of federation-controller-manager arguments to override: Example \"--arg1=value1,--arg2=value2...\"") flags.StringVar(&o.apiServerServiceTypeString, apiserverServiceTypeFlag, string(v1.ServiceTypeLoadBalancer), "The type of service to create for federation API server. Options: 'LoadBalancer' (default), 'NodePort'.") flags.StringVar(&o.apiServerAdvertiseAddress, apiserverAdvertiseAddressFlag, "", "Preferred address to advertise api server nodeport service. Valid only if '"+apiserverServiceTypeFlag+"=NodePort'.") + flags.BoolVar(&o.apiServerEnableHTTPBasicAuth, "apiserver-enable-basic-auth", false, "Enables HTTP Basic authentication for the federation-apiserver. Defaults to false.") + flags.BoolVar(&o.apiServerEnableTokenAuth, "apiserver-enable-token-auth", false, "Enables token authentication for the federation-apiserver. Defaults to false.") } // NewCmdInit defines the `init` command that bootstraps a federation @@ -196,6 +201,13 @@ type entityKeyPairs struct { admin *triple.KeyPair } +type credentials struct { + username string + password string + token string + certEntKeyPairs *entityKeyPairs +} + // Complete ensures that options are valid and marshals them if necessary. func (i *initFederation) Complete(cmd *cobra.Command, args []string) error { if len(i.options.dnsProvider) == 0 { @@ -274,19 +286,20 @@ func (i *initFederation) Run(cmdOut io.Writer, config util.AdminConfig) error { return err } - // 3. Generate TLS certificates and credentials - entKeyPairs, err := genCerts(i.commonOptions.FederationSystemNamespace, i.commonOptions.Name, svc.Name, HostClusterLocalDNSZoneName, ips, hostnames) + // 3a. Generate TLS certificates and credentials, and other credentials if needed + credentials, err := generateCredentials(i.commonOptions.FederationSystemNamespace, i.commonOptions.Name, svc.Name, HostClusterLocalDNSZoneName, serverCredName, ips, hostnames, i.options.apiServerEnableHTTPBasicAuth, i.options.apiServerEnableTokenAuth, i.options.dryRun) if err != nil { return err } - _, err = createAPIServerCredentialsSecret(hostClientset, i.commonOptions.FederationSystemNamespace, serverCredName, entKeyPairs, i.options.dryRun) + // 3b. Create the secret containing the credentials. + _, err = createAPIServerCredentialsSecret(hostClientset, i.commonOptions.FederationSystemNamespace, serverCredName, credentials, i.options.dryRun) if err != nil { return err } // 4. Create a kubeconfig secret - _, err = createControllerManagerKubeconfigSecret(hostClientset, i.commonOptions.FederationSystemNamespace, i.commonOptions.Name, svc.Name, cmKubeconfigName, entKeyPairs, i.options.dryRun) + _, err = createControllerManagerKubeconfigSecret(hostClientset, i.commonOptions.FederationSystemNamespace, i.commonOptions.Name, svc.Name, cmKubeconfigName, credentials.certEntKeyPairs, i.options.dryRun) if err != nil { return err } @@ -311,7 +324,7 @@ func (i *initFederation) Run(cmdOut io.Writer, config util.AdminConfig) error { } // 6. Create federation API server - _, err = createAPIServer(hostClientset, i.commonOptions.FederationSystemNamespace, serverName, i.options.image, serverCredName, advertiseAddress, i.options.apiServerOverrides, pvc, i.options.dryRun) + _, err = createAPIServer(hostClientset, i.commonOptions.FederationSystemNamespace, serverName, i.options.image, advertiseAddress, serverCredName, i.options.apiServerEnableHTTPBasicAuth, i.options.apiServerEnableTokenAuth, i.options.apiServerOverrides, pvc, i.options.dryRun) if err != nil { return err } @@ -358,7 +371,7 @@ func (i *initFederation) Run(cmdOut io.Writer, config util.AdminConfig) error { // 8. Write the federation API server endpoint info, credentials // and context to kubeconfig - err = updateKubeconfig(config, i.commonOptions.Name, endpoint, i.commonOptions.Kubeconfig, entKeyPairs, i.options.dryRun) + err = updateKubeconfig(config, i.commonOptions.Name, endpoint, i.commonOptions.Kubeconfig, credentials, i.options.dryRun) if err != nil { return err } @@ -498,6 +511,25 @@ func waitForLoadBalancerAddress(clientset client.Interface, svc *api.Service, dr return ips, hostnames, nil } +func generateCredentials(svcNamespace, name, svcName, localDNSZoneName, serverCredName string, ips, hostnames []string, enableHTTPBasicAuth, enableTokenAuth, dryRun bool) (*credentials, error) { + credentials := credentials{ + username: AdminCN, + } + if enableHTTPBasicAuth { + credentials.password = string(uuid.NewUUID()) + } + if enableTokenAuth { + credentials.token = string(uuid.NewUUID()) + } + + entKeyPairs, err := genCerts(svcNamespace, name, svcName, localDNSZoneName, ips, hostnames) + if err != nil { + return nil, err + } + credentials.certEntKeyPairs = entKeyPairs + return &credentials, nil +} + func genCerts(svcNamespace, name, svcName, localDNSZoneName string, ips, hostnames []string) (*entityKeyPairs, error) { ca, err := triple.NewCA(name) if err != nil { @@ -523,18 +555,26 @@ func genCerts(svcNamespace, name, svcName, localDNSZoneName string, ips, hostnam }, nil } -func createAPIServerCredentialsSecret(clientset client.Interface, namespace, credentialsName string, entKeyPairs *entityKeyPairs, dryRun bool) (*api.Secret, error) { +func createAPIServerCredentialsSecret(clientset client.Interface, namespace, credentialsName string, credentials *credentials, dryRun bool) (*api.Secret, error) { // Build the secret object with API server credentials. + data := map[string][]byte{ + "ca.crt": certutil.EncodeCertPEM(credentials.certEntKeyPairs.ca.Cert), + "server.crt": certutil.EncodeCertPEM(credentials.certEntKeyPairs.server.Cert), + "server.key": certutil.EncodePrivateKeyPEM(credentials.certEntKeyPairs.server.Key), + } + if credentials.password != "" { + data["basicauth.csv"] = authFileContents(credentials.username, credentials.password) + } + if credentials.token != "" { + data["token.csv"] = authFileContents(credentials.username, credentials.token) + } + secret := &api.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: credentialsName, Namespace: namespace, }, - Data: map[string][]byte{ - "ca.crt": certutil.EncodeCertPEM(entKeyPairs.ca.Cert), - "server.crt": certutil.EncodeCertPEM(entKeyPairs.server.Cert), - "server.key": certutil.EncodePrivateKeyPEM(entKeyPairs.server.Key), - }, + Data: data, } if dryRun { @@ -591,7 +631,7 @@ func createPVC(clientset client.Interface, namespace, svcName, etcdPVCapacity st return clientset.Core().PersistentVolumeClaims(namespace).Create(pvc) } -func createAPIServer(clientset client.Interface, namespace, name, image, credentialsName, advertiseAddress string, argOverrides map[string]string, pvc *api.PersistentVolumeClaim, dryRun bool) (*extensions.Deployment, error) { +func createAPIServer(clientset client.Interface, namespace, name, image, advertiseAddress, credentialsName string, hasHTTPBasicAuthFile, hasTokenAuthFile bool, argOverrides map[string]string, pvc *api.PersistentVolumeClaim, dryRun bool) (*extensions.Deployment, error) { command := []string{ "/hyperkube", "federation-apiserver", @@ -609,6 +649,12 @@ func createAPIServer(clientset client.Interface, namespace, name, image, credent if advertiseAddress != "" { argsMap["--advertise-address"] = advertiseAddress } + if hasHTTPBasicAuthFile { + argsMap["--basic-auth-file"] = "/etc/federation/apiserver/basicauth.csv" + } + if hasTokenAuthFile { + argsMap["--token-auth-file"] = "/etc/federation/apiserver/token.csv" + } args := argMapsToArgStrings(argsMap, argOverrides) command = append(command, args...) @@ -936,7 +982,7 @@ func printSuccess(cmdOut io.Writer, ips, hostnames []string, svc *api.Service) e return err } -func updateKubeconfig(config util.AdminConfig, name, endpoint, kubeConfigPath string, entKeyPairs *entityKeyPairs, dryRun bool) error { +func updateKubeconfig(config util.AdminConfig, name, endpoint, kubeConfigPath string, credentials *credentials, dryRun bool) error { po := config.PathOptions() po.LoadingRules.ExplicitPath = kubeConfigPath kubeconfig, err := po.GetStartingConfig() @@ -951,13 +997,20 @@ func updateKubeconfig(config util.AdminConfig, name, endpoint, kubeConfigPath st endpoint = fmt.Sprintf("https://%s", endpoint) } cluster.Server = endpoint - cluster.CertificateAuthorityData = certutil.EncodeCertPEM(entKeyPairs.ca.Cert) + cluster.CertificateAuthorityData = certutil.EncodeCertPEM(credentials.certEntKeyPairs.ca.Cert) // Populate credentials. authInfo := clientcmdapi.NewAuthInfo() - authInfo.ClientCertificateData = certutil.EncodeCertPEM(entKeyPairs.admin.Cert) - authInfo.ClientKeyData = certutil.EncodePrivateKeyPEM(entKeyPairs.admin.Key) - authInfo.Username = AdminCN + authInfo.ClientCertificateData = certutil.EncodeCertPEM(credentials.certEntKeyPairs.admin.Cert) + authInfo.ClientKeyData = certutil.EncodePrivateKeyPEM(credentials.certEntKeyPairs.admin.Key) + authInfo.Token = credentials.token + + var httpBasicAuthInfo *clientcmdapi.AuthInfo + if credentials.password != "" { + httpBasicAuthInfo = clientcmdapi.NewAuthInfo() + httpBasicAuthInfo.Password = credentials.password + httpBasicAuthInfo.Username = credentials.username + } // Populate context. context := clientcmdapi.NewContext() @@ -968,6 +1021,9 @@ func updateKubeconfig(config util.AdminConfig, name, endpoint, kubeConfigPath st // credentials and context. kubeconfig.Clusters[name] = cluster kubeconfig.AuthInfos[name] = authInfo + if httpBasicAuthInfo != nil { + kubeconfig.AuthInfos[fmt.Sprintf("%s-basic-auth", name)] = httpBasicAuthInfo + } kubeconfig.Contexts[name] = context if !dryRun { @@ -1034,3 +1090,9 @@ func addDNSProviderConfig(dep *extensions.Deployment, secretName string) *extens return dep } + +// authFileContents returns a CSV string containing the contents of an +// authentication file in the format required by the federation-apiserver. +func authFileContents(username, authSecret string) []byte { + return []byte(fmt.Sprintf("%s,%s,%s\n", authSecret, username, uuid.NewUUID())) +} diff --git a/federation/pkg/kubefed/init/init_test.go b/federation/pkg/kubefed/init/init_test.go index b39e19b679..6fa2284114 100644 --- a/federation/pkg/kubefed/init/init_test.go +++ b/federation/pkg/kubefed/init/init_test.go @@ -79,21 +79,23 @@ func TestInitFederation(t *testing.T) { defer kubefedtesting.RemoveFakeKubeconfigFiles(fakeKubeFiles) testCases := []struct { - federation string - kubeconfigGlobal string - kubeconfigExplicit string - dnsZoneName string - lbIP string - apiserverServiceType v1.ServiceType - advertiseAddress string - image string - etcdPVCapacity string - etcdPersistence string - expectedErr string - dnsProviderConfig string - dryRun string - apiserverArgOverrides string - cmArgOverrides string + federation string + kubeconfigGlobal string + kubeconfigExplicit string + dnsZoneName string + lbIP string + apiserverServiceType v1.ServiceType + advertiseAddress string + image string + etcdPVCapacity string + etcdPersistence string + expectedErr string + dnsProviderConfig string + dryRun string + apiserverArgOverrides string + cmArgOverrides string + apiserverEnableHTTPBasicAuth bool + apiserverEnableTokenAuth bool }{ { federation: "union", @@ -175,6 +177,21 @@ func TestInitFederation(t *testing.T) { expectedErr: "", dryRun: "", }, + { + federation: "union", + kubeconfigGlobal: fakeKubeFiles[0], + kubeconfigExplicit: "", + dnsZoneName: "example.test.", + apiserverServiceType: v1.ServiceTypeNodePort, + advertiseAddress: nodeIP, + image: "example.test/foo:bar", + etcdPVCapacity: "5Gi", + etcdPersistence: "true", + expectedErr: "", + dryRun: "", + apiserverEnableHTTPBasicAuth: true, + apiserverEnableTokenAuth: true, + }, } //TODO: implement a negative case for dry run @@ -191,7 +208,7 @@ func TestInitFederation(t *testing.T) { tc.dnsProviderConfig = tmpfile.Name() defer os.Remove(tmpfile.Name()) } - hostFactory, err := fakeInitHostFactory(tc.apiserverServiceType, tc.federation, util.DefaultFederationSystemNamespace, tc.advertiseAddress, tc.lbIP, tc.dnsZoneName, tc.image, dnsProvider, tc.dnsProviderConfig, tc.etcdPersistence, tc.etcdPVCapacity, tc.apiserverArgOverrides, tc.cmArgOverrides) + hostFactory, err := fakeInitHostFactory(tc.apiserverServiceType, tc.federation, util.DefaultFederationSystemNamespace, tc.advertiseAddress, tc.lbIP, tc.dnsZoneName, tc.image, dnsProvider, tc.dnsProviderConfig, tc.etcdPersistence, tc.etcdPVCapacity, tc.apiserverArgOverrides, tc.cmArgOverrides, tc.apiserverEnableHTTPBasicAuth, tc.apiserverEnableTokenAuth) if err != nil { t.Fatalf("[%d] unexpected error: %v", i, err) } @@ -227,6 +244,12 @@ func TestInitFederation(t *testing.T) { if tc.dryRun == "valid-run" { cmd.Flags().Set("dry-run", "true") } + if tc.apiserverEnableHTTPBasicAuth { + cmd.Flags().Set("apiserver-enable-basic-auth", "true") + } + if tc.apiserverEnableTokenAuth { + cmd.Flags().Set("apiserver-enable-token-auth", "true") + } cmd.Run(cmd, []string{tc.federation}) @@ -253,7 +276,7 @@ func TestInitFederation(t *testing.T) { return } - testKubeconfigUpdate(t, tc.apiserverServiceType, tc.federation, tc.advertiseAddress, tc.lbIP, tc.kubeconfigGlobal, tc.kubeconfigExplicit) + testKubeconfigUpdate(t, tc.apiserverServiceType, tc.federation, tc.advertiseAddress, tc.lbIP, tc.kubeconfigGlobal, tc.kubeconfigExplicit, tc.apiserverEnableHTTPBasicAuth, tc.apiserverEnableTokenAuth) } } @@ -554,7 +577,7 @@ func TestCertsHTTPS(t *testing.T) { } } -func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, namespaceName, advertiseAddress, lbIp, dnsZoneName, image, dnsProvider, dnsProviderConfig, etcdPersistence, etcdPVCapacity, apiserverOverrideArg, cmOverrideArg string) (cmdutil.Factory, error) { +func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, namespaceName, advertiseAddress, lbIp, dnsZoneName, image, dnsProvider, dnsProviderConfig, etcdPersistence, etcdPVCapacity, apiserverOverrideArg, cmOverrideArg string, apiserverEnableHTTPBasicAuth, apiserverEnableTokenAuth bool) (cmdutil.Factory, error) { svcName := federationName + "-apiserver" svcUrlPrefix := "/api/v1/namespaces/federation-system/services" credSecretName := svcName + "-credentials" @@ -782,6 +805,12 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na } else { apiserverArgs = append(apiserverArgs, "--client-ca-file=/etc/federation/apiserver/ca.crt") } + if apiserverEnableHTTPBasicAuth { + apiserverArgs = append(apiserverArgs, "--basic-auth-file=/etc/federation/apiserver/basicauth.csv") + } + if apiserverEnableTokenAuth { + apiserverArgs = append(apiserverArgs, "--token-auth-file=/etc/federation/apiserver/token.csv") + } sort.Strings(apiserverArgs) apiserverCommand = append(apiserverCommand, apiserverArgs...) @@ -1018,7 +1047,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na return nil, err } if !apiequality.Semantic.DeepEqual(got, namespace) { - return nil, fmt.Errorf("Unexpected namespace object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, namespace)) + return nil, fmt.Errorf("unexpected namespace object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, namespace)) } return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &namespace)}, nil case p == svcUrlPrefix && m == http.MethodPost: @@ -1032,7 +1061,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na return nil, err } if !apiequality.Semantic.DeepEqual(got, svc) { - return nil, fmt.Errorf("Unexpected service object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, svc)) + return nil, fmt.Errorf("unexpected service object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, svc)) } if apiserverServiceType == v1.ServiceTypeNodePort { svc.Spec.Type = v1.ServiceTypeNodePort @@ -1055,21 +1084,36 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na if err != nil { return nil, err } - // Obtained secret contains generated data which cannot - // be compared, so we just nullify the generated part - // and compare the rest of the secret. The generated - // parts are tested in other tests. - got.Data = nil + switch got.Name { case credSecretName: want = credSecret + if apiserverEnableHTTPBasicAuth { + if got.Data["basicauth.csv"] == nil { + return nil, fmt.Errorf("expected secret data key 'basicauth.csv', but got nil") + } + } else { + if got.Data["basicauth.csv"] != nil { + return nil, fmt.Errorf("unexpected secret data key 'basicauth.csv'") + } + } + if apiserverEnableTokenAuth { + if got.Data["token.csv"] == nil { + return nil, fmt.Errorf("expected secret data key 'token.csv', but got nil") + } + } else { + if got.Data["token.csv"] != nil { + return nil, fmt.Errorf("unexpected secret data key 'token.csv'") + } + } case cmKubeconfigSecretName: want = cmKubeconfigSecret case dnsProviderSecretName: want = cmDNSProviderSecret } + got.Data = nil if !apiequality.Semantic.DeepEqual(got, want) { - return nil, fmt.Errorf("Unexpected secret object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, want)) + return nil, fmt.Errorf("unexpected secret object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, want)) } return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &want)}, nil case p == "/api/v1/namespaces/federation-system/persistentvolumeclaims" && m == http.MethodPost: @@ -1083,7 +1127,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na return nil, err } if !apiequality.Semantic.DeepEqual(got, pvc) { - return nil, fmt.Errorf("Unexpected PVC object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, pvc)) + return nil, fmt.Errorf("unexpected PVC object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, pvc)) } return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &pvc)}, nil case p == "/apis/extensions/v1beta1/namespaces/federation-system/deployments" && m == http.MethodPost: @@ -1103,7 +1147,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na want = *cm } if !apiequality.Semantic.DeepEqual(got, want) { - return nil, fmt.Errorf("Unexpected deployment object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, want)) + return nil, fmt.Errorf("unexpected deployment object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, want)) } return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(extCodec, &want)}, nil case p == "/api/v1/namespaces/federation-system/pods" && m == http.MethodGet: @@ -1119,7 +1163,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na return nil, err } if !api.Semantic.DeepEqual(got, sa) { - return nil, fmt.Errorf("Unexpected service account object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, sa)) + return nil, fmt.Errorf("unexpected service account object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, sa)) } return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &sa)}, nil case p == "/apis/rbac.authorization.k8s.io/v1beta1/namespaces/federation-system/roles" && m == http.MethodPost: @@ -1133,7 +1177,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na return nil, err } if !api.Semantic.DeepEqual(got, role) { - return nil, fmt.Errorf("Unexpected role object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, role)) + return nil, fmt.Errorf("unexpected role object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, role)) } return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(rbacCodec, &role)}, nil case p == "/apis/rbac.authorization.k8s.io/v1beta1/namespaces/federation-system/rolebindings" && m == http.MethodPost: @@ -1147,7 +1191,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na return nil, err } if !api.Semantic.DeepEqual(got, rolebinding) { - return nil, fmt.Errorf("Unexpected rolebinding object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, rolebinding)) + return nil, fmt.Errorf("unexpected rolebinding object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, rolebinding)) } return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(rbacCodec, &rolebinding)}, nil case p == "/api/v1/nodes" && m == http.MethodGet: @@ -1160,7 +1204,7 @@ func fakeInitHostFactory(apiserverServiceType v1.ServiceType, federationName, na return f, nil } -func testKubeconfigUpdate(t *testing.T, apiserverServiceType v1.ServiceType, federationName, advertiseAddress, lbIP, kubeconfigGlobal, kubeconfigExplicit string) { +func testKubeconfigUpdate(t *testing.T, apiserverServiceType v1.ServiceType, federationName, advertiseAddress, lbIP, kubeconfigGlobal, kubeconfigExplicit string, apiserverEnableHTTPBasicAuth, apiserverEnableTokenAuth bool) { filename := kubeconfigGlobal if kubeconfigExplicit != "" { filename = kubeconfigExplicit @@ -1197,8 +1241,30 @@ func testKubeconfigUpdate(t *testing.T, apiserverServiceType v1.ServiceType, fed t.Errorf("Expected client key to be non-empty") return } - if authInfo.Username != AdminCN { - t.Errorf("Want username: %q, got: %q", AdminCN, authInfo.Username) + if !apiserverEnableTokenAuth && len(authInfo.Token) != 0 { + t.Errorf("Expected token to be empty: got: %s", authInfo.Token) + } + if apiserverEnableTokenAuth && len(authInfo.Token) == 0 { + t.Errorf("Expected token to be non-empty") + } + + httpBasicAuthInfo, ok := config.AuthInfos[fmt.Sprintf("%s-basic-auth", federationName)] + if !apiserverEnableHTTPBasicAuth && ok { + t.Errorf("Expected basic auth AuthInfo entry not to exist: got %v", httpBasicAuthInfo) + return + } + + if apiserverEnableHTTPBasicAuth { + if !ok { + t.Errorf("Expected basic auth AuthInfo entry to exist") + return + } + if httpBasicAuthInfo.Username != "admin" { + t.Errorf("Unexpected username in basic auth AuthInfo entry: got %s, want admin", httpBasicAuthInfo.Username) + } + if len(httpBasicAuthInfo.Password) == 0 { + t.Errorf("Expected basic auth AuthInfo entry to contain password") + } } context, ok := config.Contexts[federationName] diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index fab6918993..47d909d325 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -31,6 +31,11 @@ api-servers api-server-service-type api-token api-version +apiserver-arg-overrides +apiserver-count +apiserver-count +apiserver-enable-basic-auth +apiserver-enable-token-auth attach-detach-reconcile-sync-period audit-log-maxage audit-log-maxbackup diff --git a/test/e2e_federation/authn.go b/test/e2e_federation/authn.go index cac8032267..ee55ddbddf 100644 --- a/test/e2e_federation/authn.go +++ b/test/e2e_federation/authn.go @@ -21,7 +21,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/kubernetes/federation/client/clientset_generated/federation_clientset" "k8s.io/kubernetes/test/e2e/framework" fedframework "k8s.io/kubernetes/test/e2e_federation/framework" @@ -30,6 +29,8 @@ import ( . "github.com/onsi/gomega" ) +// TODO: These tests should be integration tests rather than e2e tests, when the +// integration test harness is ready. var _ = framework.KubeDescribe("[Feature:Federation]", func() { f := fedframework.NewDefaultFederatedFramework("federation-apiserver-authn") @@ -38,72 +39,163 @@ var _ = framework.KubeDescribe("[Feature:Federation]", func() { fedframework.SkipUnlessFederated(f.ClientSet) }) - It("should accept cluster resources when the client has right authentication credentials", func() { - fedframework.SkipUnlessFederated(f.ClientSet) + It("should accept cluster resources when the client has certificate authentication credentials", func() { + fcs, err := federationClientSetWithCert() + framework.ExpectNoError(err) nsName := f.FederationNamespace.Name - svc := createServiceOrFail(f.FederationClientset, nsName, FederatedServiceName) + svc := createServiceOrFail(fcs, nsName, FederatedServiceName) deleteServiceOrFail(f.FederationClientset, nsName, svc.Name, nil) }) - It("should not accept cluster resources when the client has invalid authentication credentials", func() { - fedframework.SkipUnlessFederated(f.ClientSet) - - contexts := f.GetUnderlyingFederatedContexts() - - // `contexts` is obtained by calling - // `f.GetUnderlyingFederatedContexts()`. This function in turn - // checks that the contexts it returns does not include the - // federation API server context. So `contexts` is guaranteed to - // contain only the underlying Kubernetes cluster contexts. - fcs, err := invalidAuthFederationClientSet(contexts[0].User) + It("should accept cluster resources when the client has HTTP Basic authentication credentials", func() { + fcs, err := federationClientSetWithBasicAuth(true /* valid */) framework.ExpectNoError(err) nsName := f.FederationNamespace.Name svc, err := createService(fcs, nsName, FederatedServiceName) - Expect(errors.IsUnauthorized(err)).To(BeTrue()) - if err == nil && svc != nil { - deleteServiceOrFail(fcs, nsName, svc.Name, nil) - } + Expect(err).NotTo(HaveOccurred()) + deleteServiceOrFail(fcs, nsName, svc.Name, nil) + }) + + It("should accept cluster resources when the client has token authentication credentials", func() { + fcs, err := federationClientSetWithToken(true /* valid */) + framework.ExpectNoError(err) + + nsName := f.FederationNamespace.Name + svc, err := createService(fcs, nsName, FederatedServiceName) + Expect(err).NotTo(HaveOccurred()) + deleteServiceOrFail(fcs, nsName, svc.Name, nil) }) It("should not accept cluster resources when the client has no authentication credentials", func() { - fedframework.SkipUnlessFederated(f.ClientSet) - - fcs, err := invalidAuthFederationClientSet(nil) + fcs, err := unauthenticatedFederationClientSet() framework.ExpectNoError(err) nsName := f.FederationNamespace.Name - svc, err := createService(fcs, nsName, FederatedServiceName) + _, err = createService(fcs, nsName, FederatedServiceName) Expect(errors.IsUnauthorized(err)).To(BeTrue()) - if err == nil && svc != nil { - deleteServiceOrFail(fcs, nsName, svc.Name, nil) - } }) + + // TODO: Add a test for invalid certificate credentials. The certificate is validated for + // correct format, so it cannot contain random noise. + + It("should not accept cluster resources when the client has invalid HTTP Basic authentication credentials", func() { + fcs, err := federationClientSetWithBasicAuth(false /* invalid */) + framework.ExpectNoError(err) + + nsName := f.FederationNamespace.Name + _, err = createService(fcs, nsName, FederatedServiceName) + Expect(errors.IsUnauthorized(err)).To(BeTrue()) + }) + + It("should not accept cluster resources when the client has invalid token authentication credentials", func() { + fcs, err := federationClientSetWithToken(false /* invalid */) + framework.ExpectNoError(err) + + nsName := f.FederationNamespace.Name + _, err = createService(fcs, nsName, FederatedServiceName) + Expect(errors.IsUnauthorized(err)).To(BeTrue()) + }) + }) }) -func invalidAuthFederationClientSet(user *framework.KubeUser) (*federation_clientset.Clientset, error) { - overrides := &clientcmd.ConfigOverrides{} - if user != nil { - overrides = &clientcmd.ConfigOverrides{ - AuthInfo: clientcmdapi.AuthInfo{ - Token: user.User.Token, - Username: user.User.Username, - Password: user.User.Password, - }, - } +// unauthenticatedFederationClientSet returns a Federation Clientset configured with +// no authentication credentials. +func unauthenticatedFederationClientSet() (*federation_clientset.Clientset, error) { + config, err := fedframework.LoadFederatedConfig(&clientcmd.ConfigOverrides{}) + if err != nil { + return nil, err + } + config.Insecure = true + config.CAData = []byte{} + config.CertData = []byte{} + config.KeyData = []byte{} + config.BearerToken = "" + + c, err := federation_clientset.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("error creating federation clientset: %v", err) } - config, err := fedframework.LoadFederatedConfig(overrides) + return c, nil +} + +// federationClientSetWithCert returns a Federation Clientset configured with +// certificate authentication credentials. +func federationClientSetWithCert() (*federation_clientset.Clientset, error) { + config, err := fedframework.LoadFederatedConfig(&clientcmd.ConfigOverrides{}) if err != nil { return nil, err } - if user == nil { - config.Password = "" - config.BearerToken = "" + config.BearerToken = "" + + c, err := federation_clientset.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("error creating federation clientset: %v", err) + } + + return c, nil +} + +// federationClientSetWithBasicAuth returns a Federation Clientset configured with +// HTTP Basic authentication credentials. +func federationClientSetWithBasicAuth(valid bool) (*federation_clientset.Clientset, error) { + config, err := fedframework.LoadFederatedConfig(&clientcmd.ConfigOverrides{}) + if err != nil { + return nil, err + } + + config.Insecure = true + config.CAData = []byte{} + config.CertData = []byte{} + config.KeyData = []byte{} + config.BearerToken = "" + + if !valid { config.Username = "" + config.Password = "" + } else { + // This is a hacky approach to getting the basic auth credentials, but since + // the token and the username/password cannot live in the same AuthInfo object, + // and because we do not want to store basic auth credentials with token and + // certificate credentials for security reasons, we must dig it out by hand. + c, err := framework.RestclientConfig(framework.TestContext.FederatedKubeContext) + if err != nil { + return nil, err + } + if authInfo, ok := c.AuthInfos[fmt.Sprintf("%s-basic-auth", framework.TestContext.FederatedKubeContext)]; ok { + config.Username = authInfo.Username + config.Password = authInfo.Password + } + } + + c, err := federation_clientset.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("error creating federation clientset: %v", err) + } + + return c, nil +} + +// federationClientSetWithToken returns a Federation Clientset configured with +// token authentication credentials. +func federationClientSetWithToken(valid bool) (*federation_clientset.Clientset, error) { + config, err := fedframework.LoadFederatedConfig(&clientcmd.ConfigOverrides{}) + if err != nil { + return nil, err + } + config.Insecure = true + config.CAData = []byte{} + config.CertData = []byte{} + config.KeyData = []byte{} + config.Username = "" + config.Password = "" + + if !valid { + config.BearerToken = "invalid" } c, err := federation_clientset.NewForConfig(config)